]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Formated -> Formatted
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
CommitLineData
4d4e5cd4 1import * as safeBuffer from 'safe-buffer'
65fcc311 2const Buffer = safeBuffer.Buffer
4d4e5cd4 3import * as ffmpeg from 'fluent-ffmpeg'
4d4e5cd4 4import * as magnetUtil from 'magnet-uri'
93e1258c 5import { map } from 'lodash'
4d4e5cd4 6import * as parseTorrent from 'parse-torrent'
65fcc311 7import { join } from 'path'
e02643f3 8import * as Sequelize from 'sequelize'
6fcd19ba 9import * as Promise from 'bluebird'
65fcc311 10
6fcd19ba 11import { TagInstance } from './tag-interface'
65fcc311
C
12import {
13 logger,
14 isVideoNameValid,
15 isVideoCategoryValid,
16 isVideoLicenceValid,
17 isVideoLanguageValid,
18 isVideoNSFWValid,
19 isVideoDescriptionValid,
6fcd19ba
C
20 isVideoDurationValid,
21 readFileBufferPromise,
22 unlinkPromise,
23 renamePromise,
24 writeFilePromise,
25 createTorrentPromise
74889a71 26} from '../../helpers'
65fcc311 27import {
65fcc311
C
28 CONFIG,
29 REMOTE_SCHEME,
30 STATIC_PATHS,
31 VIDEO_CATEGORIES,
32 VIDEO_LICENCES,
33 VIDEO_LANGUAGES,
93e1258c
C
34 THUMBNAILS_SIZE,
35 VIDEO_FILE_RESOLUTIONS
74889a71 36} from '../../initializers'
93e1258c
C
37import { removeVideoToFriends } from '../../lib'
38import { VideoFileInstance } from './video-file-interface'
aaf61f38 39
74889a71 40import { addMethodsToModel, getSort } from '../utils'
e02643f3 41import {
e02643f3
C
42 VideoInstance,
43 VideoAttributes,
44
45 VideoMethods
46} from './video-interface'
47
48let Video: Sequelize.Model<VideoInstance, VideoAttributes>
49let generateMagnetUri: VideoMethods.GenerateMagnetUri
50let getVideoFilename: VideoMethods.GetVideoFilename
51let getThumbnailName: VideoMethods.GetThumbnailName
52let getPreviewName: VideoMethods.GetPreviewName
93e1258c 53let getTorrentFileName: VideoMethods.GetTorrentFileName
e02643f3 54let isOwned: VideoMethods.IsOwned
0aef76c4 55let toFormattedJSON: VideoMethods.ToFormattedJSON
e02643f3
C
56let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
57let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
58let transcodeVideofile: VideoMethods.TranscodeVideofile
93e1258c
C
59let createPreview: VideoMethods.CreatePreview
60let createThumbnail: VideoMethods.CreateThumbnail
61let getVideoFilePath: VideoMethods.GetVideoFilePath
62let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
e02643f3
C
63
64let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
65let getDurationFromFile: VideoMethods.GetDurationFromFile
66let list: VideoMethods.List
67let listForApi: VideoMethods.ListForApi
0a6658fd 68let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
e02643f3
C
69let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
70let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
71let load: VideoMethods.Load
0a6658fd 72let loadByUUID: VideoMethods.LoadByUUID
e02643f3
C
73let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
74let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
0a6658fd 75let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
e02643f3 76let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
93e1258c
C
77let removeThumbnail: VideoMethods.RemoveThumbnail
78let removePreview: VideoMethods.RemovePreview
79let removeFile: VideoMethods.RemoveFile
80let removeTorrent: VideoMethods.RemoveTorrent
e02643f3 81
127944aa
C
82export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
83 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
feb4bdfd 84 {
0a6658fd 85 uuid: {
feb4bdfd
C
86 type: DataTypes.UUID,
87 defaultValue: DataTypes.UUIDV4,
0a6658fd 88 allowNull: false,
67bf9b96
C
89 validate: {
90 isUUID: 4
91 }
aaf61f38 92 },
feb4bdfd 93 name: {
67bf9b96
C
94 type: DataTypes.STRING,
95 allowNull: false,
96 validate: {
075f16ca 97 nameValid: value => {
65fcc311 98 const res = isVideoNameValid(value)
67bf9b96
C
99 if (res === false) throw new Error('Video name is not valid.')
100 }
101 }
6a94a109 102 },
6e07c3de
C
103 category: {
104 type: DataTypes.INTEGER,
105 allowNull: false,
106 validate: {
075f16ca 107 categoryValid: value => {
65fcc311 108 const res = isVideoCategoryValid(value)
6e07c3de
C
109 if (res === false) throw new Error('Video category is not valid.')
110 }
111 }
112 },
6f0c39e2
C
113 licence: {
114 type: DataTypes.INTEGER,
115 allowNull: false,
3092476e 116 defaultValue: null,
6f0c39e2 117 validate: {
075f16ca 118 licenceValid: value => {
65fcc311 119 const res = isVideoLicenceValid(value)
6f0c39e2
C
120 if (res === false) throw new Error('Video licence is not valid.')
121 }
122 }
123 },
3092476e
C
124 language: {
125 type: DataTypes.INTEGER,
126 allowNull: true,
127 validate: {
075f16ca 128 languageValid: value => {
65fcc311 129 const res = isVideoLanguageValid(value)
3092476e
C
130 if (res === false) throw new Error('Video language is not valid.')
131 }
132 }
133 },
31b59b47
C
134 nsfw: {
135 type: DataTypes.BOOLEAN,
136 allowNull: false,
137 validate: {
075f16ca 138 nsfwValid: value => {
65fcc311 139 const res = isVideoNSFWValid(value)
31b59b47
C
140 if (res === false) throw new Error('Video nsfw attribute is not valid.')
141 }
142 }
143 },
feb4bdfd 144 description: {
67bf9b96
C
145 type: DataTypes.STRING,
146 allowNull: false,
147 validate: {
075f16ca 148 descriptionValid: value => {
65fcc311 149 const res = isVideoDescriptionValid(value)
67bf9b96
C
150 if (res === false) throw new Error('Video description is not valid.')
151 }
152 }
feb4bdfd 153 },
feb4bdfd 154 duration: {
67bf9b96
C
155 type: DataTypes.INTEGER,
156 allowNull: false,
157 validate: {
075f16ca 158 durationValid: value => {
65fcc311 159 const res = isVideoDurationValid(value)
67bf9b96
C
160 if (res === false) throw new Error('Video duration is not valid.')
161 }
162 }
9e167724
C
163 },
164 views: {
165 type: DataTypes.INTEGER,
166 allowNull: false,
167 defaultValue: 0,
168 validate: {
169 min: 0,
170 isInt: true
171 }
d38b8281
C
172 },
173 likes: {
174 type: DataTypes.INTEGER,
175 allowNull: false,
176 defaultValue: 0,
177 validate: {
178 min: 0,
179 isInt: true
180 }
181 },
182 dislikes: {
183 type: DataTypes.INTEGER,
184 allowNull: false,
185 defaultValue: 0,
186 validate: {
187 min: 0,
188 isInt: true
189 }
0a6658fd
C
190 },
191 remote: {
192 type: DataTypes.BOOLEAN,
193 allowNull: false,
194 defaultValue: false
aaf61f38 195 }
feb4bdfd
C
196 },
197 {
319d072e
C
198 indexes: [
199 {
200 fields: [ 'authorId' ]
201 },
319d072e
C
202 {
203 fields: [ 'name' ]
204 },
205 {
206 fields: [ 'createdAt' ]
207 },
208 {
209 fields: [ 'duration' ]
210 },
9e167724
C
211 {
212 fields: [ 'views' ]
d38b8281
C
213 },
214 {
215 fields: [ 'likes' ]
0a6658fd
C
216 },
217 {
218 fields: [ 'uuid' ]
319d072e
C
219 }
220 ],
feb4bdfd 221 hooks: {
feb4bdfd
C
222 afterDestroy
223 }
224 }
225 )
aaf61f38 226
e02643f3
C
227 const classMethods = [
228 associate,
229
230 generateThumbnailFromData,
231 getDurationFromFile,
232 list,
233 listForApi,
234 listOwnedAndPopulateAuthorAndTags,
235 listOwnedByAuthor,
236 load,
e02643f3
C
237 loadAndPopulateAuthor,
238 loadAndPopulateAuthorAndPodAndTags,
93e1258c
C
239 loadByHostAndUUID,
240 loadByUUID,
0a6658fd 241 loadByUUIDAndPopulateAuthorAndPodAndTags,
93e1258c 242 searchAndPopulateAuthorAndPodAndTags
e02643f3
C
243 ]
244 const instanceMethods = [
93e1258c
C
245 createPreview,
246 createThumbnail,
247 createTorrentAndSetInfoHash,
e02643f3 248 generateMagnetUri,
e02643f3 249 getPreviewName,
93e1258c
C
250 getThumbnailName,
251 getTorrentFileName,
252 getVideoFilename,
253 getVideoFilePath,
e02643f3 254 isOwned,
93e1258c
C
255 removeFile,
256 removePreview,
257 removeThumbnail,
258 removeTorrent,
e02643f3 259 toAddRemoteJSON,
0aef76c4 260 toFormattedJSON,
e02643f3 261 toUpdateRemoteJSON,
6fcd19ba 262 transcodeVideofile
e02643f3
C
263 ]
264 addMethodsToModel(Video, classMethods, instanceMethods)
265
feb4bdfd
C
266 return Video
267}
aaf61f38 268
aaf61f38
C
269// ------------------------------ METHODS ------------------------------
270
feb4bdfd 271function associate (models) {
e02643f3 272 Video.belongsTo(models.Author, {
feb4bdfd
C
273 foreignKey: {
274 name: 'authorId',
275 allowNull: false
276 },
277 onDelete: 'cascade'
278 })
7920c273 279
e02643f3 280 Video.belongsToMany(models.Tag, {
7920c273
C
281 foreignKey: 'videoId',
282 through: models.VideoTag,
283 onDelete: 'cascade'
284 })
55fa55a9 285
e02643f3 286 Video.hasMany(models.VideoAbuse, {
55fa55a9
C
287 foreignKey: {
288 name: 'videoId',
289 allowNull: false
290 },
291 onDelete: 'cascade'
292 })
93e1258c
C
293
294 Video.hasMany(models.VideoFile, {
295 foreignKey: {
296 name: 'videoId',
297 allowNull: false
298 },
299 onDelete: 'cascade'
300 })
feb4bdfd
C
301}
302
93e1258c
C
303function afterDestroy (video: VideoInstance) {
304 const tasks = []
f285faa0 305
93e1258c
C
306 tasks.push(
307 video.removeThumbnail()
308 )
f285faa0 309
93e1258c
C
310 if (video.isOwned()) {
311 const removeVideoToFriendsParams = {
312 uuid: video.uuid
313 }
f285faa0 314
93e1258c
C
315 tasks.push(
316 video.removePreview(),
317 removeVideoToFriends(removeVideoToFriendsParams)
318 )
319
320 // TODO: check files is populated
321 video.VideoFiles.forEach(file => {
322 video.removeFile(file),
323 video.removeTorrent(file)
324 })
f285faa0
C
325 }
326
93e1258c 327 return Promise.all(tasks)
558d7c23
C
328}
329
93e1258c
C
330getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
331 // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
332 return this.uuid + videoFile.extname
f285faa0
C
333}
334
70c065d6 335getThumbnailName = function (this: VideoInstance) {
f285faa0 336 // We always have a copy of the thumbnail
0a6658fd
C
337 const extension = '.jpg'
338 return this.uuid + extension
558d7c23
C
339}
340
70c065d6 341getPreviewName = function (this: VideoInstance) {
f285faa0 342 const extension = '.jpg'
0a6658fd 343 return this.uuid + extension
f285faa0
C
344}
345
93e1258c 346getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
f285faa0 347 const extension = '.torrent'
93e1258c 348 // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
0a6658fd 349 return this.uuid + extension
558d7c23
C
350}
351
70c065d6 352isOwned = function (this: VideoInstance) {
0a6658fd 353 return this.remote === false
aaf61f38
C
354}
355
93e1258c
C
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
0aef76c4 417toFormattedJSON = function (this: VideoInstance) {
feb4bdfd
C
418 let podHost
419
420 if (this.Author.Pod) {
421 podHost = this.Author.Pod.host
422 } else {
423 // It means it's our video
65fcc311 424 podHost = CONFIG.WEBSERVER.HOST
feb4bdfd
C
425 }
426
6e07c3de 427 // Maybe our pod is not up to date and there are new categories since our version
65fcc311 428 let categoryLabel = VIDEO_CATEGORIES[this.category]
6e07c3de
C
429 if (!categoryLabel) categoryLabel = 'Misc'
430
6f0c39e2 431 // Maybe our pod is not up to date and there are new licences since our version
65fcc311 432 let licenceLabel = VIDEO_LICENCES[this.licence]
6f0c39e2
C
433 if (!licenceLabel) licenceLabel = 'Unknown'
434
3092476e 435 // Language is an optional attribute
65fcc311 436 let languageLabel = VIDEO_LANGUAGES[this.language]
3092476e
C
437 if (!languageLabel) languageLabel = 'Unknown'
438
aaf61f38 439 const json = {
feb4bdfd 440 id: this.id,
0a6658fd 441 uuid: this.uuid,
aaf61f38 442 name: this.name,
6e07c3de
C
443 category: this.category,
444 categoryLabel,
6f0c39e2
C
445 licence: this.licence,
446 licenceLabel,
3092476e
C
447 language: this.language,
448 languageLabel,
31b59b47 449 nsfw: this.nsfw,
aaf61f38 450 description: this.description,
feb4bdfd 451 podHost,
aaf61f38 452 isLocal: this.isOwned(),
feb4bdfd 453 author: this.Author.name,
aaf61f38 454 duration: this.duration,
9e167724 455 views: this.views,
d38b8281
C
456 likes: this.likes,
457 dislikes: this.dislikes,
6fcd19ba 458 tags: map<TagInstance, string>(this.Tags, 'name'),
65fcc311 459 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
f981dae8 460 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
79066fdf 461 createdAt: this.createdAt,
93e1258c
C
462 updatedAt: this.updatedAt,
463 files: []
aaf61f38
C
464 }
465
93e1258c
C
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
aaf61f38
C
480 return json
481}
482
6fcd19ba 483toAddRemoteJSON = function (this: VideoInstance) {
4d324488 484 // Get thumbnail data to send to the other pod
65fcc311 485 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
aaf61f38 486
6fcd19ba 487 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
aaf61f38 488 const remoteVideo = {
0a6658fd 489 uuid: this.uuid,
e02643f3
C
490 name: this.name,
491 category: this.category,
492 licence: this.licence,
493 language: this.language,
494 nsfw: this.nsfw,
495 description: this.description,
e02643f3
C
496 author: this.Author.name,
497 duration: this.duration,
4d324488 498 thumbnailData: thumbnailData.toString('binary'),
6fcd19ba 499 tags: map<TagInstance, string>(this.Tags, 'name'),
e02643f3
C
500 createdAt: this.createdAt,
501 updatedAt: this.updatedAt,
e02643f3
C
502 views: this.views,
503 likes: this.likes,
93e1258c
C
504 dislikes: this.dislikes,
505 files: []
aaf61f38
C
506 }
507
93e1258c
C
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
6fcd19ba 517 return remoteVideo
aaf61f38
C
518 })
519}
520
70c065d6 521toUpdateRemoteJSON = function (this: VideoInstance) {
7b1f49de 522 const json = {
0a6658fd 523 uuid: this.uuid,
7b1f49de 524 name: this.name,
6e07c3de 525 category: this.category,
6f0c39e2 526 licence: this.licence,
3092476e 527 language: this.language,
31b59b47 528 nsfw: this.nsfw,
7b1f49de 529 description: this.description,
7b1f49de
C
530 author: this.Author.name,
531 duration: this.duration,
6fcd19ba 532 tags: map<TagInstance, string>(this.Tags, 'name'),
7b1f49de 533 createdAt: this.createdAt,
79066fdf 534 updatedAt: this.updatedAt,
d38b8281
C
535 views: this.views,
536 likes: this.likes,
93e1258c
C
537 dislikes: this.dislikes,
538 files: []
7b1f49de
C
539 }
540
93e1258c
C
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
7b1f49de
C
550 return json
551}
552
93e1258c 553transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
65fcc311 554 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
227d02fe 555 const newExtname = '.mp4'
93e1258c
C
556 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
557 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
227d02fe 558
6fcd19ba
C
559 return new Promise<void>((res, rej) => {
560 ffmpeg(videoInputPath)
561 .output(videoOutputPath)
562 .videoCodec('libx264')
563 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
564 .outputOption('-movflags faststart')
565 .on('error', rej)
566 .on('end', () => {
567
568 return unlinkPromise(videoInputPath)
569 .then(() => {
570 // Important to do this before getVideoFilename() to take in account the new file extension
93e1258c 571 inputVideoFile.set('extname', newExtname)
6fcd19ba 572
93e1258c 573 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
227d02fe 574 })
6fcd19ba 575 .then(() => {
93e1258c 576 return this.createTorrentAndSetInfoHash(inputVideoFile)
6fcd19ba
C
577 })
578 .then(() => {
93e1258c 579 return inputVideoFile.save()
6fcd19ba
C
580 })
581 .then(() => {
582 return res()
583 })
584 .catch(err => {
93e1258c
C
585 // Autodestruction...
586 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
227d02fe 587
6fcd19ba
C
588 return rej(err)
589 })
227d02fe 590 })
6fcd19ba
C
591 .run()
592 })
227d02fe
C
593}
594
93e1258c
C
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
aaf61f38
C
615// ------------------------------ STATICS ------------------------------
616
6fcd19ba 617generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
c77fa067
C
618 // Creating the thumbnail for a remote video
619
620 const thumbnailName = video.getThumbnailName()
65fcc311 621 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
6fcd19ba
C
622 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
623 return thumbnailName
c77fa067
C
624 })
625}
626
6fcd19ba
C
627getDurationFromFile = function (videoPath: string) {
628 return new Promise<number>((res, rej) => {
075f16ca 629 ffmpeg.ffprobe(videoPath, (err, metadata) => {
6fcd19ba 630 if (err) return rej(err)
aaf61f38 631
6fcd19ba
C
632 return res(Math.floor(metadata.format.duration))
633 })
aaf61f38
C
634 })
635}
636
6fcd19ba 637list = function () {
93e1258c
C
638 const query = {
639 include: [ Video['sequelize'].models.VideoFile ]
640 }
641
642 return Video.findAll(query)
b769007f
C
643}
644
6fcd19ba 645listForApi = function (start: number, count: number, sort: string) {
198b205c 646 // Exclude Blakclisted videos from the list
feb4bdfd 647 const query = {
e02643f3 648 distinct: true,
feb4bdfd
C
649 offset: start,
650 limit: count,
e02643f3 651 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
feb4bdfd
C
652 include: [
653 {
e02643f3
C
654 model: Video['sequelize'].models.Author,
655 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
7920c273 656 },
93e1258c
C
657 Video['sequelize'].models.Tag,
658 Video['sequelize'].models.VideoFile
198b205c 659 ],
e02643f3 660 where: createBaseVideosWhere()
feb4bdfd
C
661 }
662
6fcd19ba
C
663 return Video.findAndCountAll(query).then(({ rows, count }) => {
664 return {
665 data: rows,
666 total: count
667 }
feb4bdfd 668 })
aaf61f38
C
669}
670
0a6658fd 671loadByHostAndUUID = function (fromHost: string, uuid: string) {
feb4bdfd
C
672 const query = {
673 where: {
0a6658fd 674 uuid
feb4bdfd
C
675 },
676 include: [
93e1258c
C
677 {
678 model: Video['sequelize'].models.VideoFile
679 },
feb4bdfd 680 {
e02643f3 681 model: Video['sequelize'].models.Author,
feb4bdfd
C
682 include: [
683 {
e02643f3 684 model: Video['sequelize'].models.Pod,
7920c273 685 required: true,
feb4bdfd
C
686 where: {
687 host: fromHost
688 }
689 }
690 ]
691 }
692 ]
693 }
aaf61f38 694
6fcd19ba 695 return Video.findOne(query)
aaf61f38
C
696}
697
6fcd19ba 698listOwnedAndPopulateAuthorAndTags = function () {
feb4bdfd
C
699 const query = {
700 where: {
0a6658fd 701 remote: false
feb4bdfd 702 },
93e1258c
C
703 include: [
704 Video['sequelize'].models.VideoFile,
705 Video['sequelize'].models.Author,
706 Video['sequelize'].models.Tag
707 ]
feb4bdfd
C
708 }
709
6fcd19ba 710 return Video.findAll(query)
aaf61f38
C
711}
712
6fcd19ba 713listOwnedByAuthor = function (author: string) {
feb4bdfd
C
714 const query = {
715 where: {
0a6658fd 716 remote: false
feb4bdfd
C
717 },
718 include: [
93e1258c
C
719 {
720 model: Video['sequelize'].models.VideoFile
721 },
feb4bdfd 722 {
e02643f3 723 model: Video['sequelize'].models.Author,
feb4bdfd
C
724 where: {
725 name: author
726 }
727 }
728 ]
729 }
9bd26629 730
6fcd19ba 731 return Video.findAll(query)
aaf61f38
C
732}
733
0a6658fd 734load = function (id: number) {
6fcd19ba 735 return Video.findById(id)
feb4bdfd
C
736}
737
0a6658fd
C
738loadByUUID = function (uuid: string) {
739 const query = {
740 where: {
741 uuid
93e1258c
C
742 },
743 include: [ Video['sequelize'].models.VideoFile ]
0a6658fd
C
744 }
745 return Video.findOne(query)
746}
747
748loadAndPopulateAuthor = function (id: number) {
feb4bdfd 749 const options = {
93e1258c 750 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
feb4bdfd
C
751 }
752
6fcd19ba 753 return Video.findById(id, options)
feb4bdfd
C
754}
755
0a6658fd 756loadAndPopulateAuthorAndPodAndTags = function (id: number) {
feb4bdfd
C
757 const options = {
758 include: [
759 {
e02643f3
C
760 model: Video['sequelize'].models.Author,
761 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
7920c273 762 },
93e1258c
C
763 Video['sequelize'].models.Tag,
764 Video['sequelize'].models.VideoFile
feb4bdfd
C
765 ]
766 }
767
6fcd19ba 768 return Video.findById(id, options)
aaf61f38
C
769}
770
0a6658fd
C
771loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
772 const options = {
773 where: {
774 uuid
775 },
776 include: [
777 {
778 model: Video['sequelize'].models.Author,
779 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
780 },
93e1258c
C
781 Video['sequelize'].models.Tag,
782 Video['sequelize'].models.VideoFile
0a6658fd
C
783 ]
784 }
785
786 return Video.findOne(options)
787}
788
6fcd19ba 789searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
e6d4b0ff 790 const podInclude: Sequelize.IncludeOptions = {
e02643f3 791 model: Video['sequelize'].models.Pod,
7920c273 792 required: false
feb4bdfd 793 }
7920c273 794
e6d4b0ff 795 const authorInclude: Sequelize.IncludeOptions = {
e02643f3 796 model: Video['sequelize'].models.Author,
feb4bdfd
C
797 include: [
798 podInclude
799 ]
800 }
801
e6d4b0ff 802 const tagInclude: Sequelize.IncludeOptions = {
e02643f3 803 model: Video['sequelize'].models.Tag
7920c273
C
804 }
805
93e1258c
C
806 const videoFileInclude: Sequelize.IncludeOptions = {
807 model: Video['sequelize'].models.VideoFile
808 }
809
e6d4b0ff 810 const query: Sequelize.FindOptions = {
e02643f3
C
811 distinct: true,
812 where: createBaseVideosWhere(),
feb4bdfd
C
813 offset: start,
814 limit: count,
e02643f3 815 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
feb4bdfd
C
816 }
817
aaf61f38 818 // Make an exact search with the magnet
55723d16 819 if (field === 'magnetUri') {
93e1258c
C
820 videoFileInclude.where = {
821 infoHash: magnetUtil.decode(value).infoHash
822 }
55723d16 823 } else if (field === 'tags') {
e02643f3 824 const escapedValue = Video['sequelize'].escape('%' + value + '%')
e6d4b0ff 825 query.where['id'].$in = Video['sequelize'].literal(
6fcd19ba
C
826 `(SELECT "VideoTags"."videoId"
827 FROM "Tags"
828 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
18c8e945 829 WHERE name ILIKE ${escapedValue}
6fcd19ba 830 )`
198b205c 831 )
7920c273
C
832 } else if (field === 'host') {
833 // FIXME: Include our pod? (not stored in the database)
834 podInclude.where = {
835 host: {
18c8e945 836 $iLike: '%' + value + '%'
feb4bdfd 837 }
feb4bdfd 838 }
7920c273 839 podInclude.required = true
feb4bdfd 840 } else if (field === 'author') {
7920c273
C
841 authorInclude.where = {
842 name: {
18c8e945 843 $iLike: '%' + value + '%'
feb4bdfd
C
844 }
845 }
7920c273
C
846
847 // authorInclude.or = true
aaf61f38 848 } else {
feb4bdfd 849 query.where[field] = {
18c8e945 850 $iLike: '%' + value + '%'
feb4bdfd 851 }
aaf61f38
C
852 }
853
7920c273 854 query.include = [
93e1258c 855 authorInclude, tagInclude, videoFileInclude
7920c273
C
856 ]
857
6fcd19ba
C
858 return Video.findAndCountAll(query).then(({ rows, count }) => {
859 return {
860 data: rows,
861 total: count
862 }
feb4bdfd 863 })
aaf61f38
C
864}
865
aaf61f38
C
866// ---------------------------------------------------------------------------
867
15d4ee04
C
868function createBaseVideosWhere () {
869 return {
870 id: {
e02643f3 871 $notIn: Video['sequelize'].literal(
15d4ee04
C
872 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
873 )
874 }
875 }
876}
877
6fcd19ba 878function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
e6d4b0ff 879 const options = {
f285faa0 880 filename: imageName,
558d7c23
C
881 count: 1,
882 folder
883 }
aaf61f38 884
69818c93 885 if (size) {
e6d4b0ff 886 options['size'] = size
558d7c23
C
887 }
888
6fcd19ba
C
889 return new Promise<string>((res, rej) => {
890 ffmpeg(videoPath)
891 .on('error', rej)
075f16ca 892 .on('end', () => res(imageName))
6fcd19ba
C
893 .thumbnail(options)
894 })
aaf61f38 895}