]>
Commit | Line | Data |
---|---|---|
4d4e5cd4 | 1 | import * as safeBuffer from 'safe-buffer' |
65fcc311 | 2 | const Buffer = safeBuffer.Buffer |
4d4e5cd4 | 3 | import * as magnetUtil from 'magnet-uri' |
93e1258c | 4 | import { map } from 'lodash' |
4d4e5cd4 | 5 | import * as parseTorrent from 'parse-torrent' |
65fcc311 | 6 | import { join } from 'path' |
e02643f3 | 7 | import * as Sequelize from 'sequelize' |
6fcd19ba | 8 | import * as Promise from 'bluebird' |
14d3270f | 9 | import { maxBy } from 'lodash' |
65fcc311 | 10 | |
6fcd19ba | 11 | import { TagInstance } from './tag-interface' |
65fcc311 C |
12 | import { |
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, | |
40298b02 | 25 | createTorrentPromise, |
14d3270f C |
26 | statPromise, |
27 | generateImageFromVideoFile, | |
28 | transcode, | |
29 | getVideoFileHeight | |
74889a71 | 30 | } from '../../helpers' |
65fcc311 | 31 | import { |
65fcc311 C |
32 | CONFIG, |
33 | REMOTE_SCHEME, | |
34 | STATIC_PATHS, | |
35 | VIDEO_CATEGORIES, | |
36 | VIDEO_LICENCES, | |
37 | VIDEO_LANGUAGES, | |
14d3270f | 38 | THUMBNAILS_SIZE |
74889a71 | 39 | } from '../../initializers' |
93e1258c | 40 | import { removeVideoToFriends } from '../../lib' |
40298b02 C |
41 | import { VideoResolution } from '../../../shared' |
42 | import { VideoFileInstance, VideoFileModel } from './video-file-interface' | |
aaf61f38 | 43 | |
74889a71 | 44 | import { addMethodsToModel, getSort } from '../utils' |
e02643f3 | 45 | import { |
e02643f3 C |
46 | VideoInstance, |
47 | VideoAttributes, | |
48 | ||
49 | VideoMethods | |
50 | } from './video-interface' | |
164174a6 | 51 | import { PREVIEWS_SIZE } from '../../initializers/constants' |
e02643f3 C |
52 | |
53 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> | |
40298b02 | 54 | let getOriginalFile: VideoMethods.GetOriginalFile |
e02643f3 C |
55 | let generateMagnetUri: VideoMethods.GenerateMagnetUri |
56 | let getVideoFilename: VideoMethods.GetVideoFilename | |
57 | let getThumbnailName: VideoMethods.GetThumbnailName | |
d8755eed | 58 | let getThumbnailPath: VideoMethods.GetThumbnailPath |
e02643f3 | 59 | let getPreviewName: VideoMethods.GetPreviewName |
d8755eed | 60 | let getPreviewPath: VideoMethods.GetPreviewPath |
93e1258c | 61 | let getTorrentFileName: VideoMethods.GetTorrentFileName |
e02643f3 | 62 | let isOwned: VideoMethods.IsOwned |
0aef76c4 | 63 | let toFormattedJSON: VideoMethods.ToFormattedJSON |
e02643f3 C |
64 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON |
65 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | |
40298b02 C |
66 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile |
67 | let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile | |
93e1258c C |
68 | let createPreview: VideoMethods.CreatePreview |
69 | let createThumbnail: VideoMethods.CreateThumbnail | |
70 | let getVideoFilePath: VideoMethods.GetVideoFilePath | |
71 | let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | |
40298b02 | 72 | let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight |
d8755eed | 73 | let getEmbedPath: VideoMethods.GetEmbedPath |
e02643f3 C |
74 | |
75 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | |
e02643f3 C |
76 | let list: VideoMethods.List |
77 | let listForApi: VideoMethods.ListForApi | |
0a6658fd | 78 | let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID |
e02643f3 C |
79 | let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags |
80 | let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor | |
81 | let load: VideoMethods.Load | |
0a6658fd | 82 | let loadByUUID: VideoMethods.LoadByUUID |
e02643f3 C |
83 | let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor |
84 | let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags | |
0a6658fd | 85 | let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags |
e02643f3 | 86 | let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags |
93e1258c C |
87 | let removeThumbnail: VideoMethods.RemoveThumbnail |
88 | let removePreview: VideoMethods.RemovePreview | |
89 | let removeFile: VideoMethods.RemoveFile | |
90 | let removeTorrent: VideoMethods.RemoveTorrent | |
e02643f3 | 91 | |
127944aa C |
92 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { |
93 | Video = sequelize.define<VideoInstance, VideoAttributes>('Video', | |
feb4bdfd | 94 | { |
0a6658fd | 95 | uuid: { |
feb4bdfd C |
96 | type: DataTypes.UUID, |
97 | defaultValue: DataTypes.UUIDV4, | |
0a6658fd | 98 | allowNull: false, |
67bf9b96 C |
99 | validate: { |
100 | isUUID: 4 | |
101 | } | |
aaf61f38 | 102 | }, |
feb4bdfd | 103 | name: { |
67bf9b96 C |
104 | type: DataTypes.STRING, |
105 | allowNull: false, | |
106 | validate: { | |
075f16ca | 107 | nameValid: value => { |
65fcc311 | 108 | const res = isVideoNameValid(value) |
67bf9b96 C |
109 | if (res === false) throw new Error('Video name is not valid.') |
110 | } | |
111 | } | |
6a94a109 | 112 | }, |
6e07c3de C |
113 | category: { |
114 | type: DataTypes.INTEGER, | |
115 | allowNull: false, | |
116 | validate: { | |
075f16ca | 117 | categoryValid: value => { |
65fcc311 | 118 | const res = isVideoCategoryValid(value) |
6e07c3de C |
119 | if (res === false) throw new Error('Video category is not valid.') |
120 | } | |
121 | } | |
122 | }, | |
6f0c39e2 C |
123 | licence: { |
124 | type: DataTypes.INTEGER, | |
125 | allowNull: false, | |
3092476e | 126 | defaultValue: null, |
6f0c39e2 | 127 | validate: { |
075f16ca | 128 | licenceValid: value => { |
65fcc311 | 129 | const res = isVideoLicenceValid(value) |
6f0c39e2 C |
130 | if (res === false) throw new Error('Video licence is not valid.') |
131 | } | |
132 | } | |
133 | }, | |
3092476e C |
134 | language: { |
135 | type: DataTypes.INTEGER, | |
136 | allowNull: true, | |
137 | validate: { | |
075f16ca | 138 | languageValid: value => { |
65fcc311 | 139 | const res = isVideoLanguageValid(value) |
3092476e C |
140 | if (res === false) throw new Error('Video language is not valid.') |
141 | } | |
142 | } | |
143 | }, | |
31b59b47 C |
144 | nsfw: { |
145 | type: DataTypes.BOOLEAN, | |
146 | allowNull: false, | |
147 | validate: { | |
075f16ca | 148 | nsfwValid: value => { |
65fcc311 | 149 | const res = isVideoNSFWValid(value) |
31b59b47 C |
150 | if (res === false) throw new Error('Video nsfw attribute is not valid.') |
151 | } | |
152 | } | |
153 | }, | |
feb4bdfd | 154 | description: { |
67bf9b96 C |
155 | type: DataTypes.STRING, |
156 | allowNull: false, | |
157 | validate: { | |
075f16ca | 158 | descriptionValid: value => { |
65fcc311 | 159 | const res = isVideoDescriptionValid(value) |
67bf9b96 C |
160 | if (res === false) throw new Error('Video description is not valid.') |
161 | } | |
162 | } | |
feb4bdfd | 163 | }, |
feb4bdfd | 164 | duration: { |
67bf9b96 C |
165 | type: DataTypes.INTEGER, |
166 | allowNull: false, | |
167 | validate: { | |
075f16ca | 168 | durationValid: value => { |
65fcc311 | 169 | const res = isVideoDurationValid(value) |
67bf9b96 C |
170 | if (res === false) throw new Error('Video duration is not valid.') |
171 | } | |
172 | } | |
9e167724 C |
173 | }, |
174 | views: { | |
175 | type: DataTypes.INTEGER, | |
176 | allowNull: false, | |
177 | defaultValue: 0, | |
178 | validate: { | |
179 | min: 0, | |
180 | isInt: true | |
181 | } | |
d38b8281 C |
182 | }, |
183 | likes: { | |
184 | type: DataTypes.INTEGER, | |
185 | allowNull: false, | |
186 | defaultValue: 0, | |
187 | validate: { | |
188 | min: 0, | |
189 | isInt: true | |
190 | } | |
191 | }, | |
192 | dislikes: { | |
193 | type: DataTypes.INTEGER, | |
194 | allowNull: false, | |
195 | defaultValue: 0, | |
196 | validate: { | |
197 | min: 0, | |
198 | isInt: true | |
199 | } | |
0a6658fd C |
200 | }, |
201 | remote: { | |
202 | type: DataTypes.BOOLEAN, | |
203 | allowNull: false, | |
204 | defaultValue: false | |
aaf61f38 | 205 | } |
feb4bdfd C |
206 | }, |
207 | { | |
319d072e C |
208 | indexes: [ |
209 | { | |
210 | fields: [ 'authorId' ] | |
211 | }, | |
319d072e C |
212 | { |
213 | fields: [ 'name' ] | |
214 | }, | |
215 | { | |
216 | fields: [ 'createdAt' ] | |
217 | }, | |
218 | { | |
219 | fields: [ 'duration' ] | |
220 | }, | |
9e167724 C |
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: { |
feb4bdfd C |
232 | afterDestroy |
233 | } | |
234 | } | |
235 | ) | |
aaf61f38 | 236 | |
e02643f3 C |
237 | const classMethods = [ |
238 | associate, | |
239 | ||
240 | generateThumbnailFromData, | |
e02643f3 C |
241 | list, |
242 | listForApi, | |
243 | listOwnedAndPopulateAuthorAndTags, | |
244 | listOwnedByAuthor, | |
245 | load, | |
e02643f3 C |
246 | loadAndPopulateAuthor, |
247 | loadAndPopulateAuthorAndPodAndTags, | |
93e1258c C |
248 | loadByHostAndUUID, |
249 | loadByUUID, | |
0a6658fd | 250 | loadByUUIDAndPopulateAuthorAndPodAndTags, |
93e1258c | 251 | searchAndPopulateAuthorAndPodAndTags |
e02643f3 C |
252 | ] |
253 | const instanceMethods = [ | |
93e1258c C |
254 | createPreview, |
255 | createThumbnail, | |
256 | createTorrentAndSetInfoHash, | |
e02643f3 | 257 | generateMagnetUri, |
e02643f3 | 258 | getPreviewName, |
d8755eed | 259 | getPreviewPath, |
93e1258c | 260 | getThumbnailName, |
d8755eed | 261 | getThumbnailPath, |
93e1258c C |
262 | getTorrentFileName, |
263 | getVideoFilename, | |
264 | getVideoFilePath, | |
40298b02 | 265 | getOriginalFile, |
e02643f3 | 266 | isOwned, |
93e1258c C |
267 | removeFile, |
268 | removePreview, | |
269 | removeThumbnail, | |
270 | removeTorrent, | |
e02643f3 | 271 | toAddRemoteJSON, |
0aef76c4 | 272 | toFormattedJSON, |
e02643f3 | 273 | toUpdateRemoteJSON, |
40298b02 C |
274 | optimizeOriginalVideofile, |
275 | transcodeOriginalVideofile, | |
d8755eed C |
276 | getOriginalFileHeight, |
277 | getEmbedPath | |
e02643f3 C |
278 | ] |
279 | addMethodsToModel(Video, classMethods, instanceMethods) | |
280 | ||
feb4bdfd C |
281 | return Video |
282 | } | |
aaf61f38 | 283 | |
aaf61f38 C |
284 | // ------------------------------ METHODS ------------------------------ |
285 | ||
feb4bdfd | 286 | function associate (models) { |
e02643f3 | 287 | Video.belongsTo(models.Author, { |
feb4bdfd C |
288 | foreignKey: { |
289 | name: 'authorId', | |
290 | allowNull: false | |
291 | }, | |
292 | onDelete: 'cascade' | |
293 | }) | |
7920c273 | 294 | |
e02643f3 | 295 | Video.belongsToMany(models.Tag, { |
7920c273 C |
296 | foreignKey: 'videoId', |
297 | through: models.VideoTag, | |
298 | onDelete: 'cascade' | |
299 | }) | |
55fa55a9 | 300 | |
e02643f3 | 301 | Video.hasMany(models.VideoAbuse, { |
55fa55a9 C |
302 | foreignKey: { |
303 | name: 'videoId', | |
304 | allowNull: false | |
305 | }, | |
306 | onDelete: 'cascade' | |
307 | }) | |
93e1258c C |
308 | |
309 | Video.hasMany(models.VideoFile, { | |
310 | foreignKey: { | |
311 | name: 'videoId', | |
312 | allowNull: false | |
313 | }, | |
314 | onDelete: 'cascade' | |
315 | }) | |
feb4bdfd C |
316 | } |
317 | ||
91f6f169 | 318 | function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { |
93e1258c | 319 | const tasks = [] |
f285faa0 | 320 | |
93e1258c C |
321 | tasks.push( |
322 | video.removeThumbnail() | |
323 | ) | |
f285faa0 | 324 | |
93e1258c C |
325 | if (video.isOwned()) { |
326 | const removeVideoToFriendsParams = { | |
327 | uuid: video.uuid | |
328 | } | |
f285faa0 | 329 | |
93e1258c C |
330 | tasks.push( |
331 | video.removePreview(), | |
91f6f169 | 332 | removeVideoToFriends(removeVideoToFriendsParams, options.transaction) |
93e1258c C |
333 | ) |
334 | ||
91f6f169 | 335 | // Remove physical files and torrents |
93e1258c C |
336 | video.VideoFiles.forEach(file => { |
337 | video.removeFile(file), | |
338 | video.removeTorrent(file) | |
339 | }) | |
f285faa0 C |
340 | } |
341 | ||
93e1258c | 342 | return Promise.all(tasks) |
558d7c23 C |
343 | } |
344 | ||
40298b02 C |
345 | getOriginalFile = function (this: VideoInstance) { |
346 | if (Array.isArray(this.VideoFiles) === false) return undefined | |
347 | ||
14d3270f C |
348 | // The original file is the file that have the higher resolution |
349 | return maxBy(this.VideoFiles, file => file.resolution) | |
40298b02 C |
350 | } |
351 | ||
93e1258c | 352 | getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { |
14d3270f | 353 | return this.uuid + '-' + videoFile.resolution + videoFile.extname |
f285faa0 C |
354 | } |
355 | ||
70c065d6 | 356 | getThumbnailName = function (this: VideoInstance) { |
f285faa0 | 357 | // We always have a copy of the thumbnail |
0a6658fd C |
358 | const extension = '.jpg' |
359 | return this.uuid + extension | |
558d7c23 C |
360 | } |
361 | ||
70c065d6 | 362 | getPreviewName = function (this: VideoInstance) { |
f285faa0 | 363 | const extension = '.jpg' |
0a6658fd | 364 | return this.uuid + extension |
f285faa0 C |
365 | } |
366 | ||
93e1258c | 367 | getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { |
f285faa0 | 368 | const extension = '.torrent' |
14d3270f | 369 | return this.uuid + '-' + videoFile.resolution + extension |
558d7c23 C |
370 | } |
371 | ||
70c065d6 | 372 | isOwned = function (this: VideoInstance) { |
0a6658fd | 373 | return this.remote === false |
aaf61f38 C |
374 | } |
375 | ||
93e1258c | 376 | createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { |
164174a6 C |
377 | const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height |
378 | ||
14d3270f C |
379 | return generateImageFromVideoFile( |
380 | this.getVideoFilePath(videoFile), | |
381 | CONFIG.STORAGE.PREVIEWS_DIR, | |
164174a6 C |
382 | this.getPreviewName(), |
383 | imageSize | |
14d3270f | 384 | ) |
93e1258c C |
385 | } |
386 | ||
387 | createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { | |
d8755eed C |
388 | const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height |
389 | ||
14d3270f C |
390 | return generateImageFromVideoFile( |
391 | this.getVideoFilePath(videoFile), | |
392 | CONFIG.STORAGE.THUMBNAILS_DIR, | |
393 | this.getThumbnailName(), | |
d8755eed | 394 | imageSize |
14d3270f | 395 | ) |
93e1258c C |
396 | } |
397 | ||
398 | getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { | |
399 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | |
400 | } | |
401 | ||
402 | createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { | |
403 | const options = { | |
404 | announceList: [ | |
405 | [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] | |
406 | ], | |
407 | urlList: [ | |
408 | CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | |
409 | ] | |
410 | } | |
411 | ||
412 | return createTorrentPromise(this.getVideoFilePath(videoFile), options) | |
413 | .then(torrent => { | |
414 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | |
fdbda9e3 C |
415 | logger.info('Creating torrent %s.', filePath) |
416 | ||
93e1258c C |
417 | return writeFilePromise(filePath, torrent).then(() => torrent) |
418 | }) | |
419 | .then(torrent => { | |
420 | const parsedTorrent = parseTorrent(torrent) | |
421 | ||
422 | videoFile.infoHash = parsedTorrent.infoHash | |
423 | }) | |
424 | } | |
425 | ||
426 | generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) { | |
427 | let baseUrlHttp | |
428 | let baseUrlWs | |
429 | ||
430 | if (this.isOwned()) { | |
431 | baseUrlHttp = CONFIG.WEBSERVER.URL | |
432 | baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | |
433 | } else { | |
434 | baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host | |
435 | baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host | |
436 | } | |
437 | ||
438 | const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) | |
439 | const announce = [ baseUrlWs + '/tracker/socket' ] | |
440 | const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] | |
441 | ||
442 | const magnetHash = { | |
443 | xs, | |
444 | announce, | |
445 | urlList, | |
446 | infoHash: videoFile.infoHash, | |
447 | name: this.name | |
448 | } | |
449 | ||
450 | return magnetUtil.encode(magnetHash) | |
451 | } | |
452 | ||
d8755eed C |
453 | getEmbedPath = function (this: VideoInstance) { |
454 | return '/videos/embed/' + this.uuid | |
455 | } | |
456 | ||
457 | getThumbnailPath = function (this: VideoInstance) { | |
458 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | |
459 | } | |
460 | ||
461 | getPreviewPath = function (this: VideoInstance) { | |
462 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | |
463 | } | |
464 | ||
0aef76c4 | 465 | toFormattedJSON = function (this: VideoInstance) { |
feb4bdfd C |
466 | let podHost |
467 | ||
468 | if (this.Author.Pod) { | |
469 | podHost = this.Author.Pod.host | |
470 | } else { | |
471 | // It means it's our video | |
65fcc311 | 472 | podHost = CONFIG.WEBSERVER.HOST |
feb4bdfd C |
473 | } |
474 | ||
6e07c3de | 475 | // Maybe our pod is not up to date and there are new categories since our version |
65fcc311 | 476 | let categoryLabel = VIDEO_CATEGORIES[this.category] |
6e07c3de C |
477 | if (!categoryLabel) categoryLabel = 'Misc' |
478 | ||
6f0c39e2 | 479 | // Maybe our pod is not up to date and there are new licences since our version |
65fcc311 | 480 | let licenceLabel = VIDEO_LICENCES[this.licence] |
6f0c39e2 C |
481 | if (!licenceLabel) licenceLabel = 'Unknown' |
482 | ||
3092476e | 483 | // Language is an optional attribute |
65fcc311 | 484 | let languageLabel = VIDEO_LANGUAGES[this.language] |
3092476e C |
485 | if (!languageLabel) languageLabel = 'Unknown' |
486 | ||
aaf61f38 | 487 | const json = { |
feb4bdfd | 488 | id: this.id, |
0a6658fd | 489 | uuid: this.uuid, |
aaf61f38 | 490 | name: this.name, |
6e07c3de C |
491 | category: this.category, |
492 | categoryLabel, | |
6f0c39e2 C |
493 | licence: this.licence, |
494 | licenceLabel, | |
3092476e C |
495 | language: this.language, |
496 | languageLabel, | |
31b59b47 | 497 | nsfw: this.nsfw, |
aaf61f38 | 498 | description: this.description, |
feb4bdfd | 499 | podHost, |
aaf61f38 | 500 | isLocal: this.isOwned(), |
feb4bdfd | 501 | author: this.Author.name, |
aaf61f38 | 502 | duration: this.duration, |
9e167724 | 503 | views: this.views, |
d38b8281 C |
504 | likes: this.likes, |
505 | dislikes: this.dislikes, | |
6fcd19ba | 506 | tags: map<TagInstance, string>(this.Tags, 'name'), |
d8755eed C |
507 | thumbnailPath: this.getThumbnailPath(), |
508 | previewPath: this.getPreviewPath(), | |
509 | embedPath: this.getEmbedPath(), | |
79066fdf | 510 | createdAt: this.createdAt, |
93e1258c C |
511 | updatedAt: this.updatedAt, |
512 | files: [] | |
aaf61f38 C |
513 | } |
514 | ||
aa8b6df4 C |
515 | // Format and sort video files |
516 | json.files = this.VideoFiles | |
517 | .map(videoFile => { | |
14d3270f | 518 | let resolutionLabel = videoFile.resolution + 'p' |
aa8b6df4 C |
519 | |
520 | const videoFileJson = { | |
521 | resolution: videoFile.resolution, | |
522 | resolutionLabel, | |
523 | magnetUri: this.generateMagnetUri(videoFile), | |
524 | size: videoFile.size | |
525 | } | |
526 | ||
527 | return videoFileJson | |
528 | }) | |
529 | .sort((a, b) => { | |
530 | if (a.resolution < b.resolution) return 1 | |
531 | if (a.resolution === b.resolution) return 0 | |
532 | return -1 | |
533 | }) | |
93e1258c | 534 | |
aaf61f38 C |
535 | return json |
536 | } | |
537 | ||
6fcd19ba | 538 | toAddRemoteJSON = function (this: VideoInstance) { |
4d324488 | 539 | // Get thumbnail data to send to the other pod |
65fcc311 | 540 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) |
aaf61f38 | 541 | |
6fcd19ba | 542 | return readFileBufferPromise(thumbnailPath).then(thumbnailData => { |
aaf61f38 | 543 | const remoteVideo = { |
0a6658fd | 544 | uuid: this.uuid, |
e02643f3 C |
545 | name: this.name, |
546 | category: this.category, | |
547 | licence: this.licence, | |
548 | language: this.language, | |
549 | nsfw: this.nsfw, | |
550 | description: this.description, | |
e02643f3 C |
551 | author: this.Author.name, |
552 | duration: this.duration, | |
4d324488 | 553 | thumbnailData: thumbnailData.toString('binary'), |
6fcd19ba | 554 | tags: map<TagInstance, string>(this.Tags, 'name'), |
e02643f3 C |
555 | createdAt: this.createdAt, |
556 | updatedAt: this.updatedAt, | |
e02643f3 C |
557 | views: this.views, |
558 | likes: this.likes, | |
93e1258c C |
559 | dislikes: this.dislikes, |
560 | files: [] | |
aaf61f38 C |
561 | } |
562 | ||
93e1258c C |
563 | this.VideoFiles.forEach(videoFile => { |
564 | remoteVideo.files.push({ | |
565 | infoHash: videoFile.infoHash, | |
566 | resolution: videoFile.resolution, | |
567 | extname: videoFile.extname, | |
568 | size: videoFile.size | |
569 | }) | |
570 | }) | |
571 | ||
6fcd19ba | 572 | return remoteVideo |
aaf61f38 C |
573 | }) |
574 | } | |
575 | ||
70c065d6 | 576 | toUpdateRemoteJSON = function (this: VideoInstance) { |
7b1f49de | 577 | const json = { |
0a6658fd | 578 | uuid: this.uuid, |
7b1f49de | 579 | name: this.name, |
6e07c3de | 580 | category: this.category, |
6f0c39e2 | 581 | licence: this.licence, |
3092476e | 582 | language: this.language, |
31b59b47 | 583 | nsfw: this.nsfw, |
7b1f49de | 584 | description: this.description, |
7b1f49de C |
585 | author: this.Author.name, |
586 | duration: this.duration, | |
6fcd19ba | 587 | tags: map<TagInstance, string>(this.Tags, 'name'), |
7b1f49de | 588 | createdAt: this.createdAt, |
79066fdf | 589 | updatedAt: this.updatedAt, |
d38b8281 C |
590 | views: this.views, |
591 | likes: this.likes, | |
93e1258c C |
592 | dislikes: this.dislikes, |
593 | files: [] | |
7b1f49de C |
594 | } |
595 | ||
93e1258c C |
596 | this.VideoFiles.forEach(videoFile => { |
597 | json.files.push({ | |
598 | infoHash: videoFile.infoHash, | |
599 | resolution: videoFile.resolution, | |
600 | extname: videoFile.extname, | |
601 | size: videoFile.size | |
602 | }) | |
603 | }) | |
604 | ||
7b1f49de C |
605 | return json |
606 | } | |
607 | ||
40298b02 | 608 | optimizeOriginalVideofile = function (this: VideoInstance) { |
65fcc311 | 609 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
227d02fe | 610 | const newExtname = '.mp4' |
40298b02 | 611 | const inputVideoFile = this.getOriginalFile() |
93e1258c C |
612 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) |
613 | const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | |
227d02fe | 614 | |
14d3270f C |
615 | const transcodeOptions = { |
616 | inputPath: videoInputPath, | |
617 | outputPath: videoOutputPath | |
618 | } | |
619 | ||
620 | return transcode(transcodeOptions) | |
621 | .then(() => { | |
622 | return unlinkPromise(videoInputPath) | |
623 | }) | |
624 | .then(() => { | |
625 | // Important to do this before getVideoFilename() to take in account the new file extension | |
626 | inputVideoFile.set('extname', newExtname) | |
627 | ||
628 | return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) | |
629 | }) | |
630 | .then(() => { | |
631 | return statPromise(this.getVideoFilePath(inputVideoFile)) | |
632 | }) | |
633 | .then(stats => { | |
634 | return inputVideoFile.set('size', stats.size) | |
635 | }) | |
636 | .then(() => { | |
637 | return this.createTorrentAndSetInfoHash(inputVideoFile) | |
638 | }) | |
639 | .then(() => { | |
640 | return inputVideoFile.save() | |
641 | }) | |
642 | .then(() => { | |
643 | return undefined | |
644 | }) | |
645 | .catch(err => { | |
646 | // Auto destruction... | |
647 | this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) | |
648 | ||
649 | throw err | |
650 | }) | |
227d02fe C |
651 | } |
652 | ||
40298b02 C |
653 | transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { |
654 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | |
655 | const extname = '.mp4' | |
656 | ||
657 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | |
658 | const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) | |
659 | ||
660 | const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({ | |
661 | resolution, | |
662 | extname, | |
663 | size: 0, | |
664 | videoId: this.id | |
665 | }) | |
666 | const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) | |
14d3270f C |
667 | |
668 | const transcodeOptions = { | |
669 | inputPath: videoInputPath, | |
670 | outputPath: videoOutputPath, | |
671 | resolution | |
672 | } | |
673 | return transcode(transcodeOptions) | |
674 | .then(() => { | |
675 | return statPromise(videoOutputPath) | |
676 | }) | |
677 | .then(stats => { | |
678 | newVideoFile.set('size', stats.size) | |
679 | ||
680 | return undefined | |
681 | }) | |
682 | .then(() => { | |
683 | return this.createTorrentAndSetInfoHash(newVideoFile) | |
684 | }) | |
685 | .then(() => { | |
686 | return newVideoFile.save() | |
687 | }) | |
688 | .then(() => { | |
689 | return this.VideoFiles.push(newVideoFile) | |
690 | }) | |
691 | .then(() => undefined) | |
40298b02 C |
692 | } |
693 | ||
694 | getOriginalFileHeight = function (this: VideoInstance) { | |
695 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | |
696 | ||
14d3270f | 697 | return getVideoFileHeight(originalFilePath) |
40298b02 C |
698 | } |
699 | ||
93e1258c C |
700 | removeThumbnail = function (this: VideoInstance) { |
701 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | |
702 | return unlinkPromise(thumbnailPath) | |
703 | } | |
704 | ||
705 | removePreview = function (this: VideoInstance) { | |
706 | // Same name than video thumbnail | |
707 | return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | |
708 | } | |
709 | ||
710 | removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { | |
711 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | |
712 | return unlinkPromise(filePath) | |
713 | } | |
714 | ||
715 | removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { | |
b0f9f39e C |
716 | const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) |
717 | return unlinkPromise(torrentPath) | |
93e1258c C |
718 | } |
719 | ||
aaf61f38 C |
720 | // ------------------------------ STATICS ------------------------------ |
721 | ||
6fcd19ba | 722 | generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { |
c77fa067 C |
723 | // Creating the thumbnail for a remote video |
724 | ||
725 | const thumbnailName = video.getThumbnailName() | |
65fcc311 | 726 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) |
6fcd19ba C |
727 | return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => { |
728 | return thumbnailName | |
c77fa067 C |
729 | }) |
730 | } | |
731 | ||
6fcd19ba | 732 | list = function () { |
93e1258c C |
733 | const query = { |
734 | include: [ Video['sequelize'].models.VideoFile ] | |
735 | } | |
736 | ||
737 | return Video.findAll(query) | |
b769007f C |
738 | } |
739 | ||
6fcd19ba | 740 | listForApi = function (start: number, count: number, sort: string) { |
556ddc31 | 741 | // Exclude blacklisted videos from the list |
feb4bdfd | 742 | const query = { |
e02643f3 | 743 | distinct: true, |
feb4bdfd C |
744 | offset: start, |
745 | limit: count, | |
e02643f3 | 746 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], |
feb4bdfd C |
747 | include: [ |
748 | { | |
e02643f3 C |
749 | model: Video['sequelize'].models.Author, |
750 | include: [ { model: Video['sequelize'].models.Pod, required: false } ] | |
7920c273 | 751 | }, |
93e1258c C |
752 | Video['sequelize'].models.Tag, |
753 | Video['sequelize'].models.VideoFile | |
198b205c | 754 | ], |
e02643f3 | 755 | where: createBaseVideosWhere() |
feb4bdfd C |
756 | } |
757 | ||
6fcd19ba C |
758 | return Video.findAndCountAll(query).then(({ rows, count }) => { |
759 | return { | |
760 | data: rows, | |
761 | total: count | |
762 | } | |
feb4bdfd | 763 | }) |
aaf61f38 C |
764 | } |
765 | ||
0a6658fd | 766 | loadByHostAndUUID = function (fromHost: string, uuid: string) { |
feb4bdfd C |
767 | const query = { |
768 | where: { | |
0a6658fd | 769 | uuid |
feb4bdfd C |
770 | }, |
771 | include: [ | |
93e1258c C |
772 | { |
773 | model: Video['sequelize'].models.VideoFile | |
774 | }, | |
feb4bdfd | 775 | { |
e02643f3 | 776 | model: Video['sequelize'].models.Author, |
feb4bdfd C |
777 | include: [ |
778 | { | |
e02643f3 | 779 | model: Video['sequelize'].models.Pod, |
7920c273 | 780 | required: true, |
feb4bdfd C |
781 | where: { |
782 | host: fromHost | |
783 | } | |
784 | } | |
785 | ] | |
786 | } | |
787 | ] | |
788 | } | |
aaf61f38 | 789 | |
6fcd19ba | 790 | return Video.findOne(query) |
aaf61f38 C |
791 | } |
792 | ||
6fcd19ba | 793 | listOwnedAndPopulateAuthorAndTags = function () { |
feb4bdfd C |
794 | const query = { |
795 | where: { | |
0a6658fd | 796 | remote: false |
feb4bdfd | 797 | }, |
93e1258c C |
798 | include: [ |
799 | Video['sequelize'].models.VideoFile, | |
800 | Video['sequelize'].models.Author, | |
801 | Video['sequelize'].models.Tag | |
802 | ] | |
feb4bdfd C |
803 | } |
804 | ||
6fcd19ba | 805 | return Video.findAll(query) |
aaf61f38 C |
806 | } |
807 | ||
6fcd19ba | 808 | listOwnedByAuthor = function (author: string) { |
feb4bdfd C |
809 | const query = { |
810 | where: { | |
0a6658fd | 811 | remote: false |
feb4bdfd C |
812 | }, |
813 | include: [ | |
93e1258c C |
814 | { |
815 | model: Video['sequelize'].models.VideoFile | |
816 | }, | |
feb4bdfd | 817 | { |
e02643f3 | 818 | model: Video['sequelize'].models.Author, |
feb4bdfd C |
819 | where: { |
820 | name: author | |
821 | } | |
822 | } | |
823 | ] | |
824 | } | |
9bd26629 | 825 | |
6fcd19ba | 826 | return Video.findAll(query) |
aaf61f38 C |
827 | } |
828 | ||
0a6658fd | 829 | load = function (id: number) { |
6fcd19ba | 830 | return Video.findById(id) |
feb4bdfd C |
831 | } |
832 | ||
0a6658fd C |
833 | loadByUUID = function (uuid: string) { |
834 | const query = { | |
835 | where: { | |
836 | uuid | |
93e1258c C |
837 | }, |
838 | include: [ Video['sequelize'].models.VideoFile ] | |
0a6658fd C |
839 | } |
840 | return Video.findOne(query) | |
841 | } | |
842 | ||
843 | loadAndPopulateAuthor = function (id: number) { | |
feb4bdfd | 844 | const options = { |
93e1258c | 845 | include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ] |
feb4bdfd C |
846 | } |
847 | ||
6fcd19ba | 848 | return Video.findById(id, options) |
feb4bdfd C |
849 | } |
850 | ||
0a6658fd | 851 | loadAndPopulateAuthorAndPodAndTags = function (id: number) { |
feb4bdfd C |
852 | const options = { |
853 | include: [ | |
854 | { | |
e02643f3 C |
855 | model: Video['sequelize'].models.Author, |
856 | include: [ { model: Video['sequelize'].models.Pod, required: false } ] | |
7920c273 | 857 | }, |
93e1258c C |
858 | Video['sequelize'].models.Tag, |
859 | Video['sequelize'].models.VideoFile | |
feb4bdfd C |
860 | ] |
861 | } | |
862 | ||
6fcd19ba | 863 | return Video.findById(id, options) |
aaf61f38 C |
864 | } |
865 | ||
0a6658fd C |
866 | loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { |
867 | const options = { | |
868 | where: { | |
869 | uuid | |
870 | }, | |
871 | include: [ | |
872 | { | |
873 | model: Video['sequelize'].models.Author, | |
874 | include: [ { model: Video['sequelize'].models.Pod, required: false } ] | |
875 | }, | |
93e1258c C |
876 | Video['sequelize'].models.Tag, |
877 | Video['sequelize'].models.VideoFile | |
0a6658fd C |
878 | ] |
879 | } | |
880 | ||
881 | return Video.findOne(options) | |
882 | } | |
883 | ||
6fcd19ba | 884 | searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { |
e6d4b0ff | 885 | const podInclude: Sequelize.IncludeOptions = { |
e02643f3 | 886 | model: Video['sequelize'].models.Pod, |
7920c273 | 887 | required: false |
feb4bdfd | 888 | } |
7920c273 | 889 | |
e6d4b0ff | 890 | const authorInclude: Sequelize.IncludeOptions = { |
e02643f3 | 891 | model: Video['sequelize'].models.Author, |
feb4bdfd C |
892 | include: [ |
893 | podInclude | |
894 | ] | |
895 | } | |
896 | ||
e6d4b0ff | 897 | const tagInclude: Sequelize.IncludeOptions = { |
e02643f3 | 898 | model: Video['sequelize'].models.Tag |
7920c273 C |
899 | } |
900 | ||
93e1258c C |
901 | const videoFileInclude: Sequelize.IncludeOptions = { |
902 | model: Video['sequelize'].models.VideoFile | |
903 | } | |
904 | ||
556ddc31 | 905 | const query: Sequelize.FindOptions<VideoAttributes> = { |
e02643f3 C |
906 | distinct: true, |
907 | where: createBaseVideosWhere(), | |
feb4bdfd C |
908 | offset: start, |
909 | limit: count, | |
e02643f3 | 910 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] |
feb4bdfd C |
911 | } |
912 | ||
aaf61f38 | 913 | // Make an exact search with the magnet |
55723d16 | 914 | if (field === 'magnetUri') { |
93e1258c C |
915 | videoFileInclude.where = { |
916 | infoHash: magnetUtil.decode(value).infoHash | |
917 | } | |
55723d16 | 918 | } else if (field === 'tags') { |
e02643f3 | 919 | const escapedValue = Video['sequelize'].escape('%' + value + '%') |
e6d4b0ff | 920 | query.where['id'].$in = Video['sequelize'].literal( |
6fcd19ba C |
921 | `(SELECT "VideoTags"."videoId" |
922 | FROM "Tags" | |
923 | INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" | |
18c8e945 | 924 | WHERE name ILIKE ${escapedValue} |
6fcd19ba | 925 | )` |
198b205c | 926 | ) |
7920c273 C |
927 | } else if (field === 'host') { |
928 | // FIXME: Include our pod? (not stored in the database) | |
929 | podInclude.where = { | |
930 | host: { | |
18c8e945 | 931 | $iLike: '%' + value + '%' |
feb4bdfd | 932 | } |
feb4bdfd | 933 | } |
7920c273 | 934 | podInclude.required = true |
feb4bdfd | 935 | } else if (field === 'author') { |
7920c273 C |
936 | authorInclude.where = { |
937 | name: { | |
18c8e945 | 938 | $iLike: '%' + value + '%' |
feb4bdfd C |
939 | } |
940 | } | |
7920c273 C |
941 | |
942 | // authorInclude.or = true | |
aaf61f38 | 943 | } else { |
feb4bdfd | 944 | query.where[field] = { |
18c8e945 | 945 | $iLike: '%' + value + '%' |
feb4bdfd | 946 | } |
aaf61f38 C |
947 | } |
948 | ||
7920c273 | 949 | query.include = [ |
93e1258c | 950 | authorInclude, tagInclude, videoFileInclude |
7920c273 C |
951 | ] |
952 | ||
6fcd19ba C |
953 | return Video.findAndCountAll(query).then(({ rows, count }) => { |
954 | return { | |
955 | data: rows, | |
956 | total: count | |
957 | } | |
feb4bdfd | 958 | }) |
aaf61f38 C |
959 | } |
960 | ||
aaf61f38 C |
961 | // --------------------------------------------------------------------------- |
962 | ||
15d4ee04 C |
963 | function createBaseVideosWhere () { |
964 | return { | |
965 | id: { | |
e02643f3 | 966 | $notIn: Video['sequelize'].literal( |
15d4ee04 C |
967 | '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' |
968 | ) | |
969 | } | |
970 | } | |
971 | } |