diff options
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r-- | server/models/video/video.ts | 112 |
1 files changed, 89 insertions, 23 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0f18d9f0c..80a6c7832 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -94,6 +94,7 @@ import { | |||
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | 96 | import { UserModel } from '../account/user' |
97 | import { VideoImportModel } from './video-import' | ||
97 | 98 | ||
98 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 99 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
99 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 100 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -102,17 +103,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ | |||
102 | { fields: [ 'createdAt' ] }, | 103 | { fields: [ 'createdAt' ] }, |
103 | { fields: [ 'publishedAt' ] }, | 104 | { fields: [ 'publishedAt' ] }, |
104 | { fields: [ 'duration' ] }, | 105 | { fields: [ 'duration' ] }, |
105 | { fields: [ 'category' ] }, | ||
106 | { fields: [ 'licence' ] }, | ||
107 | { fields: [ 'nsfw' ] }, | ||
108 | { fields: [ 'language' ] }, | ||
109 | { fields: [ 'waitTranscoding' ] }, | ||
110 | { fields: [ 'state' ] }, | ||
111 | { fields: [ 'remote' ] }, | ||
112 | { fields: [ 'views' ] }, | 106 | { fields: [ 'views' ] }, |
113 | { fields: [ 'likes' ] }, | ||
114 | { fields: [ 'channelId' ] }, | 107 | { fields: [ 'channelId' ] }, |
115 | { | 108 | { |
109 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
110 | where: { | ||
111 | category: { | ||
112 | [Sequelize.Op.ne]: null | ||
113 | } | ||
114 | } | ||
115 | }, | ||
116 | { | ||
117 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
118 | where: { | ||
119 | licence: { | ||
120 | [Sequelize.Op.ne]: null | ||
121 | } | ||
122 | } | ||
123 | }, | ||
124 | { | ||
125 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
126 | where: { | ||
127 | language: { | ||
128 | [Sequelize.Op.ne]: null | ||
129 | } | ||
130 | } | ||
131 | }, | ||
132 | { | ||
133 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
134 | where: { | ||
135 | nsfw: true | ||
136 | } | ||
137 | }, | ||
138 | { | ||
139 | fields: [ 'remote' ], // Only index local videos | ||
140 | where: { | ||
141 | remote: false | ||
142 | } | ||
143 | }, | ||
144 | { | ||
116 | fields: [ 'uuid' ], | 145 | fields: [ 'uuid' ], |
117 | unique: true | 146 | unique: true |
118 | }, | 147 | }, |
@@ -140,7 +169,7 @@ type ForAPIOptions = { | |||
140 | 169 | ||
141 | type AvailableForListIDsOptions = { | 170 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | 171 | serverAccountId: number |
143 | actorId: number | 172 | followerActorId: number |
144 | includeLocalVideos: boolean | 173 | includeLocalVideos: boolean |
145 | filter?: VideoFilter | 174 | filter?: VideoFilter |
146 | categoryOneOf?: number[] | 175 | categoryOneOf?: number[] |
@@ -153,7 +182,8 @@ type AvailableForListIDsOptions = { | |||
153 | accountId?: number | 182 | accountId?: number |
154 | videoChannelId?: number | 183 | videoChannelId?: number |
155 | trendingDays?: number | 184 | trendingDays?: number |
156 | user?: UserModel | 185 | user?: UserModel, |
186 | historyOfUser?: UserModel | ||
157 | } | 187 | } |
158 | 188 | ||
159 | @Scopes({ | 189 | @Scopes({ |
@@ -315,7 +345,7 @@ type AvailableForListIDsOptions = { | |||
315 | query.include.push(videoChannelInclude) | 345 | query.include.push(videoChannelInclude) |
316 | } | 346 | } |
317 | 347 | ||
318 | if (options.actorId) { | 348 | if (options.followerActorId) { |
319 | let localVideosReq = '' | 349 | let localVideosReq = '' |
320 | if (options.includeLocalVideos === true) { | 350 | if (options.includeLocalVideos === true) { |
321 | localVideosReq = ' UNION ALL ' + | 351 | localVideosReq = ' UNION ALL ' + |
@@ -327,7 +357,7 @@ type AvailableForListIDsOptions = { | |||
327 | } | 357 | } |
328 | 358 | ||
329 | // Force actorId to be a number to avoid SQL injections | 359 | // Force actorId to be a number to avoid SQL injections |
330 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 360 | const actorIdNumber = parseInt(options.followerActorId.toString(), 10) |
331 | query.where[ 'id' ][ Sequelize.Op.and ].push({ | 361 | query.where[ 'id' ][ Sequelize.Op.and ].push({ |
332 | [ Sequelize.Op.in ]: Sequelize.literal( | 362 | [ Sequelize.Op.in ]: Sequelize.literal( |
333 | '(' + | 363 | '(' + |
@@ -416,6 +446,21 @@ type AvailableForListIDsOptions = { | |||
416 | query.subQuery = false | 446 | query.subQuery = false |
417 | } | 447 | } |
418 | 448 | ||
449 | if (options.historyOfUser) { | ||
450 | query.include.push({ | ||
451 | model: UserVideoHistoryModel, | ||
452 | required: true, | ||
453 | where: { | ||
454 | userId: options.historyOfUser.id | ||
455 | } | ||
456 | }) | ||
457 | |||
458 | // Even if the relation is n:m, we know that a user only have 0..1 video history | ||
459 | // So we won't have multiple rows for the same video | ||
460 | // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel | ||
461 | query.subQuery = false | ||
462 | } | ||
463 | |||
419 | return query | 464 | return query |
420 | }, | 465 | }, |
421 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 466 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
@@ -741,6 +786,15 @@ export class VideoModel extends Model<VideoModel> { | |||
741 | }) | 786 | }) |
742 | VideoBlacklist: VideoBlacklistModel | 787 | VideoBlacklist: VideoBlacklistModel |
743 | 788 | ||
789 | @HasOne(() => VideoImportModel, { | ||
790 | foreignKey: { | ||
791 | name: 'videoId', | ||
792 | allowNull: true | ||
793 | }, | ||
794 | onDelete: 'set null' | ||
795 | }) | ||
796 | VideoImport: VideoImportModel | ||
797 | |||
744 | @HasMany(() => VideoCaptionModel, { | 798 | @HasMany(() => VideoCaptionModel, { |
745 | foreignKey: { | 799 | foreignKey: { |
746 | name: 'videoId', | 800 | name: 'videoId', |
@@ -985,9 +1039,10 @@ export class VideoModel extends Model<VideoModel> { | |||
985 | filter?: VideoFilter, | 1039 | filter?: VideoFilter, |
986 | accountId?: number, | 1040 | accountId?: number, |
987 | videoChannelId?: number, | 1041 | videoChannelId?: number, |
988 | actorId?: number | 1042 | followerActorId?: number |
989 | trendingDays?: number, | 1043 | trendingDays?: number, |
990 | user?: UserModel | 1044 | user?: UserModel, |
1045 | historyOfUser?: UserModel | ||
991 | }, countVideos = true) { | 1046 | }, countVideos = true) { |
992 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1047 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
993 | throw new Error('Try to filter all-local but no user has not the see all videos right') | 1048 | throw new Error('Try to filter all-local but no user has not the see all videos right') |
@@ -1008,11 +1063,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1008 | 1063 | ||
1009 | const serverActor = await getServerActor() | 1064 | const serverActor = await getServerActor() |
1010 | 1065 | ||
1011 | // actorId === null has a meaning, so just check undefined | 1066 | // followerActorId === null has a meaning, so just check undefined |
1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id | 1067 | const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id |
1013 | 1068 | ||
1014 | const queryOptions = { | 1069 | const queryOptions = { |
1015 | actorId, | 1070 | followerActorId, |
1016 | serverAccountId: serverActor.Account.id, | 1071 | serverAccountId: serverActor.Account.id, |
1017 | nsfw: options.nsfw, | 1072 | nsfw: options.nsfw, |
1018 | categoryOneOf: options.categoryOneOf, | 1073 | categoryOneOf: options.categoryOneOf, |
@@ -1026,6 +1081,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1026 | videoChannelId: options.videoChannelId, | 1081 | videoChannelId: options.videoChannelId, |
1027 | includeLocalVideos: options.includeLocalVideos, | 1082 | includeLocalVideos: options.includeLocalVideos, |
1028 | user: options.user, | 1083 | user: options.user, |
1084 | historyOfUser: options.historyOfUser, | ||
1029 | trendingDays | 1085 | trendingDays |
1030 | } | 1086 | } |
1031 | 1087 | ||
@@ -1118,7 +1174,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1118 | 1174 | ||
1119 | const serverActor = await getServerActor() | 1175 | const serverActor = await getServerActor() |
1120 | const queryOptions = { | 1176 | const queryOptions = { |
1121 | actorId: serverActor.id, | 1177 | followerActorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | 1178 | serverAccountId: serverActor.Account.id, |
1123 | includeLocalVideos: options.includeLocalVideos, | 1179 | includeLocalVideos: options.includeLocalVideos, |
1124 | nsfw: options.nsfw, | 1180 | nsfw: options.nsfw, |
@@ -1273,11 +1329,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1273 | // threshold corresponds to how many video the field should have to be returned | 1329 | // threshold corresponds to how many video the field should have to be returned |
1274 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1330 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1275 | const serverActor = await getServerActor() | 1331 | const serverActor = await getServerActor() |
1276 | const actorId = serverActor.id | 1332 | const followerActorId = serverActor.id |
1277 | 1333 | ||
1278 | const scopeOptions: AvailableForListIDsOptions = { | 1334 | const scopeOptions: AvailableForListIDsOptions = { |
1279 | serverAccountId: serverActor.Account.id, | 1335 | serverAccountId: serverActor.Account.id, |
1280 | actorId, | 1336 | followerActorId, |
1281 | includeLocalVideos: true | 1337 | includeLocalVideos: true |
1282 | } | 1338 | } |
1283 | 1339 | ||
@@ -1341,7 +1397,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1341 | } | 1397 | } |
1342 | 1398 | ||
1343 | const [ count, rowsId ] = await Promise.all([ | 1399 | const [ count, rowsId ] = await Promise.all([ |
1344 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), | 1400 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined), |
1345 | VideoModel.scope(idsScope).findAll(query) | 1401 | VideoModel.scope(idsScope).findAll(query) |
1346 | ]) | 1402 | ]) |
1347 | const ids = rowsId.map(r => r.id) | 1403 | const ids = rowsId.map(r => r.id) |
@@ -1481,6 +1537,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1481 | videoFile.infoHash = parsedTorrent.infoHash | 1537 | videoFile.infoHash = parsedTorrent.infoHash |
1482 | } | 1538 | } |
1483 | 1539 | ||
1540 | getWatchStaticPath () { | ||
1541 | return '/videos/watch/' + this.uuid | ||
1542 | } | ||
1543 | |||
1484 | getEmbedStaticPath () { | 1544 | getEmbedStaticPath () { |
1485 | return '/videos/embed/' + this.uuid | 1545 | return '/videos/embed/' + this.uuid |
1486 | } | 1546 | } |
@@ -1538,8 +1598,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1538 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | 1598 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) |
1539 | } | 1599 | } |
1540 | 1600 | ||
1541 | removeFile (videoFile: VideoFileModel) { | 1601 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1542 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1602 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1603 | |||
1604 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | ||
1543 | return remove(filePath) | 1605 | return remove(filePath) |
1544 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1606 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1545 | } | 1607 | } |
@@ -1617,6 +1679,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1617 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 1679 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1618 | } | 1680 | } |
1619 | 1681 | ||
1682 | getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | ||
1683 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | ||
1684 | } | ||
1685 | |||
1620 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1686 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1621 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1687 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1622 | } | 1688 | } |