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