aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-04-17 10:07:00 +0200
committerChocobozzz <me@florianbigard.com>2019-04-24 16:25:52 +0200
commite8bafea35bc930cb8ac5b2d521a188642a1adffe (patch)
tree7537f957ed7307b464e3c90b71b813d992acaade /server/models
parent94565d52bb2883e09f16d1363170ac9c0dccb7a1 (diff)
downloadPeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.gz
PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.zst
PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.zip
Create a dedicated table to track video thumbnails
Diffstat (limited to 'server/models')
-rw-r--r--server/models/video/thumbnail.ts116
-rw-r--r--server/models/video/video-format-utils.ts8
-rw-r--r--server/models/video/video-playlist.ts85
-rw-r--r--server/models/video/video.ts155
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 @@
1import { join } from 'path'
2import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
4import { logger } from '../../helpers/logger'
5import { remove } from 'fs-extra'
6import { CONFIG } from '../../initializers/config'
7import { VideoModel } from './video'
8import { VideoPlaylistModel } from './video-playlist'
9import { 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})
23export 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'
10import { MIMETYPES, THUMBNAILS_SIZE, WEBSERVER } from '../../initializers/constants' 10import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
11import { VideoCaptionModel } from './video-caption' 11import { VideoCaptionModel } from './video-caption'
12import { 12import {
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 @@
1import { 1import {
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'
40import { VideoPlaylistElementModel } from './video-playlist-element' 40import { VideoPlaylistElementModel } from './video-playlist-element'
41import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 41import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
42import { activityPubCollectionPagination } from '../../helpers/activitypub' 42import { activityPubCollectionPagination } from '../../helpers/activitypub'
43import { remove } from 'fs-extra'
44import { logger } from '../../helpers/logger'
45import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' 43import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
46import { CONFIG } from '../../initializers/config' 44import { ThumbnailModel } from './thumbnail'
45import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
47 46
48enum ScopeNames { 47enum 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'
107import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 107import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
108import { VideoPlaylistElementModel } from './video-playlist-element' 108import { VideoPlaylistElementModel } from './video-playlist-element'
109import { CONFIG } from '../../initializers/config' 109import { CONFIG } from '../../initializers/config'
110import { ThumbnailModel } from './thumbnail'
111import { 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
112const indexes: Sequelize.DefineIndexesOptions[] = [ 114const 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
187type ForAPIOptions = { 190type 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 }