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