]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Modify video file size to bigint
[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
91f6f169 303function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
93e1258c 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(),
91f6f169 317 removeVideoToFriends(removeVideoToFriendsParams, options.transaction)
93e1258c
C
318 )
319
91f6f169 320 // Remove physical files and torrents
93e1258c
C
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))
fdbda9e3
C
381 logger.info('Creating torrent %s.', filePath)
382
93e1258c
C
383 return writeFilePromise(filePath, torrent).then(() => torrent)
384 })
385 .then(torrent => {
386 const parsedTorrent = parseTorrent(torrent)
387
388 videoFile.infoHash = parsedTorrent.infoHash
389 })
390}
391
392generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) {
393 let baseUrlHttp
394 let baseUrlWs
395
396 if (this.isOwned()) {
397 baseUrlHttp = CONFIG.WEBSERVER.URL
398 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
399 } else {
400 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
401 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
402 }
403
404 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
405 const announce = [ baseUrlWs + '/tracker/socket' ]
406 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
407
408 const magnetHash = {
409 xs,
410 announce,
411 urlList,
412 infoHash: videoFile.infoHash,
413 name: this.name
414 }
415
416 return magnetUtil.encode(magnetHash)
417}
418
0aef76c4 419toFormattedJSON = function (this: VideoInstance) {
feb4bdfd
C
420 let podHost
421
422 if (this.Author.Pod) {
423 podHost = this.Author.Pod.host
424 } else {
425 // It means it's our video
65fcc311 426 podHost = CONFIG.WEBSERVER.HOST
feb4bdfd
C
427 }
428
6e07c3de 429 // Maybe our pod is not up to date and there are new categories since our version
65fcc311 430 let categoryLabel = VIDEO_CATEGORIES[this.category]
6e07c3de
C
431 if (!categoryLabel) categoryLabel = 'Misc'
432
6f0c39e2 433 // Maybe our pod is not up to date and there are new licences since our version
65fcc311 434 let licenceLabel = VIDEO_LICENCES[this.licence]
6f0c39e2
C
435 if (!licenceLabel) licenceLabel = 'Unknown'
436
3092476e 437 // Language is an optional attribute
65fcc311 438 let languageLabel = VIDEO_LANGUAGES[this.language]
3092476e
C
439 if (!languageLabel) languageLabel = 'Unknown'
440
aaf61f38 441 const json = {
feb4bdfd 442 id: this.id,
0a6658fd 443 uuid: this.uuid,
aaf61f38 444 name: this.name,
6e07c3de
C
445 category: this.category,
446 categoryLabel,
6f0c39e2
C
447 licence: this.licence,
448 licenceLabel,
3092476e
C
449 language: this.language,
450 languageLabel,
31b59b47 451 nsfw: this.nsfw,
aaf61f38 452 description: this.description,
feb4bdfd 453 podHost,
aaf61f38 454 isLocal: this.isOwned(),
feb4bdfd 455 author: this.Author.name,
aaf61f38 456 duration: this.duration,
9e167724 457 views: this.views,
d38b8281
C
458 likes: this.likes,
459 dislikes: this.dislikes,
6fcd19ba 460 tags: map<TagInstance, string>(this.Tags, 'name'),
65fcc311 461 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
f981dae8 462 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
79066fdf 463 createdAt: this.createdAt,
93e1258c
C
464 updatedAt: this.updatedAt,
465 files: []
aaf61f38
C
466 }
467
93e1258c
C
468 this.VideoFiles.forEach(videoFile => {
469 let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
470 if (!resolutionLabel) resolutionLabel = 'Unknown'
471
472 const videoFileJson = {
473 resolution: videoFile.resolution,
474 resolutionLabel,
475 magnetUri: this.generateMagnetUri(videoFile),
476 size: videoFile.size
477 }
478
479 json.files.push(videoFileJson)
480 })
481
aaf61f38
C
482 return json
483}
484
6fcd19ba 485toAddRemoteJSON = function (this: VideoInstance) {
4d324488 486 // Get thumbnail data to send to the other pod
65fcc311 487 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
aaf61f38 488
6fcd19ba 489 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
aaf61f38 490 const remoteVideo = {
0a6658fd 491 uuid: this.uuid,
e02643f3
C
492 name: this.name,
493 category: this.category,
494 licence: this.licence,
495 language: this.language,
496 nsfw: this.nsfw,
497 description: this.description,
e02643f3
C
498 author: this.Author.name,
499 duration: this.duration,
4d324488 500 thumbnailData: thumbnailData.toString('binary'),
6fcd19ba 501 tags: map<TagInstance, string>(this.Tags, 'name'),
e02643f3
C
502 createdAt: this.createdAt,
503 updatedAt: this.updatedAt,
e02643f3
C
504 views: this.views,
505 likes: this.likes,
93e1258c
C
506 dislikes: this.dislikes,
507 files: []
aaf61f38
C
508 }
509
93e1258c
C
510 this.VideoFiles.forEach(videoFile => {
511 remoteVideo.files.push({
512 infoHash: videoFile.infoHash,
513 resolution: videoFile.resolution,
514 extname: videoFile.extname,
515 size: videoFile.size
516 })
517 })
518
6fcd19ba 519 return remoteVideo
aaf61f38
C
520 })
521}
522
70c065d6 523toUpdateRemoteJSON = function (this: VideoInstance) {
7b1f49de 524 const json = {
0a6658fd 525 uuid: this.uuid,
7b1f49de 526 name: this.name,
6e07c3de 527 category: this.category,
6f0c39e2 528 licence: this.licence,
3092476e 529 language: this.language,
31b59b47 530 nsfw: this.nsfw,
7b1f49de 531 description: this.description,
7b1f49de
C
532 author: this.Author.name,
533 duration: this.duration,
6fcd19ba 534 tags: map<TagInstance, string>(this.Tags, 'name'),
7b1f49de 535 createdAt: this.createdAt,
79066fdf 536 updatedAt: this.updatedAt,
d38b8281
C
537 views: this.views,
538 likes: this.likes,
93e1258c
C
539 dislikes: this.dislikes,
540 files: []
7b1f49de
C
541 }
542
93e1258c
C
543 this.VideoFiles.forEach(videoFile => {
544 json.files.push({
545 infoHash: videoFile.infoHash,
546 resolution: videoFile.resolution,
547 extname: videoFile.extname,
548 size: videoFile.size
549 })
550 })
551
7b1f49de
C
552 return json
553}
554
93e1258c 555transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
65fcc311 556 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
227d02fe 557 const newExtname = '.mp4'
93e1258c
C
558 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
559 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
227d02fe 560
6fcd19ba
C
561 return new Promise<void>((res, rej) => {
562 ffmpeg(videoInputPath)
563 .output(videoOutputPath)
564 .videoCodec('libx264')
565 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
566 .outputOption('-movflags faststart')
567 .on('error', rej)
568 .on('end', () => {
569
570 return unlinkPromise(videoInputPath)
571 .then(() => {
572 // Important to do this before getVideoFilename() to take in account the new file extension
93e1258c 573 inputVideoFile.set('extname', newExtname)
6fcd19ba 574
93e1258c 575 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
227d02fe 576 })
6fcd19ba 577 .then(() => {
93e1258c 578 return this.createTorrentAndSetInfoHash(inputVideoFile)
6fcd19ba
C
579 })
580 .then(() => {
93e1258c 581 return inputVideoFile.save()
6fcd19ba
C
582 })
583 .then(() => {
584 return res()
585 })
586 .catch(err => {
b0f9f39e 587 // Auto destruction...
93e1258c 588 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
227d02fe 589
6fcd19ba
C
590 return rej(err)
591 })
227d02fe 592 })
6fcd19ba
C
593 .run()
594 })
227d02fe
C
595}
596
93e1258c
C
597removeThumbnail = function (this: VideoInstance) {
598 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
599 return unlinkPromise(thumbnailPath)
600}
601
602removePreview = function (this: VideoInstance) {
603 // Same name than video thumbnail
604 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
605}
606
607removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
608 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
609 return unlinkPromise(filePath)
610}
611
612removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
b0f9f39e
C
613 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
614 return unlinkPromise(torrentPath)
93e1258c
C
615}
616
aaf61f38
C
617// ------------------------------ STATICS ------------------------------
618
6fcd19ba 619generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
c77fa067
C
620 // Creating the thumbnail for a remote video
621
622 const thumbnailName = video.getThumbnailName()
65fcc311 623 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
6fcd19ba
C
624 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
625 return thumbnailName
c77fa067
C
626 })
627}
628
6fcd19ba
C
629getDurationFromFile = function (videoPath: string) {
630 return new Promise<number>((res, rej) => {
075f16ca 631 ffmpeg.ffprobe(videoPath, (err, metadata) => {
6fcd19ba 632 if (err) return rej(err)
aaf61f38 633
6fcd19ba
C
634 return res(Math.floor(metadata.format.duration))
635 })
aaf61f38
C
636 })
637}
638
6fcd19ba 639list = function () {
93e1258c
C
640 const query = {
641 include: [ Video['sequelize'].models.VideoFile ]
642 }
643
644 return Video.findAll(query)
b769007f
C
645}
646
6fcd19ba 647listForApi = function (start: number, count: number, sort: string) {
556ddc31 648 // Exclude blacklisted videos from the list
feb4bdfd 649 const query = {
e02643f3 650 distinct: true,
feb4bdfd
C
651 offset: start,
652 limit: count,
e02643f3 653 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
feb4bdfd
C
654 include: [
655 {
e02643f3
C
656 model: Video['sequelize'].models.Author,
657 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
7920c273 658 },
93e1258c
C
659 Video['sequelize'].models.Tag,
660 Video['sequelize'].models.VideoFile
198b205c 661 ],
e02643f3 662 where: createBaseVideosWhere()
feb4bdfd
C
663 }
664
6fcd19ba
C
665 return Video.findAndCountAll(query).then(({ rows, count }) => {
666 return {
667 data: rows,
668 total: count
669 }
feb4bdfd 670 })
aaf61f38
C
671}
672
0a6658fd 673loadByHostAndUUID = function (fromHost: string, uuid: string) {
feb4bdfd
C
674 const query = {
675 where: {
0a6658fd 676 uuid
feb4bdfd
C
677 },
678 include: [
93e1258c
C
679 {
680 model: Video['sequelize'].models.VideoFile
681 },
feb4bdfd 682 {
e02643f3 683 model: Video['sequelize'].models.Author,
feb4bdfd
C
684 include: [
685 {
e02643f3 686 model: Video['sequelize'].models.Pod,
7920c273 687 required: true,
feb4bdfd
C
688 where: {
689 host: fromHost
690 }
691 }
692 ]
693 }
694 ]
695 }
aaf61f38 696
6fcd19ba 697 return Video.findOne(query)
aaf61f38
C
698}
699
6fcd19ba 700listOwnedAndPopulateAuthorAndTags = function () {
feb4bdfd
C
701 const query = {
702 where: {
0a6658fd 703 remote: false
feb4bdfd 704 },
93e1258c
C
705 include: [
706 Video['sequelize'].models.VideoFile,
707 Video['sequelize'].models.Author,
708 Video['sequelize'].models.Tag
709 ]
feb4bdfd
C
710 }
711
6fcd19ba 712 return Video.findAll(query)
aaf61f38
C
713}
714
6fcd19ba 715listOwnedByAuthor = function (author: string) {
feb4bdfd
C
716 const query = {
717 where: {
0a6658fd 718 remote: false
feb4bdfd
C
719 },
720 include: [
93e1258c
C
721 {
722 model: Video['sequelize'].models.VideoFile
723 },
feb4bdfd 724 {
e02643f3 725 model: Video['sequelize'].models.Author,
feb4bdfd
C
726 where: {
727 name: author
728 }
729 }
730 ]
731 }
9bd26629 732
6fcd19ba 733 return Video.findAll(query)
aaf61f38
C
734}
735
0a6658fd 736load = function (id: number) {
6fcd19ba 737 return Video.findById(id)
feb4bdfd
C
738}
739
0a6658fd
C
740loadByUUID = function (uuid: string) {
741 const query = {
742 where: {
743 uuid
93e1258c
C
744 },
745 include: [ Video['sequelize'].models.VideoFile ]
0a6658fd
C
746 }
747 return Video.findOne(query)
748}
749
750loadAndPopulateAuthor = function (id: number) {
feb4bdfd 751 const options = {
93e1258c 752 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
feb4bdfd
C
753 }
754
6fcd19ba 755 return Video.findById(id, options)
feb4bdfd
C
756}
757
0a6658fd 758loadAndPopulateAuthorAndPodAndTags = function (id: number) {
feb4bdfd
C
759 const options = {
760 include: [
761 {
e02643f3
C
762 model: Video['sequelize'].models.Author,
763 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
7920c273 764 },
93e1258c
C
765 Video['sequelize'].models.Tag,
766 Video['sequelize'].models.VideoFile
feb4bdfd
C
767 ]
768 }
769
6fcd19ba 770 return Video.findById(id, options)
aaf61f38
C
771}
772
0a6658fd
C
773loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
774 const options = {
775 where: {
776 uuid
777 },
778 include: [
779 {
780 model: Video['sequelize'].models.Author,
781 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
782 },
93e1258c
C
783 Video['sequelize'].models.Tag,
784 Video['sequelize'].models.VideoFile
0a6658fd
C
785 ]
786 }
787
788 return Video.findOne(options)
789}
790
6fcd19ba 791searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
e6d4b0ff 792 const podInclude: Sequelize.IncludeOptions = {
e02643f3 793 model: Video['sequelize'].models.Pod,
7920c273 794 required: false
feb4bdfd 795 }
7920c273 796
e6d4b0ff 797 const authorInclude: Sequelize.IncludeOptions = {
e02643f3 798 model: Video['sequelize'].models.Author,
feb4bdfd
C
799 include: [
800 podInclude
801 ]
802 }
803
e6d4b0ff 804 const tagInclude: Sequelize.IncludeOptions = {
e02643f3 805 model: Video['sequelize'].models.Tag
7920c273
C
806 }
807
93e1258c
C
808 const videoFileInclude: Sequelize.IncludeOptions = {
809 model: Video['sequelize'].models.VideoFile
810 }
811
556ddc31 812 const query: Sequelize.FindOptions<VideoAttributes> = {
e02643f3
C
813 distinct: true,
814 where: createBaseVideosWhere(),
feb4bdfd
C
815 offset: start,
816 limit: count,
e02643f3 817 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
feb4bdfd
C
818 }
819
aaf61f38 820 // Make an exact search with the magnet
55723d16 821 if (field === 'magnetUri') {
93e1258c
C
822 videoFileInclude.where = {
823 infoHash: magnetUtil.decode(value).infoHash
824 }
55723d16 825 } else if (field === 'tags') {
e02643f3 826 const escapedValue = Video['sequelize'].escape('%' + value + '%')
e6d4b0ff 827 query.where['id'].$in = Video['sequelize'].literal(
6fcd19ba
C
828 `(SELECT "VideoTags"."videoId"
829 FROM "Tags"
830 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
18c8e945 831 WHERE name ILIKE ${escapedValue}
6fcd19ba 832 )`
198b205c 833 )
7920c273
C
834 } else if (field === 'host') {
835 // FIXME: Include our pod? (not stored in the database)
836 podInclude.where = {
837 host: {
18c8e945 838 $iLike: '%' + value + '%'
feb4bdfd 839 }
feb4bdfd 840 }
7920c273 841 podInclude.required = true
feb4bdfd 842 } else if (field === 'author') {
7920c273
C
843 authorInclude.where = {
844 name: {
18c8e945 845 $iLike: '%' + value + '%'
feb4bdfd
C
846 }
847 }
7920c273
C
848
849 // authorInclude.or = true
aaf61f38 850 } else {
feb4bdfd 851 query.where[field] = {
18c8e945 852 $iLike: '%' + value + '%'
feb4bdfd 853 }
aaf61f38
C
854 }
855
7920c273 856 query.include = [
93e1258c 857 authorInclude, tagInclude, videoFileInclude
7920c273
C
858 ]
859
6fcd19ba
C
860 return Video.findAndCountAll(query).then(({ rows, count }) => {
861 return {
862 data: rows,
863 total: count
864 }
feb4bdfd 865 })
aaf61f38
C
866}
867
aaf61f38
C
868// ---------------------------------------------------------------------------
869
15d4ee04
C
870function createBaseVideosWhere () {
871 return {
872 id: {
e02643f3 873 $notIn: Video['sequelize'].literal(
15d4ee04
C
874 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
875 )
876 }
877 }
878}
879
6fcd19ba 880function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
e6d4b0ff 881 const options = {
f285faa0 882 filename: imageName,
558d7c23
C
883 count: 1,
884 folder
885 }
aaf61f38 886
69818c93 887 if (size) {
e6d4b0ff 888 options['size'] = size
558d7c23
C
889 }
890
6fcd19ba
C
891 return new Promise<string>((res, rej) => {
892 ffmpeg(videoPath)
893 .on('error', rej)
075f16ca 894 .on('end', () => res(imageName))
6fcd19ba
C
895 .thumbnail(options)
896 })
aaf61f38 897}