aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r--server/models/video/video.ts112
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 {
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { 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
99const indexes: Sequelize.DefineIndexesOptions[] = [ 100const 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
141type AvailableForListIDsOptions = { 170type 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 }