aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video.ts
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-08-25 11:36:23 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-08-25 11:36:23 +0200
commit93e1258c7cbc0d1235ca6d2a1f7c1875985328b8 (patch)
treeb0a1f77af7ab54dc5f58f569fcd1e9d84b04c533 /server/models/video/video.ts
parent69f224587e99d56008e1fa129d0641840a486620 (diff)
downloadPeerTube-93e1258c7cbc0d1235ca6d2a1f7c1875985328b8.tar.gz
PeerTube-93e1258c7cbc0d1235ca6d2a1f7c1875985328b8.tar.zst
PeerTube-93e1258c7cbc0d1235ca6d2a1f7c1875985328b8.zip
Move video file metadata in their own table
Will be used for user video quotas and multiple video resolutions
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r--server/models/video/video.ts434
1 files changed, 223 insertions, 211 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index b7eb24c4a..1e4bdf51c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -2,13 +2,12 @@ import * as safeBuffer from 'safe-buffer'
2const Buffer = safeBuffer.Buffer 2const Buffer = safeBuffer.Buffer
3import * as ffmpeg from 'fluent-ffmpeg' 3import * as ffmpeg from 'fluent-ffmpeg'
4import * as magnetUtil from 'magnet-uri' 4import * as magnetUtil from 'magnet-uri'
5import { map, values } from 'lodash' 5import { map } from 'lodash'
6import * as parseTorrent from 'parse-torrent' 6import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 7import { join } from 'path'
8import * as Sequelize from 'sequelize' 8import * as Sequelize from 'sequelize'
9import * as Promise from 'bluebird' 9import * as Promise from 'bluebird'
10 10
11import { database as db } from '../../initializers/database'
12import { TagInstance } from './tag-interface' 11import { TagInstance } from './tag-interface'
13import { 12import {
14 logger, 13 logger,
@@ -18,7 +17,6 @@ import {
18 isVideoLanguageValid, 17 isVideoLanguageValid,
19 isVideoNSFWValid, 18 isVideoNSFWValid,
20 isVideoDescriptionValid, 19 isVideoDescriptionValid,
21 isVideoInfoHashValid,
22 isVideoDurationValid, 20 isVideoDurationValid,
23 readFileBufferPromise, 21 readFileBufferPromise,
24 unlinkPromise, 22 unlinkPromise,
@@ -27,16 +25,17 @@ import {
27 createTorrentPromise 25 createTorrentPromise
28} from '../../helpers' 26} from '../../helpers'
29import { 27import {
30 CONSTRAINTS_FIELDS,
31 CONFIG, 28 CONFIG,
32 REMOTE_SCHEME, 29 REMOTE_SCHEME,
33 STATIC_PATHS, 30 STATIC_PATHS,
34 VIDEO_CATEGORIES, 31 VIDEO_CATEGORIES,
35 VIDEO_LICENCES, 32 VIDEO_LICENCES,
36 VIDEO_LANGUAGES, 33 VIDEO_LANGUAGES,
37 THUMBNAILS_SIZE 34 THUMBNAILS_SIZE,
35 VIDEO_FILE_RESOLUTIONS
38} from '../../initializers' 36} from '../../initializers'
39import { JobScheduler, removeVideoToFriends } from '../../lib' 37import { removeVideoToFriends } from '../../lib'
38import { VideoFileInstance } from './video-file-interface'
40 39
41import { addMethodsToModel, getSort } from '../utils' 40import { addMethodsToModel, getSort } from '../utils'
42import { 41import {
@@ -51,12 +50,16 @@ let generateMagnetUri: VideoMethods.GenerateMagnetUri
51let getVideoFilename: VideoMethods.GetVideoFilename 50let getVideoFilename: VideoMethods.GetVideoFilename
52let getThumbnailName: VideoMethods.GetThumbnailName 51let getThumbnailName: VideoMethods.GetThumbnailName
53let getPreviewName: VideoMethods.GetPreviewName 52let getPreviewName: VideoMethods.GetPreviewName
54let getTorrentName: VideoMethods.GetTorrentName 53let getTorrentFileName: VideoMethods.GetTorrentFileName
55let isOwned: VideoMethods.IsOwned 54let isOwned: VideoMethods.IsOwned
56let toFormatedJSON: VideoMethods.ToFormatedJSON 55let toFormatedJSON: VideoMethods.ToFormatedJSON
57let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 56let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
58let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 57let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
59let transcodeVideofile: VideoMethods.TranscodeVideofile 58let transcodeVideofile: VideoMethods.TranscodeVideofile
59let createPreview: VideoMethods.CreatePreview
60let createThumbnail: VideoMethods.CreateThumbnail
61let getVideoFilePath: VideoMethods.GetVideoFilePath
62let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
60 63
61let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 64let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
62let getDurationFromFile: VideoMethods.GetDurationFromFile 65let getDurationFromFile: VideoMethods.GetDurationFromFile
@@ -71,6 +74,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
71let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 74let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
72let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 75let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
73let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 76let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
77let removeThumbnail: VideoMethods.RemoveThumbnail
78let removePreview: VideoMethods.RemovePreview
79let removeFile: VideoMethods.RemoveFile
80let removeTorrent: VideoMethods.RemoveTorrent
74 81
75export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 82export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
76 Video = sequelize.define<VideoInstance, VideoAttributes>('Video', 83 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
@@ -93,10 +100,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
93 } 100 }
94 } 101 }
95 }, 102 },
96 extname: {
97 type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
98 allowNull: false
99 },
100 category: { 103 category: {
101 type: DataTypes.INTEGER, 104 type: DataTypes.INTEGER,
102 allowNull: false, 105 allowNull: false,
@@ -148,16 +151,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
148 } 151 }
149 } 152 }
150 }, 153 },
151 infoHash: {
152 type: DataTypes.STRING,
153 allowNull: false,
154 validate: {
155 infoHashValid: value => {
156 const res = isVideoInfoHashValid(value)
157 if (res === false) throw new Error('Video info hash is not valid.')
158 }
159 }
160 },
161 duration: { 154 duration: {
162 type: DataTypes.INTEGER, 155 type: DataTypes.INTEGER,
163 allowNull: false, 156 allowNull: false,
@@ -216,9 +209,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
216 fields: [ 'duration' ] 209 fields: [ 'duration' ]
217 }, 210 },
218 { 211 {
219 fields: [ 'infoHash' ]
220 },
221 {
222 fields: [ 'views' ] 212 fields: [ 'views' ]
223 }, 213 },
224 { 214 {
@@ -229,8 +219,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
229 } 219 }
230 ], 220 ],
231 hooks: { 221 hooks: {
232 beforeValidate,
233 beforeCreate,
234 afterDestroy 222 afterDestroy
235 } 223 }
236 } 224 }
@@ -246,23 +234,30 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
246 listOwnedAndPopulateAuthorAndTags, 234 listOwnedAndPopulateAuthorAndTags,
247 listOwnedByAuthor, 235 listOwnedByAuthor,
248 load, 236 load,
249 loadByUUID,
250 loadByHostAndUUID,
251 loadAndPopulateAuthor, 237 loadAndPopulateAuthor,
252 loadAndPopulateAuthorAndPodAndTags, 238 loadAndPopulateAuthorAndPodAndTags,
239 loadByHostAndUUID,
240 loadByUUID,
253 loadByUUIDAndPopulateAuthorAndPodAndTags, 241 loadByUUIDAndPopulateAuthorAndPodAndTags,
254 searchAndPopulateAuthorAndPodAndTags, 242 searchAndPopulateAuthorAndPodAndTags
255 removeFromBlacklist
256 ] 243 ]
257 const instanceMethods = [ 244 const instanceMethods = [
245 createPreview,
246 createThumbnail,
247 createTorrentAndSetInfoHash,
258 generateMagnetUri, 248 generateMagnetUri,
259 getVideoFilename,
260 getThumbnailName,
261 getPreviewName, 249 getPreviewName,
262 getTorrentName, 250 getThumbnailName,
251 getTorrentFileName,
252 getVideoFilename,
253 getVideoFilePath,
263 isOwned, 254 isOwned,
264 toFormatedJSON, 255 removeFile,
256 removePreview,
257 removeThumbnail,
258 removeTorrent,
265 toAddRemoteJSON, 259 toAddRemoteJSON,
260 toFormatedJSON,
266 toUpdateRemoteJSON, 261 toUpdateRemoteJSON,
267 transcodeVideofile 262 transcodeVideofile
268 ] 263 ]
@@ -271,65 +266,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
271 return Video 266 return Video
272} 267}
273 268
274function beforeValidate (video: VideoInstance) {
275 // Put a fake infoHash if it does not exists yet
276 if (video.isOwned() && !video.infoHash) {
277 // 40 hexa length
278 video.infoHash = '0123456789abcdef0123456789abcdef01234567'
279 }
280}
281
282function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
283 if (video.isOwned()) {
284 const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
285 const tasks = []
286
287 tasks.push(
288 createTorrentFromVideo(video, videoPath),
289 createThumbnail(video, videoPath),
290 createPreview(video, videoPath)
291 )
292
293 if (CONFIG.TRANSCODING.ENABLED === true) {
294 // Put uuid because we don't have id auto incremented for now
295 const dataInput = {
296 videoUUID: video.uuid
297 }
298
299 tasks.push(
300 JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput)
301 )
302 }
303
304 return Promise.all(tasks)
305 }
306
307 return Promise.resolve()
308}
309
310function afterDestroy (video: VideoInstance) {
311 const tasks = []
312
313 tasks.push(
314 removeThumbnail(video)
315 )
316
317 if (video.isOwned()) {
318 const removeVideoToFriendsParams = {
319 uuid: video.uuid
320 }
321
322 tasks.push(
323 removeFile(video),
324 removeTorrent(video),
325 removePreview(video),
326 removeVideoToFriends(removeVideoToFriendsParams)
327 )
328 }
329
330 return Promise.all(tasks)
331}
332
333// ------------------------------ METHODS ------------------------------ 269// ------------------------------ METHODS ------------------------------
334 270
335function associate (models) { 271function associate (models) {
@@ -354,37 +290,46 @@ function associate (models) {
354 }, 290 },
355 onDelete: 'cascade' 291 onDelete: 'cascade'
356 }) 292 })
293
294 Video.hasMany(models.VideoFile, {
295 foreignKey: {
296 name: 'videoId',
297 allowNull: false
298 },
299 onDelete: 'cascade'
300 })
357} 301}
358 302
359generateMagnetUri = function (this: VideoInstance) { 303function afterDestroy (video: VideoInstance) {
360 let baseUrlHttp 304 const tasks = []
361 let baseUrlWs
362 305
363 if (this.isOwned()) { 306 tasks.push(
364 baseUrlHttp = CONFIG.WEBSERVER.URL 307 video.removeThumbnail()
365 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 308 )
366 } else {
367 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
368 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
369 }
370 309
371 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() 310 if (video.isOwned()) {
372 const announce = [ baseUrlWs + '/tracker/socket' ] 311 const removeVideoToFriendsParams = {
373 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] 312 uuid: video.uuid
313 }
374 314
375 const magnetHash = { 315 tasks.push(
376 xs, 316 video.removePreview(),
377 announce, 317 removeVideoToFriends(removeVideoToFriendsParams)
378 urlList, 318 )
379 infoHash: this.infoHash, 319
380 name: this.name 320 // TODO: check files is populated
321 video.VideoFiles.forEach(file => {
322 video.removeFile(file),
323 video.removeTorrent(file)
324 })
381 } 325 }
382 326
383 return magnetUtil.encode(magnetHash) 327 return Promise.all(tasks)
384} 328}
385 329
386getVideoFilename = function (this: VideoInstance) { 330getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
387 return this.uuid + this.extname 331 // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
332 return this.uuid + videoFile.extname
388} 333}
389 334
390getThumbnailName = function (this: VideoInstance) { 335getThumbnailName = function (this: VideoInstance) {
@@ -398,8 +343,9 @@ getPreviewName = function (this: VideoInstance) {
398 return this.uuid + extension 343 return this.uuid + extension
399} 344}
400 345
401getTorrentName = function (this: VideoInstance) { 346getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
402 const extension = '.torrent' 347 const extension = '.torrent'
348 // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
403 return this.uuid + extension 349 return this.uuid + extension
404} 350}
405 351
@@ -407,6 +353,67 @@ isOwned = function (this: VideoInstance) {
407 return this.remote === false 353 return this.remote === false
408} 354}
409 355
356createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
357 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null)
358}
359
360createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
361 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE)
362}
363
364getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
365 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
366}
367
368createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
369 const options = {
370 announceList: [
371 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
372 ],
373 urlList: [
374 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
375 ]
376 }
377
378 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
379 .then(torrent => {
380 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
381 return writeFilePromise(filePath, torrent).then(() => torrent)
382 })
383 .then(torrent => {
384 const parsedTorrent = parseTorrent(torrent)
385
386 videoFile.infoHash = parsedTorrent.infoHash
387 })
388}
389
390generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) {
391 let baseUrlHttp
392 let baseUrlWs
393
394 if (this.isOwned()) {
395 baseUrlHttp = CONFIG.WEBSERVER.URL
396 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
397 } else {
398 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
399 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
400 }
401
402 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
403 const announce = [ baseUrlWs + '/tracker/socket' ]
404 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
405
406 const magnetHash = {
407 xs,
408 announce,
409 urlList,
410 infoHash: videoFile.infoHash,
411 name: this.name
412 }
413
414 return magnetUtil.encode(magnetHash)
415}
416
410toFormatedJSON = function (this: VideoInstance) { 417toFormatedJSON = function (this: VideoInstance) {
411 let podHost 418 let podHost
412 419
@@ -443,7 +450,6 @@ toFormatedJSON = function (this: VideoInstance) {
443 description: this.description, 450 description: this.description,
444 podHost, 451 podHost,
445 isLocal: this.isOwned(), 452 isLocal: this.isOwned(),
446 magnetUri: this.generateMagnetUri(),
447 author: this.Author.name, 453 author: this.Author.name,
448 duration: this.duration, 454 duration: this.duration,
449 views: this.views, 455 views: this.views,
@@ -453,9 +459,24 @@ toFormatedJSON = function (this: VideoInstance) {
453 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), 459 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
454 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), 460 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
455 createdAt: this.createdAt, 461 createdAt: this.createdAt,
456 updatedAt: this.updatedAt 462 updatedAt: this.updatedAt,
463 files: []
457 } 464 }
458 465
466 this.VideoFiles.forEach(videoFile => {
467 let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
468 if (!resolutionLabel) resolutionLabel = 'Unknown'
469
470 const videoFileJson = {
471 resolution: videoFile.resolution,
472 resolutionLabel,
473 magnetUri: this.generateMagnetUri(videoFile),
474 size: videoFile.size
475 }
476
477 json.files.push(videoFileJson)
478 })
479
459 return json 480 return json
460} 481}
461 482
@@ -472,19 +493,27 @@ toAddRemoteJSON = function (this: VideoInstance) {
472 language: this.language, 493 language: this.language,
473 nsfw: this.nsfw, 494 nsfw: this.nsfw,
474 description: this.description, 495 description: this.description,
475 infoHash: this.infoHash,
476 author: this.Author.name, 496 author: this.Author.name,
477 duration: this.duration, 497 duration: this.duration,
478 thumbnailData: thumbnailData.toString('binary'), 498 thumbnailData: thumbnailData.toString('binary'),
479 tags: map<TagInstance, string>(this.Tags, 'name'), 499 tags: map<TagInstance, string>(this.Tags, 'name'),
480 createdAt: this.createdAt, 500 createdAt: this.createdAt,
481 updatedAt: this.updatedAt, 501 updatedAt: this.updatedAt,
482 extname: this.extname,
483 views: this.views, 502 views: this.views,
484 likes: this.likes, 503 likes: this.likes,
485 dislikes: this.dislikes 504 dislikes: this.dislikes,
505 files: []
486 } 506 }
487 507
508 this.VideoFiles.forEach(videoFile => {
509 remoteVideo.files.push({
510 infoHash: videoFile.infoHash,
511 resolution: videoFile.resolution,
512 extname: videoFile.extname,
513 size: videoFile.size
514 })
515 })
516
488 return remoteVideo 517 return remoteVideo
489 }) 518 })
490} 519}
@@ -498,28 +527,34 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
498 language: this.language, 527 language: this.language,
499 nsfw: this.nsfw, 528 nsfw: this.nsfw,
500 description: this.description, 529 description: this.description,
501 infoHash: this.infoHash,
502 author: this.Author.name, 530 author: this.Author.name,
503 duration: this.duration, 531 duration: this.duration,
504 tags: map<TagInstance, string>(this.Tags, 'name'), 532 tags: map<TagInstance, string>(this.Tags, 'name'),
505 createdAt: this.createdAt, 533 createdAt: this.createdAt,
506 updatedAt: this.updatedAt, 534 updatedAt: this.updatedAt,
507 extname: this.extname,
508 views: this.views, 535 views: this.views,
509 likes: this.likes, 536 likes: this.likes,
510 dislikes: this.dislikes 537 dislikes: this.dislikes,
538 files: []
511 } 539 }
512 540
541 this.VideoFiles.forEach(videoFile => {
542 json.files.push({
543 infoHash: videoFile.infoHash,
544 resolution: videoFile.resolution,
545 extname: videoFile.extname,
546 size: videoFile.size
547 })
548 })
549
513 return json 550 return json
514} 551}
515 552
516transcodeVideofile = function (this: VideoInstance) { 553transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
517 const video = this
518
519 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 554 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
520 const newExtname = '.mp4' 555 const newExtname = '.mp4'
521 const videoInputPath = join(videosDirectory, video.getVideoFilename()) 556 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
522 const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 557 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
523 558
524 return new Promise<void>((res, rej) => { 559 return new Promise<void>((res, rej) => {
525 ffmpeg(videoInputPath) 560 ffmpeg(videoInputPath)
@@ -533,24 +568,22 @@ transcodeVideofile = function (this: VideoInstance) {
533 return unlinkPromise(videoInputPath) 568 return unlinkPromise(videoInputPath)
534 .then(() => { 569 .then(() => {
535 // Important to do this before getVideoFilename() to take in account the new file extension 570 // Important to do this before getVideoFilename() to take in account the new file extension
536 video.set('extname', newExtname) 571 inputVideoFile.set('extname', newExtname)
537 572
538 const newVideoPath = join(videosDirectory, video.getVideoFilename()) 573 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
539 return renamePromise(videoOutputPath, newVideoPath)
540 }) 574 })
541 .then(() => { 575 .then(() => {
542 const newVideoPath = join(videosDirectory, video.getVideoFilename()) 576 return this.createTorrentAndSetInfoHash(inputVideoFile)
543 return createTorrentFromVideo(video, newVideoPath)
544 }) 577 })
545 .then(() => { 578 .then(() => {
546 return video.save() 579 return inputVideoFile.save()
547 }) 580 })
548 .then(() => { 581 .then(() => {
549 return res() 582 return res()
550 }) 583 })
551 .catch(err => { 584 .catch(err => {
552 // Autodesctruction... 585 // Autodestruction...
553 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 586 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
554 587
555 return rej(err) 588 return rej(err)
556 }) 589 })
@@ -559,6 +592,26 @@ transcodeVideofile = function (this: VideoInstance) {
559 }) 592 })
560} 593}
561 594
595removeThumbnail = function (this: VideoInstance) {
596 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
597 return unlinkPromise(thumbnailPath)
598}
599
600removePreview = function (this: VideoInstance) {
601 // Same name than video thumbnail
602 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
603}
604
605removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
606 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
607 return unlinkPromise(filePath)
608}
609
610removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
611 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
612 return unlinkPromise(torrenPath)
613}
614
562// ------------------------------ STATICS ------------------------------ 615// ------------------------------ STATICS ------------------------------
563 616
564generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { 617generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
@@ -582,7 +635,11 @@ getDurationFromFile = function (videoPath: string) {
582} 635}
583 636
584list = function () { 637list = function () {
585 return Video.findAll() 638 const query = {
639 include: [ Video['sequelize'].models.VideoFile ]
640 }
641
642 return Video.findAll(query)
586} 643}
587 644
588listForApi = function (start: number, count: number, sort: string) { 645listForApi = function (start: number, count: number, sort: string) {
@@ -597,8 +654,8 @@ listForApi = function (start: number, count: number, sort: string) {
597 model: Video['sequelize'].models.Author, 654 model: Video['sequelize'].models.Author,
598 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 655 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
599 }, 656 },
600 657 Video['sequelize'].models.Tag,
601 Video['sequelize'].models.Tag 658 Video['sequelize'].models.VideoFile
602 ], 659 ],
603 where: createBaseVideosWhere() 660 where: createBaseVideosWhere()
604 } 661 }
@@ -618,6 +675,9 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) {
618 }, 675 },
619 include: [ 676 include: [
620 { 677 {
678 model: Video['sequelize'].models.VideoFile
679 },
680 {
621 model: Video['sequelize'].models.Author, 681 model: Video['sequelize'].models.Author,
622 include: [ 682 include: [
623 { 683 {
@@ -640,7 +700,11 @@ listOwnedAndPopulateAuthorAndTags = function () {
640 where: { 700 where: {
641 remote: false 701 remote: false
642 }, 702 },
643 include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] 703 include: [
704 Video['sequelize'].models.VideoFile,
705 Video['sequelize'].models.Author,
706 Video['sequelize'].models.Tag
707 ]
644 } 708 }
645 709
646 return Video.findAll(query) 710 return Video.findAll(query)
@@ -653,6 +717,9 @@ listOwnedByAuthor = function (author: string) {
653 }, 717 },
654 include: [ 718 include: [
655 { 719 {
720 model: Video['sequelize'].models.VideoFile
721 },
722 {
656 model: Video['sequelize'].models.Author, 723 model: Video['sequelize'].models.Author,
657 where: { 724 where: {
658 name: author 725 name: author
@@ -672,14 +739,15 @@ loadByUUID = function (uuid: string) {
672 const query = { 739 const query = {
673 where: { 740 where: {
674 uuid 741 uuid
675 } 742 },
743 include: [ Video['sequelize'].models.VideoFile ]
676 } 744 }
677 return Video.findOne(query) 745 return Video.findOne(query)
678} 746}
679 747
680loadAndPopulateAuthor = function (id: number) { 748loadAndPopulateAuthor = function (id: number) {
681 const options = { 749 const options = {
682 include: [ Video['sequelize'].models.Author ] 750 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
683 } 751 }
684 752
685 return Video.findById(id, options) 753 return Video.findById(id, options)
@@ -692,7 +760,8 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
692 model: Video['sequelize'].models.Author, 760 model: Video['sequelize'].models.Author,
693 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 761 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
694 }, 762 },
695 Video['sequelize'].models.Tag 763 Video['sequelize'].models.Tag,
764 Video['sequelize'].models.VideoFile
696 ] 765 ]
697 } 766 }
698 767
@@ -709,7 +778,8 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
709 model: Video['sequelize'].models.Author, 778 model: Video['sequelize'].models.Author,
710 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 779 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
711 }, 780 },
712 Video['sequelize'].models.Tag 781 Video['sequelize'].models.Tag,
782 Video['sequelize'].models.VideoFile
713 ] 783 ]
714 } 784 }
715 785
@@ -733,6 +803,10 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
733 model: Video['sequelize'].models.Tag 803 model: Video['sequelize'].models.Tag
734 } 804 }
735 805
806 const videoFileInclude: Sequelize.IncludeOptions = {
807 model: Video['sequelize'].models.VideoFile
808 }
809
736 const query: Sequelize.FindOptions = { 810 const query: Sequelize.FindOptions = {
737 distinct: true, 811 distinct: true,
738 where: createBaseVideosWhere(), 812 where: createBaseVideosWhere(),
@@ -743,8 +817,9 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
743 817
744 // Make an exact search with the magnet 818 // Make an exact search with the magnet
745 if (field === 'magnetUri') { 819 if (field === 'magnetUri') {
746 const infoHash = magnetUtil.decode(value).infoHash 820 videoFileInclude.where = {
747 query.where['infoHash'] = infoHash 821 infoHash: magnetUtil.decode(value).infoHash
822 }
748 } else if (field === 'tags') { 823 } else if (field === 'tags') {
749 const escapedValue = Video['sequelize'].escape('%' + value + '%') 824 const escapedValue = Video['sequelize'].escape('%' + value + '%')
750 query.where['id'].$in = Video['sequelize'].literal( 825 query.where['id'].$in = Video['sequelize'].literal(
@@ -777,7 +852,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
777 } 852 }
778 853
779 query.include = [ 854 query.include = [
780 authorInclude, tagInclude 855 authorInclude, tagInclude, videoFileInclude
781 ] 856 ]
782 857
783 return Video.findAndCountAll(query).then(({ rows, count }) => { 858 return Video.findAndCountAll(query).then(({ rows, count }) => {
@@ -800,56 +875,6 @@ function createBaseVideosWhere () {
800 } 875 }
801} 876}
802 877
803function removeThumbnail (video: VideoInstance) {
804 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
805 return unlinkPromise(thumbnailPath)
806}
807
808function removeFile (video: VideoInstance) {
809 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
810 return unlinkPromise(filePath)
811}
812
813function removeTorrent (video: VideoInstance) {
814 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
815 return unlinkPromise(torrenPath)
816}
817
818function removePreview (video: VideoInstance) {
819 // Same name than video thumnail
820 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
821}
822
823function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
824 const options = {
825 announceList: [
826 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
827 ],
828 urlList: [
829 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename()
830 ]
831 }
832
833 return createTorrentPromise(videoPath, options)
834 .then(torrent => {
835 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
836 return writeFilePromise(filePath, torrent).then(() => torrent)
837 })
838 .then(torrent => {
839 const parsedTorrent = parseTorrent(torrent)
840 video.set('infoHash', parsedTorrent.infoHash)
841 return video.validate()
842 })
843}
844
845function createPreview (video: VideoInstance, videoPath: string) {
846 return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
847}
848
849function createThumbnail (video: VideoInstance, videoPath: string) {
850 return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
851}
852
853function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { 878function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
854 const options = { 879 const options = {
855 filename: imageName, 880 filename: imageName,
@@ -868,16 +893,3 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string,
868 .thumbnail(options) 893 .thumbnail(options)
869 }) 894 })
870} 895}
871
872function removeFromBlacklist (video: VideoInstance) {
873 // Find the blacklisted video
874 return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
875 // Not found the video, skip
876 if (!video) {
877 return null
878 }
879
880 // If we found the video, remove it from the blacklist
881 return video.destroy()
882 })
883}