diff options
author | Chocobozzz <me@florianbigard.com> | 2019-04-17 10:07:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-04-24 16:25:52 +0200 |
commit | e8bafea35bc930cb8ac5b2d521a188642a1adffe (patch) | |
tree | 7537f957ed7307b464e3c90b71b813d992acaade /server/models/video | |
parent | 94565d52bb2883e09f16d1363170ac9c0dccb7a1 (diff) | |
download | PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.gz PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.zst PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.zip |
Create a dedicated table to track video thumbnails
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/thumbnail.ts | 116 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 8 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 85 | ||||
-rw-r--r-- | server/models/video/video.ts | 155 |
4 files changed, 266 insertions, 98 deletions
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts new file mode 100644 index 000000000..baa5533ac --- /dev/null +++ b/server/models/video/thumbnail.ts | |||
@@ -0,0 +1,116 @@ | |||
1 | import { join } from 'path' | ||
2 | import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { remove } from 'fs-extra' | ||
6 | import { CONFIG } from '../../initializers/config' | ||
7 | import { VideoModel } from './video' | ||
8 | import { VideoPlaylistModel } from './video-playlist' | ||
9 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
10 | |||
11 | @Table({ | ||
12 | tableName: 'thumbnail', | ||
13 | indexes: [ | ||
14 | { | ||
15 | fields: [ 'videoId' ] | ||
16 | }, | ||
17 | { | ||
18 | fields: [ 'videoPlaylistId' ], | ||
19 | unique: true | ||
20 | } | ||
21 | ] | ||
22 | }) | ||
23 | export class ThumbnailModel extends Model<ThumbnailModel> { | ||
24 | |||
25 | @AllowNull(false) | ||
26 | @Column | ||
27 | filename: string | ||
28 | |||
29 | @AllowNull(true) | ||
30 | @Default(null) | ||
31 | @Column | ||
32 | height: number | ||
33 | |||
34 | @AllowNull(true) | ||
35 | @Default(null) | ||
36 | @Column | ||
37 | width: number | ||
38 | |||
39 | @AllowNull(false) | ||
40 | @Column | ||
41 | type: ThumbnailType | ||
42 | |||
43 | @AllowNull(true) | ||
44 | @Column | ||
45 | url: string | ||
46 | |||
47 | @ForeignKey(() => VideoModel) | ||
48 | @Column | ||
49 | videoId: number | ||
50 | |||
51 | @BelongsTo(() => VideoModel, { | ||
52 | foreignKey: { | ||
53 | allowNull: true | ||
54 | }, | ||
55 | onDelete: 'CASCADE' | ||
56 | }) | ||
57 | Video: VideoModel | ||
58 | |||
59 | @ForeignKey(() => VideoPlaylistModel) | ||
60 | @Column | ||
61 | videoPlaylistId: number | ||
62 | |||
63 | @BelongsTo(() => VideoPlaylistModel, { | ||
64 | foreignKey: { | ||
65 | allowNull: true | ||
66 | }, | ||
67 | onDelete: 'CASCADE' | ||
68 | }) | ||
69 | VideoPlaylist: VideoPlaylistModel | ||
70 | |||
71 | @CreatedAt | ||
72 | createdAt: Date | ||
73 | |||
74 | @UpdatedAt | ||
75 | updatedAt: Date | ||
76 | |||
77 | private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { | ||
78 | [ThumbnailType.THUMBNAIL]: { | ||
79 | label: 'thumbnail', | ||
80 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, | ||
81 | staticPath: STATIC_PATHS.THUMBNAILS | ||
82 | }, | ||
83 | [ThumbnailType.PREVIEW]: { | ||
84 | label: 'preview', | ||
85 | directory: CONFIG.STORAGE.PREVIEWS_DIR, | ||
86 | staticPath: STATIC_PATHS.PREVIEWS | ||
87 | } | ||
88 | } | ||
89 | |||
90 | @AfterDestroy | ||
91 | static removeFilesAndSendDelete (instance: ThumbnailModel) { | ||
92 | logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) | ||
93 | |||
94 | // Don't block the transaction | ||
95 | instance.removeThumbnail() | ||
96 | .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err)) | ||
97 | } | ||
98 | |||
99 | static generateDefaultPreviewName (videoUUID: string) { | ||
100 | return videoUUID + '.jpg' | ||
101 | } | ||
102 | |||
103 | getUrl () { | ||
104 | if (this.url) return this.url | ||
105 | |||
106 | const staticPath = ThumbnailModel.types[this.type].staticPath | ||
107 | return WEBSERVER.URL + staticPath + this.filename | ||
108 | } | ||
109 | |||
110 | removeThumbnail () { | ||
111 | const directory = ThumbnailModel.types[this.type].directory | ||
112 | const thumbnailPath = join(directory, this.filename) | ||
113 | |||
114 | return remove(thumbnailPath) | ||
115 | } | ||
116 | } | ||
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index 64771b1ff..89992a5a8 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | ActivityUrlObject, | 7 | ActivityUrlObject, |
8 | VideoTorrentObject | 8 | VideoTorrentObject |
9 | } from '../../../shared/models/activitypub/objects' | 9 | } from '../../../shared/models/activitypub/objects' |
10 | import { MIMETYPES, THUMBNAILS_SIZE, WEBSERVER } from '../../initializers/constants' | 10 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
11 | import { VideoCaptionModel } from './video-caption' | 11 | import { VideoCaptionModel } from './video-caption' |
12 | import { | 12 | import { |
13 | getVideoCommentsActivityPubUrl, | 13 | getVideoCommentsActivityPubUrl, |
@@ -326,10 +326,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
326 | subtitleLanguage, | 326 | subtitleLanguage, |
327 | icon: { | 327 | icon: { |
328 | type: 'Image', | 328 | type: 'Image', |
329 | url: video.getThumbnailUrl(baseUrlHttp), | 329 | url: video.getThumbnail().getUrl(), |
330 | mediaType: 'image/jpeg', | 330 | mediaType: 'image/jpeg', |
331 | width: THUMBNAILS_SIZE.width, | 331 | width: video.getThumbnail().width, |
332 | height: THUMBNAILS_SIZE.height | 332 | height: video.getThumbnail().height |
333 | }, | 333 | }, |
334 | url, | 334 | url, |
335 | likes: getVideoLikesActivityPubUrl(video), | 335 | likes: getVideoLikesActivityPubUrl(video), |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 0725b752a..073609c24 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { | 1 | import { |
2 | AllowNull, | 2 | AllowNull, |
3 | BeforeDestroy, | ||
4 | BelongsTo, | 3 | BelongsTo, |
5 | Column, | 4 | Column, |
6 | CreatedAt, | 5 | CreatedAt, |
@@ -8,6 +7,7 @@ import { | |||
8 | Default, | 7 | Default, |
9 | ForeignKey, | 8 | ForeignKey, |
10 | HasMany, | 9 | HasMany, |
10 | HasOne, | ||
11 | Is, | 11 | Is, |
12 | IsUUID, | 12 | IsUUID, |
13 | Model, | 13 | Model, |
@@ -40,16 +40,16 @@ import { join } from 'path' | |||
40 | import { VideoPlaylistElementModel } from './video-playlist-element' | 40 | import { VideoPlaylistElementModel } from './video-playlist-element' |
41 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 41 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
42 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | 42 | import { activityPubCollectionPagination } from '../../helpers/activitypub' |
43 | import { remove } from 'fs-extra' | ||
44 | import { logger } from '../../helpers/logger' | ||
45 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | 43 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' |
46 | import { CONFIG } from '../../initializers/config' | 44 | import { ThumbnailModel } from './thumbnail' |
45 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
47 | 46 | ||
48 | enum ScopeNames { | 47 | enum ScopeNames { |
49 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 48 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
50 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', | 49 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', |
51 | WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', | 50 | WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', |
52 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 51 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
52 | WITH_THUMBNAIL = 'WITH_THUMBNAIL', | ||
53 | WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' | 53 | WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' |
54 | } | 54 | } |
55 | 55 | ||
@@ -62,6 +62,14 @@ type AvailableForListOptions = { | |||
62 | } | 62 | } |
63 | 63 | ||
64 | @Scopes({ | 64 | @Scopes({ |
65 | [ ScopeNames.WITH_THUMBNAIL ]: { | ||
66 | include: [ | ||
67 | { | ||
68 | model: () => ThumbnailModel, | ||
69 | required: false | ||
70 | } | ||
71 | ] | ||
72 | }, | ||
65 | [ ScopeNames.WITH_VIDEOS_LENGTH ]: { | 73 | [ ScopeNames.WITH_VIDEOS_LENGTH ]: { |
66 | attributes: { | 74 | attributes: { |
67 | include: [ | 75 | include: [ |
@@ -256,12 +264,15 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
256 | }) | 264 | }) |
257 | VideoPlaylistElements: VideoPlaylistElementModel[] | 265 | VideoPlaylistElements: VideoPlaylistElementModel[] |
258 | 266 | ||
259 | @BeforeDestroy | 267 | @HasOne(() => ThumbnailModel, { |
260 | static async removeFiles (instance: VideoPlaylistModel) { | 268 | foreignKey: { |
261 | logger.info('Removing files of video playlist %s.', instance.url) | 269 | name: 'videoPlaylistId', |
262 | 270 | allowNull: true | |
263 | return instance.removeThumbnail() | 271 | }, |
264 | } | 272 | onDelete: 'CASCADE', |
273 | hooks: true | ||
274 | }) | ||
275 | Thumbnail: ThumbnailModel | ||
265 | 276 | ||
266 | static listForApi (options: { | 277 | static listForApi (options: { |
267 | followerActorId: number | 278 | followerActorId: number |
@@ -292,7 +303,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
292 | } as AvailableForListOptions | 303 | } as AvailableForListOptions |
293 | ] | 304 | ] |
294 | } as any, // FIXME: typings | 305 | } as any, // FIXME: typings |
295 | ScopeNames.WITH_VIDEOS_LENGTH | 306 | ScopeNames.WITH_VIDEOS_LENGTH, |
307 | ScopeNames.WITH_THUMBNAIL | ||
296 | ] | 308 | ] |
297 | 309 | ||
298 | return VideoPlaylistModel | 310 | return VideoPlaylistModel |
@@ -365,7 +377,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
365 | } | 377 | } |
366 | 378 | ||
367 | return VideoPlaylistModel | 379 | return VideoPlaylistModel |
368 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ]) | 380 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) |
369 | .findOne(query) | 381 | .findOne(query) |
370 | } | 382 | } |
371 | 383 | ||
@@ -378,7 +390,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
378 | } | 390 | } |
379 | 391 | ||
380 | return VideoPlaylistModel | 392 | return VideoPlaylistModel |
381 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ]) | 393 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) |
382 | .findOne(query) | 394 | .findOne(query) |
383 | } | 395 | } |
384 | 396 | ||
@@ -389,7 +401,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
389 | } | 401 | } |
390 | } | 402 | } |
391 | 403 | ||
392 | return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query) | 404 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
393 | } | 405 | } |
394 | 406 | ||
395 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 407 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
@@ -411,24 +423,34 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
411 | return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) | 423 | return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) |
412 | } | 424 | } |
413 | 425 | ||
414 | getThumbnailName () { | 426 | setThumbnail (thumbnail: ThumbnailModel) { |
427 | this.Thumbnail = thumbnail | ||
428 | } | ||
429 | |||
430 | getThumbnail () { | ||
431 | return this.Thumbnail | ||
432 | } | ||
433 | |||
434 | hasThumbnail () { | ||
435 | return !!this.Thumbnail | ||
436 | } | ||
437 | |||
438 | generateThumbnailName () { | ||
415 | const extension = '.jpg' | 439 | const extension = '.jpg' |
416 | 440 | ||
417 | return 'playlist-' + this.uuid + extension | 441 | return 'playlist-' + this.uuid + extension |
418 | } | 442 | } |
419 | 443 | ||
420 | getThumbnailUrl () { | 444 | getThumbnailUrl () { |
421 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | 445 | if (!this.hasThumbnail()) return null |
446 | |||
447 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename | ||
422 | } | 448 | } |
423 | 449 | ||
424 | getThumbnailStaticPath () { | 450 | getThumbnailStaticPath () { |
425 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | 451 | if (!this.hasThumbnail()) return null |
426 | } | ||
427 | 452 | ||
428 | removeThumbnail () { | 453 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename) |
429 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
430 | return remove(thumbnailPath) | ||
431 | .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err })) | ||
432 | } | 454 | } |
433 | 455 | ||
434 | setAsRefreshed () { | 456 | setAsRefreshed () { |
@@ -482,6 +504,17 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
482 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) | 504 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) |
483 | } | 505 | } |
484 | 506 | ||
507 | let icon: ActivityIconObject | ||
508 | if (this.hasThumbnail()) { | ||
509 | icon = { | ||
510 | type: 'Image' as 'Image', | ||
511 | url: this.getThumbnailUrl(), | ||
512 | mediaType: 'image/jpeg' as 'image/jpeg', | ||
513 | width: THUMBNAILS_SIZE.width, | ||
514 | height: THUMBNAILS_SIZE.height | ||
515 | } | ||
516 | } | ||
517 | |||
485 | return activityPubCollectionPagination(this.url, handler, page) | 518 | return activityPubCollectionPagination(this.url, handler, page) |
486 | .then(o => { | 519 | .then(o => { |
487 | return Object.assign(o, { | 520 | return Object.assign(o, { |
@@ -492,13 +525,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
492 | published: this.createdAt.toISOString(), | 525 | published: this.createdAt.toISOString(), |
493 | updated: this.updatedAt.toISOString(), | 526 | updated: this.updatedAt.toISOString(), |
494 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], | 527 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], |
495 | icon: { | 528 | icon |
496 | type: 'Image' as 'Image', | ||
497 | url: this.getThumbnailUrl(), | ||
498 | mediaType: 'image/jpeg' as 'image/jpeg', | ||
499 | width: THUMBNAILS_SIZE.width, | ||
500 | height: THUMBNAILS_SIZE.height | ||
501 | } | ||
502 | }) | 529 | }) |
503 | }) | 530 | }) |
504 | } | 531 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 38447797e..9840d17fd 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -107,6 +107,8 @@ import { VideoImportModel } from './video-import' | |||
107 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 107 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
108 | import { VideoPlaylistElementModel } from './video-playlist-element' | 108 | import { VideoPlaylistElementModel } from './video-playlist-element' |
109 | import { CONFIG } from '../../initializers/config' | 109 | import { CONFIG } from '../../initializers/config' |
110 | import { ThumbnailModel } from './thumbnail' | ||
111 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
110 | 112 | ||
111 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 113 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
112 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 114 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -181,7 +183,8 @@ export enum ScopeNames { | |||
181 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 183 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
182 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', | 184 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
183 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 185 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
184 | WITH_USER_ID = 'WITH_USER_ID' | 186 | WITH_USER_ID = 'WITH_USER_ID', |
187 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' | ||
185 | } | 188 | } |
186 | 189 | ||
187 | type ForAPIOptions = { | 190 | type ForAPIOptions = { |
@@ -473,6 +476,14 @@ type AvailableForListIDsOptions = { | |||
473 | 476 | ||
474 | return query | 477 | return query |
475 | }, | 478 | }, |
479 | [ ScopeNames.WITH_THUMBNAILS ]: { | ||
480 | include: [ | ||
481 | { | ||
482 | model: () => ThumbnailModel, | ||
483 | required: false | ||
484 | } | ||
485 | ] | ||
486 | }, | ||
476 | [ ScopeNames.WITH_USER_ID ]: { | 487 | [ ScopeNames.WITH_USER_ID ]: { |
477 | include: [ | 488 | include: [ |
478 | { | 489 | { |
@@ -771,6 +782,16 @@ export class VideoModel extends Model<VideoModel> { | |||
771 | }) | 782 | }) |
772 | Tags: TagModel[] | 783 | Tags: TagModel[] |
773 | 784 | ||
785 | @HasMany(() => ThumbnailModel, { | ||
786 | foreignKey: { | ||
787 | name: 'videoId', | ||
788 | allowNull: true | ||
789 | }, | ||
790 | hooks: true, | ||
791 | onDelete: 'cascade' | ||
792 | }) | ||
793 | Thumbnails: ThumbnailModel[] | ||
794 | |||
774 | @HasMany(() => VideoPlaylistElementModel, { | 795 | @HasMany(() => VideoPlaylistElementModel, { |
775 | foreignKey: { | 796 | foreignKey: { |
776 | name: 'videoId', | 797 | name: 'videoId', |
@@ -920,15 +941,11 @@ export class VideoModel extends Model<VideoModel> { | |||
920 | 941 | ||
921 | logger.info('Removing files of video %s.', instance.url) | 942 | logger.info('Removing files of video %s.', instance.url) |
922 | 943 | ||
923 | tasks.push(instance.removeThumbnail()) | ||
924 | |||
925 | if (instance.isOwned()) { | 944 | if (instance.isOwned()) { |
926 | if (!Array.isArray(instance.VideoFiles)) { | 945 | if (!Array.isArray(instance.VideoFiles)) { |
927 | instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[] | 946 | instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[] |
928 | } | 947 | } |
929 | 948 | ||
930 | tasks.push(instance.removePreview()) | ||
931 | |||
932 | // Remove physical files and torrents | 949 | // Remove physical files and torrents |
933 | instance.VideoFiles.forEach(file => { | 950 | instance.VideoFiles.forEach(file => { |
934 | tasks.push(instance.removeFile(file)) | 951 | tasks.push(instance.removeFile(file)) |
@@ -955,7 +972,11 @@ export class VideoModel extends Model<VideoModel> { | |||
955 | } | 972 | } |
956 | } | 973 | } |
957 | 974 | ||
958 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query) | 975 | return VideoModel.scope([ |
976 | ScopeNames.WITH_FILES, | ||
977 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
978 | ScopeNames.WITH_THUMBNAILS | ||
979 | ]).findAll(query) | ||
959 | } | 980 | } |
960 | 981 | ||
961 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { | 982 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { |
@@ -1048,7 +1069,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1048 | 1069 | ||
1049 | return Bluebird.all([ | 1070 | return Bluebird.all([ |
1050 | // FIXME: typing issue | 1071 | // FIXME: typing issue |
1051 | VideoModel.findAll(query as any), | 1072 | VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any), |
1052 | VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) | 1073 | VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) |
1053 | ]).then(([ rows, totals ]) => { | 1074 | ]).then(([ rows, totals ]) => { |
1054 | // totals: totalVideos + totalVideoShares | 1075 | // totals: totalVideos + totalVideoShares |
@@ -1102,12 +1123,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1102 | }) | 1123 | }) |
1103 | } | 1124 | } |
1104 | 1125 | ||
1105 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { | 1126 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS) |
1106 | return { | 1127 | .findAndCountAll(query) |
1107 | data: rows, | 1128 | .then(({ rows, count }) => { |
1108 | total: count | 1129 | return { |
1109 | } | 1130 | data: rows, |
1110 | }) | 1131 | total: count |
1132 | } | ||
1133 | }) | ||
1111 | } | 1134 | } |
1112 | 1135 | ||
1113 | static async listForApi (options: { | 1136 | static async listForApi (options: { |
@@ -1296,7 +1319,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1296 | transaction: t | 1319 | transaction: t |
1297 | } | 1320 | } |
1298 | 1321 | ||
1299 | return VideoModel.findOne(options) | 1322 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) |
1300 | } | 1323 | } |
1301 | 1324 | ||
1302 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { | 1325 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { |
@@ -1306,7 +1329,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1306 | transaction: t | 1329 | transaction: t |
1307 | } | 1330 | } |
1308 | 1331 | ||
1309 | return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options) | 1332 | return VideoModel.scope([ |
1333 | ScopeNames.WITH_BLACKLISTED, | ||
1334 | ScopeNames.WITH_USER_ID, | ||
1335 | ScopeNames.WITH_THUMBNAILS | ||
1336 | ]).findOne(options) | ||
1310 | } | 1337 | } |
1311 | 1338 | ||
1312 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { | 1339 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { |
@@ -1318,12 +1345,15 @@ export class VideoModel extends Model<VideoModel> { | |||
1318 | transaction: t | 1345 | transaction: t |
1319 | } | 1346 | } |
1320 | 1347 | ||
1321 | return VideoModel.findOne(options) | 1348 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) |
1322 | } | 1349 | } |
1323 | 1350 | ||
1324 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { | 1351 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { |
1325 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) | 1352 | return VideoModel.scope([ |
1326 | .findByPk(id, { transaction: t, logging }) | 1353 | ScopeNames.WITH_FILES, |
1354 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1355 | ScopeNames.WITH_THUMBNAILS | ||
1356 | ]).findByPk(id, { transaction: t, logging }) | ||
1327 | } | 1357 | } |
1328 | 1358 | ||
1329 | static loadByUUIDWithFile (uuid: string) { | 1359 | static loadByUUIDWithFile (uuid: string) { |
@@ -1333,7 +1363,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1333 | } | 1363 | } |
1334 | } | 1364 | } |
1335 | 1365 | ||
1336 | return VideoModel.findOne(options) | 1366 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) |
1337 | } | 1367 | } |
1338 | 1368 | ||
1339 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 1369 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
@@ -1344,7 +1374,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1344 | transaction | 1374 | transaction |
1345 | } | 1375 | } |
1346 | 1376 | ||
1347 | return VideoModel.findOne(query) | 1377 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) |
1348 | } | 1378 | } |
1349 | 1379 | ||
1350 | static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { | 1380 | static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { |
@@ -1358,7 +1388,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1358 | return VideoModel.scope([ | 1388 | return VideoModel.scope([ |
1359 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1389 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1360 | ScopeNames.WITH_FILES, | 1390 | ScopeNames.WITH_FILES, |
1361 | ScopeNames.WITH_STREAMING_PLAYLISTS | 1391 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1392 | ScopeNames.WITH_THUMBNAILS | ||
1362 | ]).findOne(query) | 1393 | ]).findOne(query) |
1363 | } | 1394 | } |
1364 | 1395 | ||
@@ -1377,7 +1408,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1377 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1408 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1378 | ScopeNames.WITH_SCHEDULED_UPDATE, | 1409 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1379 | ScopeNames.WITH_FILES, | 1410 | ScopeNames.WITH_FILES, |
1380 | ScopeNames.WITH_STREAMING_PLAYLISTS | 1411 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1412 | ScopeNames.WITH_THUMBNAILS | ||
1381 | ] | 1413 | ] |
1382 | 1414 | ||
1383 | if (userId) { | 1415 | if (userId) { |
@@ -1403,6 +1435,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1403 | ScopeNames.WITH_BLACKLISTED, | 1435 | ScopeNames.WITH_BLACKLISTED, |
1404 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1436 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1405 | ScopeNames.WITH_SCHEDULED_UPDATE, | 1437 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1438 | ScopeNames.WITH_THUMBNAILS, | ||
1406 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings | 1439 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings |
1407 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings | 1440 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings |
1408 | ] | 1441 | ] |
@@ -1555,7 +1588,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1555 | } | 1588 | } |
1556 | 1589 | ||
1557 | // FIXME: typing | 1590 | // FIXME: typing |
1558 | const apiScope: any[] = [] | 1591 | const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ] |
1559 | 1592 | ||
1560 | if (options.user) { | 1593 | if (options.user) { |
1561 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) | 1594 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) |
@@ -1611,18 +1644,37 @@ export class VideoModel extends Model<VideoModel> { | |||
1611 | return maxBy(this.VideoFiles, file => file.resolution) | 1644 | return maxBy(this.VideoFiles, file => file.resolution) |
1612 | } | 1645 | } |
1613 | 1646 | ||
1647 | addThumbnail (thumbnail: ThumbnailModel) { | ||
1648 | if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = [] | ||
1649 | |||
1650 | // Already have this thumbnail, skip | ||
1651 | if (this.Thumbnails.find(t => t.id === thumbnail.id)) return | ||
1652 | |||
1653 | this.Thumbnails.push(thumbnail) | ||
1654 | } | ||
1655 | |||
1614 | getVideoFilename (videoFile: VideoFileModel) { | 1656 | getVideoFilename (videoFile: VideoFileModel) { |
1615 | return this.uuid + '-' + videoFile.resolution + videoFile.extname | 1657 | return this.uuid + '-' + videoFile.resolution + videoFile.extname |
1616 | } | 1658 | } |
1617 | 1659 | ||
1618 | getThumbnailName () { | 1660 | generateThumbnailName () { |
1619 | const extension = '.jpg' | 1661 | return this.uuid + '.jpg' |
1620 | return this.uuid + extension | ||
1621 | } | 1662 | } |
1622 | 1663 | ||
1623 | getPreviewName () { | 1664 | getThumbnail () { |
1624 | const extension = '.jpg' | 1665 | if (Array.isArray(this.Thumbnails) === false) return undefined |
1625 | return this.uuid + extension | 1666 | |
1667 | return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL) | ||
1668 | } | ||
1669 | |||
1670 | generatePreviewName () { | ||
1671 | return this.uuid + '.jpg' | ||
1672 | } | ||
1673 | |||
1674 | getPreview () { | ||
1675 | if (Array.isArray(this.Thumbnails) === false) return undefined | ||
1676 | |||
1677 | return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) | ||
1626 | } | 1678 | } |
1627 | 1679 | ||
1628 | getTorrentFileName (videoFile: VideoFileModel) { | 1680 | getTorrentFileName (videoFile: VideoFileModel) { |
@@ -1634,24 +1686,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1634 | return this.remote === false | 1686 | return this.remote === false |
1635 | } | 1687 | } |
1636 | 1688 | ||
1637 | createPreview (videoFile: VideoFileModel) { | ||
1638 | return generateImageFromVideoFile( | ||
1639 | this.getVideoFilePath(videoFile), | ||
1640 | CONFIG.STORAGE.PREVIEWS_DIR, | ||
1641 | this.getPreviewName(), | ||
1642 | PREVIEWS_SIZE | ||
1643 | ) | ||
1644 | } | ||
1645 | |||
1646 | createThumbnail (videoFile: VideoFileModel) { | ||
1647 | return generateImageFromVideoFile( | ||
1648 | this.getVideoFilePath(videoFile), | ||
1649 | CONFIG.STORAGE.THUMBNAILS_DIR, | ||
1650 | this.getThumbnailName(), | ||
1651 | THUMBNAILS_SIZE | ||
1652 | ) | ||
1653 | } | ||
1654 | |||
1655 | getTorrentFilePath (videoFile: VideoFileModel) { | 1689 | getTorrentFilePath (videoFile: VideoFileModel) { |
1656 | return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | 1690 | return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) |
1657 | } | 1691 | } |
@@ -1692,11 +1726,18 @@ export class VideoModel extends Model<VideoModel> { | |||
1692 | } | 1726 | } |
1693 | 1727 | ||
1694 | getThumbnailStaticPath () { | 1728 | getThumbnailStaticPath () { |
1695 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | 1729 | const thumbnail = this.getThumbnail() |
1730 | if (!thumbnail) return null | ||
1731 | |||
1732 | return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename) | ||
1696 | } | 1733 | } |
1697 | 1734 | ||
1698 | getPreviewStaticPath () { | 1735 | getPreviewStaticPath () { |
1699 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | 1736 | const preview = this.getPreview() |
1737 | if (!preview) return null | ||
1738 | |||
1739 | // We use a local cache, so specify our cache endpoint instead of potential remote URL | ||
1740 | return join(STATIC_PATHS.PREVIEWS, preview.filename) | ||
1700 | } | 1741 | } |
1701 | 1742 | ||
1702 | toFormattedJSON (options?: VideoFormattingJSONOptions): Video { | 1743 | toFormattedJSON (options?: VideoFormattingJSONOptions): Video { |
@@ -1732,18 +1773,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1732 | return `/api/${API_VERSION}/videos/${this.uuid}/description` | 1773 | return `/api/${API_VERSION}/videos/${this.uuid}/description` |
1733 | } | 1774 | } |
1734 | 1775 | ||
1735 | removeThumbnail () { | ||
1736 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
1737 | return remove(thumbnailPath) | ||
1738 | .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err })) | ||
1739 | } | ||
1740 | |||
1741 | removePreview () { | ||
1742 | const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | ||
1743 | return remove(previewPath) | ||
1744 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | ||
1745 | } | ||
1746 | |||
1747 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { | 1776 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1748 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR | 1777 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1749 | 1778 | ||
@@ -1816,10 +1845,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1816 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | 1845 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] |
1817 | } | 1846 | } |
1818 | 1847 | ||
1819 | getThumbnailUrl (baseUrlHttp: string) { | ||
1820 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | ||
1821 | } | ||
1822 | |||
1823 | getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1848 | getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1824 | return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) | 1849 | return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) |
1825 | } | 1850 | } |