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