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.ts372
1 files changed, 310 insertions, 62 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index a9baaf1da..0feeed4f8 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -27,7 +27,7 @@ import {
27 Table, 27 Table,
28 UpdatedAt 28 UpdatedAt
29} from 'sequelize-typescript' 29} from 'sequelize-typescript'
30import { VideoPrivacy, VideoState } from '../../../shared' 30import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
33import { VideoFilter } from '../../../shared/models/videos/video-query.type' 33import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -52,7 +52,7 @@ import {
52 ACTIVITY_PUB, 52 ACTIVITY_PUB,
53 API_VERSION, 53 API_VERSION,
54 CONFIG, 54 CONFIG,
55 CONSTRAINTS_FIELDS, 55 CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY,
56 PREVIEWS_SIZE, 56 PREVIEWS_SIZE,
57 REMOTE_SCHEME, 57 REMOTE_SCHEME,
58 STATIC_DOWNLOAD_PATHS, 58 STATIC_DOWNLOAD_PATHS,
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
70import { ActorModel } from '../activitypub/actor' 70import { ActorModel } from '../activitypub/actor'
71import { AvatarModel } from '../avatar/avatar' 71import { AvatarModel } from '../avatar/avatar'
72import { ServerModel } from '../server/server' 72import { ServerModel } from '../server/server'
73import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' 73import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
74import { TagModel } from './tag' 74import { TagModel } from './tag'
75import { VideoAbuseModel } from './video-abuse' 75import { VideoAbuseModel } from './video-abuse'
76import { VideoChannelModel } from './video-channel' 76import { VideoChannelModel } from './video-channel'
@@ -93,6 +93,9 @@ import {
93} from './video-format-utils' 93} from './video-format-utils'
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'
97import { VideoImportModel } from './video-import'
98import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
96 99
97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 100// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
98const indexes: Sequelize.DefineIndexesOptions[] = [ 101const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -101,17 +104,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [
101 { fields: [ 'createdAt' ] }, 104 { fields: [ 'createdAt' ] },
102 { fields: [ 'publishedAt' ] }, 105 { fields: [ 'publishedAt' ] },
103 { fields: [ 'duration' ] }, 106 { fields: [ 'duration' ] },
104 { fields: [ 'category' ] },
105 { fields: [ 'licence' ] },
106 { fields: [ 'nsfw' ] },
107 { fields: [ 'language' ] },
108 { fields: [ 'waitTranscoding' ] },
109 { fields: [ 'state' ] },
110 { fields: [ 'remote' ] },
111 { fields: [ 'views' ] }, 107 { fields: [ 'views' ] },
112 { fields: [ 'likes' ] },
113 { fields: [ 'channelId' ] }, 108 { fields: [ 'channelId' ] },
114 { 109 {
110 fields: [ 'category' ], // We don't care videos with an unknown category
111 where: {
112 category: {
113 [Sequelize.Op.ne]: null
114 }
115 }
116 },
117 {
118 fields: [ 'licence' ], // We don't care videos with an unknown licence
119 where: {
120 licence: {
121 [Sequelize.Op.ne]: null
122 }
123 }
124 },
125 {
126 fields: [ 'language' ], // We don't care videos with an unknown language
127 where: {
128 language: {
129 [Sequelize.Op.ne]: null
130 }
131 }
132 },
133 {
134 fields: [ 'nsfw' ], // Most of the videos are not NSFW
135 where: {
136 nsfw: true
137 }
138 },
139 {
140 fields: [ 'remote' ], // Only index local videos
141 where: {
142 remote: false
143 }
144 },
145 {
115 fields: [ 'uuid' ], 146 fields: [ 'uuid' ],
116 unique: true 147 unique: true
117 }, 148 },
@@ -129,7 +160,9 @@ export enum ScopeNames {
129 WITH_FILES = 'WITH_FILES', 160 WITH_FILES = 'WITH_FILES',
130 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 161 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
131 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 162 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
132 WITH_USER_HISTORY = 'WITH_USER_HISTORY' 163 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
164 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
165 WITH_USER_ID = 'WITH_USER_ID'
133} 166}
134 167
135type ForAPIOptions = { 168type ForAPIOptions = {
@@ -138,7 +171,8 @@ type ForAPIOptions = {
138} 171}
139 172
140type AvailableForListIDsOptions = { 173type AvailableForListIDsOptions = {
141 actorId: number 174 serverAccountId: number
175 followerActorId: number
142 includeLocalVideos: boolean 176 includeLocalVideos: boolean
143 filter?: VideoFilter 177 filter?: VideoFilter
144 categoryOneOf?: number[] 178 categoryOneOf?: number[]
@@ -151,6 +185,8 @@ type AvailableForListIDsOptions = {
151 accountId?: number 185 accountId?: number
152 videoChannelId?: number 186 videoChannelId?: number
153 trendingDays?: number 187 trendingDays?: number
188 user?: UserModel,
189 historyOfUser?: UserModel
154} 190}
155 191
156@Scopes({ 192@Scopes({
@@ -236,6 +272,22 @@ type AvailableForListIDsOptions = {
236 } 272 }
237 ] 273 ]
238 }, 274 },
275 channelId: {
276 [ Sequelize.Op.notIn ]: Sequelize.literal(
277 '(' +
278 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
279 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
280 ')' +
281 ')'
282 )
283 }
284 },
285 include: []
286 }
287
288 // Only list public/published videos
289 if (!options.filter || options.filter !== 'all-local') {
290 const privacyWhere = {
239 // Always list public videos 291 // Always list public videos
240 privacy: VideoPrivacy.PUBLIC, 292 privacy: VideoPrivacy.PUBLIC,
241 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 293 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@@ -250,8 +302,9 @@ type AvailableForListIDsOptions = {
250 } 302 }
251 } 303 }
252 ] 304 ]
253 }, 305 }
254 include: [] 306
307 Object.assign(query.where, privacyWhere)
255 } 308 }
256 309
257 if (options.filter || options.accountId || options.videoChannelId) { 310 if (options.filter || options.accountId || options.videoChannelId) {
@@ -295,7 +348,7 @@ type AvailableForListIDsOptions = {
295 query.include.push(videoChannelInclude) 348 query.include.push(videoChannelInclude)
296 } 349 }
297 350
298 if (options.actorId) { 351 if (options.followerActorId) {
299 let localVideosReq = '' 352 let localVideosReq = ''
300 if (options.includeLocalVideos === true) { 353 if (options.includeLocalVideos === true) {
301 localVideosReq = ' UNION ALL ' + 354 localVideosReq = ' UNION ALL ' +
@@ -307,7 +360,7 @@ type AvailableForListIDsOptions = {
307 } 360 }
308 361
309 // Force actorId to be a number to avoid SQL injections 362 // Force actorId to be a number to avoid SQL injections
310 const actorIdNumber = parseInt(options.actorId.toString(), 10) 363 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
311 query.where[ 'id' ][ Sequelize.Op.and ].push({ 364 query.where[ 'id' ][ Sequelize.Op.and ].push({
312 [ Sequelize.Op.in ]: Sequelize.literal( 365 [ Sequelize.Op.in ]: Sequelize.literal(
313 '(' + 366 '(' +
@@ -396,8 +449,39 @@ type AvailableForListIDsOptions = {
396 query.subQuery = false 449 query.subQuery = false
397 } 450 }
398 451
452 if (options.historyOfUser) {
453 query.include.push({
454 model: UserVideoHistoryModel,
455 required: true,
456 where: {
457 userId: options.historyOfUser.id
458 }
459 })
460
461 // Even if the relation is n:m, we know that a user only have 0..1 video history
462 // So we won't have multiple rows for the same video
463 // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel
464 query.subQuery = false
465 }
466
399 return query 467 return query
400 }, 468 },
469 [ ScopeNames.WITH_USER_ID ]: {
470 include: [
471 {
472 attributes: [ 'accountId' ],
473 model: () => VideoChannelModel.unscoped(),
474 required: true,
475 include: [
476 {
477 attributes: [ 'userId' ],
478 model: () => AccountModel.unscoped(),
479 required: true
480 }
481 ]
482 }
483 ]
484 },
401 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 485 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
402 include: [ 486 include: [
403 { 487 {
@@ -462,22 +546,55 @@ type AvailableForListIDsOptions = {
462 } 546 }
463 ] 547 ]
464 }, 548 },
465 [ ScopeNames.WITH_FILES ]: { 549 [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => {
466 include: [ 550 let subInclude: any[] = []
467 { 551
468 model: () => VideoFileModel.unscoped(), 552 if (withRedundancies === true) {
469 // FIXME: typings 553 subInclude = [
470 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join 554 {
471 required: false, 555 attributes: [ 'fileUrl' ],
472 include: [ 556 model: VideoRedundancyModel.unscoped(),
473 { 557 required: false
474 attributes: [ 'fileUrl' ], 558 }
475 model: () => VideoRedundancyModel.unscoped(), 559 ]
476 required: false 560 }
477 } 561
478 ] 562 return {
479 } 563 include: [
480 ] 564 {
565 model: VideoFileModel.unscoped(),
566 // FIXME: typings
567 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
568 required: false,
569 include: subInclude
570 }
571 ]
572 }
573 },
574 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
575 let subInclude: any[] = []
576
577 if (withRedundancies === true) {
578 subInclude = [
579 {
580 attributes: [ 'fileUrl' ],
581 model: VideoRedundancyModel.unscoped(),
582 required: false
583 }
584 ]
585 }
586
587 return {
588 include: [
589 {
590 model: VideoStreamingPlaylistModel.unscoped(),
591 // FIXME: typings
592 [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
593 required: false,
594 include: subInclude
595 }
596 ]
597 }
481 }, 598 },
482 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 599 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
483 include: [ 600 include: [
@@ -661,6 +778,16 @@ export class VideoModel extends Model<VideoModel> {
661 }) 778 })
662 VideoFiles: VideoFileModel[] 779 VideoFiles: VideoFileModel[]
663 780
781 @HasMany(() => VideoStreamingPlaylistModel, {
782 foreignKey: {
783 name: 'videoId',
784 allowNull: false
785 },
786 hooks: true,
787 onDelete: 'cascade'
788 })
789 VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
790
664 @HasMany(() => VideoShareModel, { 791 @HasMany(() => VideoShareModel, {
665 foreignKey: { 792 foreignKey: {
666 name: 'videoId', 793 name: 'videoId',
@@ -725,6 +852,15 @@ export class VideoModel extends Model<VideoModel> {
725 }) 852 })
726 VideoBlacklist: VideoBlacklistModel 853 VideoBlacklist: VideoBlacklistModel
727 854
855 @HasOne(() => VideoImportModel, {
856 foreignKey: {
857 name: 'videoId',
858 allowNull: true
859 },
860 onDelete: 'set null'
861 })
862 VideoImport: VideoImportModel
863
728 @HasMany(() => VideoCaptionModel, { 864 @HasMany(() => VideoCaptionModel, {
729 foreignKey: { 865 foreignKey: {
730 name: 'videoId', 866 name: 'videoId',
@@ -777,6 +913,9 @@ export class VideoModel extends Model<VideoModel> {
777 tasks.push(instance.removeFile(file)) 913 tasks.push(instance.removeFile(file))
778 tasks.push(instance.removeTorrent(file)) 914 tasks.push(instance.removeTorrent(file))
779 }) 915 })
916
917 // Remove playlists file
918 tasks.push(instance.removeStreamingPlaylist())
780 } 919 }
781 920
782 // Do not wait video deletion because we could be in a transaction 921 // Do not wait video deletion because we could be in a transaction
@@ -788,8 +927,14 @@ export class VideoModel extends Model<VideoModel> {
788 return undefined 927 return undefined
789 } 928 }
790 929
791 static list () { 930 static listLocal () {
792 return VideoModel.scope(ScopeNames.WITH_FILES).findAll() 931 const query = {
932 where: {
933 remote: false
934 }
935 }
936
937 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query)
793 } 938 }
794 939
795 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 940 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -959,10 +1104,15 @@ export class VideoModel extends Model<VideoModel> {
959 filter?: VideoFilter, 1104 filter?: VideoFilter,
960 accountId?: number, 1105 accountId?: number,
961 videoChannelId?: number, 1106 videoChannelId?: number,
962 actorId?: number 1107 followerActorId?: number
963 trendingDays?: number, 1108 trendingDays?: number,
964 userId?: number 1109 user?: UserModel,
1110 historyOfUser?: UserModel
965 }, countVideos = true) { 1111 }, countVideos = true) {
1112 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1113 throw new Error('Try to filter all-local but no user has not the see all videos right')
1114 }
1115
966 const query: IFindOptions<VideoModel> = { 1116 const query: IFindOptions<VideoModel> = {
967 offset: options.start, 1117 offset: options.start,
968 limit: options.count, 1118 limit: options.count,
@@ -976,11 +1126,14 @@ export class VideoModel extends Model<VideoModel> {
976 query.group = 'VideoModel.id' 1126 query.group = 'VideoModel.id'
977 } 1127 }
978 1128
979 // actorId === null has a meaning, so just check undefined 1129 const serverActor = await getServerActor()
980 const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id 1130
1131 // followerActorId === null has a meaning, so just check undefined
1132 const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id
981 1133
982 const queryOptions = { 1134 const queryOptions = {
983 actorId, 1135 followerActorId,
1136 serverAccountId: serverActor.Account.id,
984 nsfw: options.nsfw, 1137 nsfw: options.nsfw,
985 categoryOneOf: options.categoryOneOf, 1138 categoryOneOf: options.categoryOneOf,
986 licenceOneOf: options.licenceOneOf, 1139 licenceOneOf: options.licenceOneOf,
@@ -992,7 +1145,8 @@ export class VideoModel extends Model<VideoModel> {
992 accountId: options.accountId, 1145 accountId: options.accountId,
993 videoChannelId: options.videoChannelId, 1146 videoChannelId: options.videoChannelId,
994 includeLocalVideos: options.includeLocalVideos, 1147 includeLocalVideos: options.includeLocalVideos,
995 userId: options.userId, 1148 user: options.user,
1149 historyOfUser: options.historyOfUser,
996 trendingDays 1150 trendingDays
997 } 1151 }
998 1152
@@ -1015,7 +1169,8 @@ export class VideoModel extends Model<VideoModel> {
1015 tagsAllOf?: string[] 1169 tagsAllOf?: string[]
1016 durationMin?: number // seconds 1170 durationMin?: number // seconds
1017 durationMax?: number // seconds 1171 durationMax?: number // seconds
1018 userId?: number 1172 user?: UserModel,
1173 filter?: VideoFilter
1019 }) { 1174 }) {
1020 const whereAnd = [] 1175 const whereAnd = []
1021 1176
@@ -1084,7 +1239,8 @@ export class VideoModel extends Model<VideoModel> {
1084 1239
1085 const serverActor = await getServerActor() 1240 const serverActor = await getServerActor()
1086 const queryOptions = { 1241 const queryOptions = {
1087 actorId: serverActor.id, 1242 followerActorId: serverActor.id,
1243 serverAccountId: serverActor.Account.id,
1088 includeLocalVideos: options.includeLocalVideos, 1244 includeLocalVideos: options.includeLocalVideos,
1089 nsfw: options.nsfw, 1245 nsfw: options.nsfw,
1090 categoryOneOf: options.categoryOneOf, 1246 categoryOneOf: options.categoryOneOf,
@@ -1092,7 +1248,8 @@ export class VideoModel extends Model<VideoModel> {
1092 languageOneOf: options.languageOneOf, 1248 languageOneOf: options.languageOneOf,
1093 tagsOneOf: options.tagsOneOf, 1249 tagsOneOf: options.tagsOneOf,
1094 tagsAllOf: options.tagsAllOf, 1250 tagsAllOf: options.tagsAllOf,
1095 userId: options.userId 1251 user: options.user,
1252 filter: options.filter
1096 } 1253 }
1097 1254
1098 return VideoModel.getAvailableForApi(query, queryOptions) 1255 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1108,6 +1265,16 @@ export class VideoModel extends Model<VideoModel> {
1108 return VideoModel.findOne(options) 1265 return VideoModel.findOne(options)
1109 } 1266 }
1110 1267
1268 static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
1269 const where = VideoModel.buildWhereIdOrUUID(id)
1270 const options = {
1271 where,
1272 transaction: t
1273 }
1274
1275 return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options)
1276 }
1277
1111 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { 1278 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
1112 const where = VideoModel.buildWhereIdOrUUID(id) 1279 const where = VideoModel.buildWhereIdOrUUID(id)
1113 1280
@@ -1120,8 +1287,8 @@ export class VideoModel extends Model<VideoModel> {
1120 return VideoModel.findOne(options) 1287 return VideoModel.findOne(options)
1121 } 1288 }
1122 1289
1123 static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { 1290 static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
1124 return VideoModel.scope(ScopeNames.WITH_FILES) 1291 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ])
1125 .findById(id, { transaction: t, logging }) 1292 .findById(id, { transaction: t, logging })
1126 } 1293 }
1127 1294
@@ -1132,9 +1299,7 @@ export class VideoModel extends Model<VideoModel> {
1132 } 1299 }
1133 } 1300 }
1134 1301
1135 return VideoModel 1302 return VideoModel.findOne(options)
1136 .scope([ ScopeNames.WITH_FILES ])
1137 .findOne(options)
1138 } 1303 }
1139 1304
1140 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 1305 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
@@ -1156,7 +1321,11 @@ export class VideoModel extends Model<VideoModel> {
1156 transaction 1321 transaction
1157 } 1322 }
1158 1323
1159 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1324 return VideoModel.scope([
1325 ScopeNames.WITH_ACCOUNT_DETAILS,
1326 ScopeNames.WITH_FILES,
1327 ScopeNames.WITH_STREAMING_PLAYLISTS
1328 ]).findOne(query)
1160 } 1329 }
1161 1330
1162 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { 1331 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
@@ -1171,9 +1340,37 @@ export class VideoModel extends Model<VideoModel> {
1171 const scopes = [ 1340 const scopes = [
1172 ScopeNames.WITH_TAGS, 1341 ScopeNames.WITH_TAGS,
1173 ScopeNames.WITH_BLACKLISTED, 1342 ScopeNames.WITH_BLACKLISTED,
1343 ScopeNames.WITH_ACCOUNT_DETAILS,
1344 ScopeNames.WITH_SCHEDULED_UPDATE,
1174 ScopeNames.WITH_FILES, 1345 ScopeNames.WITH_FILES,
1346 ScopeNames.WITH_STREAMING_PLAYLISTS
1347 ]
1348
1349 if (userId) {
1350 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1351 }
1352
1353 return VideoModel
1354 .scope(scopes)
1355 .findOne(options)
1356 }
1357
1358 static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1359 const where = VideoModel.buildWhereIdOrUUID(id)
1360
1361 const options = {
1362 order: [ [ 'Tags', 'name', 'ASC' ] ],
1363 where,
1364 transaction: t
1365 }
1366
1367 const scopes = [
1368 ScopeNames.WITH_TAGS,
1369 ScopeNames.WITH_BLACKLISTED,
1175 ScopeNames.WITH_ACCOUNT_DETAILS, 1370 ScopeNames.WITH_ACCOUNT_DETAILS,
1176 ScopeNames.WITH_SCHEDULED_UPDATE 1371 ScopeNames.WITH_SCHEDULED_UPDATE,
1372 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
1373 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
1177 ] 1374 ]
1178 1375
1179 if (userId) { 1376 if (userId) {
@@ -1217,12 +1414,31 @@ export class VideoModel extends Model<VideoModel> {
1217 }) 1414 })
1218 } 1415 }
1219 1416
1417 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1418 // Instances only share videos
1419 const query = 'SELECT 1 FROM "videoShare" ' +
1420 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1421 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1422 'LIMIT 1'
1423
1424 const options = {
1425 type: Sequelize.QueryTypes.SELECT,
1426 bind: { followerActorId, videoId },
1427 raw: true
1428 }
1429
1430 return VideoModel.sequelize.query(query, options)
1431 .then(results => results.length === 1)
1432 }
1433
1220 // threshold corresponds to how many video the field should have to be returned 1434 // threshold corresponds to how many video the field should have to be returned
1221 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1435 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1222 const actorId = (await getServerActor()).id 1436 const serverActor = await getServerActor()
1437 const followerActorId = serverActor.id
1223 1438
1224 const scopeOptions = { 1439 const scopeOptions: AvailableForListIDsOptions = {
1225 actorId, 1440 serverAccountId: serverActor.Account.id,
1441 followerActorId,
1226 includeLocalVideos: true 1442 includeLocalVideos: true
1227 } 1443 }
1228 1444
@@ -1256,7 +1472,7 @@ export class VideoModel extends Model<VideoModel> {
1256 } 1472 }
1257 1473
1258 private static buildActorWhereWithFilter (filter?: VideoFilter) { 1474 private static buildActorWhereWithFilter (filter?: VideoFilter) {
1259 if (filter && filter === 'local') { 1475 if (filter && (filter === 'local' || filter === 'all-local')) {
1260 return { 1476 return {
1261 serverId: null 1477 serverId: null
1262 } 1478 }
@@ -1267,7 +1483,7 @@ export class VideoModel extends Model<VideoModel> {
1267 1483
1268 private static async getAvailableForApi ( 1484 private static async getAvailableForApi (
1269 query: IFindOptions<VideoModel>, 1485 query: IFindOptions<VideoModel>,
1270 options: AvailableForListIDsOptions & { userId?: number}, 1486 options: AvailableForListIDsOptions,
1271 countVideos = true 1487 countVideos = true
1272 ) { 1488 ) {
1273 const idsScope = { 1489 const idsScope = {
@@ -1286,7 +1502,7 @@ export class VideoModel extends Model<VideoModel> {
1286 } 1502 }
1287 1503
1288 const [ count, rowsId ] = await Promise.all([ 1504 const [ count, rowsId ] = await Promise.all([
1289 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), 1505 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined),
1290 VideoModel.scope(idsScope).findAll(query) 1506 VideoModel.scope(idsScope).findAll(query)
1291 ]) 1507 ])
1292 const ids = rowsId.map(r => r.id) 1508 const ids = rowsId.map(r => r.id)
@@ -1300,8 +1516,8 @@ export class VideoModel extends Model<VideoModel> {
1300 } 1516 }
1301 ] 1517 ]
1302 1518
1303 if (options.userId) { 1519 if (options.user) {
1304 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] }) 1520 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1305 } 1521 }
1306 1522
1307 const secondQuery = { 1523 const secondQuery = {
@@ -1426,6 +1642,10 @@ export class VideoModel extends Model<VideoModel> {
1426 videoFile.infoHash = parsedTorrent.infoHash 1642 videoFile.infoHash = parsedTorrent.infoHash
1427 } 1643 }
1428 1644
1645 getWatchStaticPath () {
1646 return '/videos/watch/' + this.uuid
1647 }
1648
1429 getEmbedStaticPath () { 1649 getEmbedStaticPath () {
1430 return '/videos/embed/' + this.uuid 1650 return '/videos/embed/' + this.uuid
1431 } 1651 }
@@ -1483,8 +1703,10 @@ export class VideoModel extends Model<VideoModel> {
1483 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) 1703 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
1484 } 1704 }
1485 1705
1486 removeFile (videoFile: VideoFileModel) { 1706 removeFile (videoFile: VideoFileModel, isRedundancy = false) {
1487 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 1707 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1708
1709 const filePath = join(baseDir, this.getVideoFilename(videoFile))
1488 return remove(filePath) 1710 return remove(filePath)
1489 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1711 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1490 } 1712 }
@@ -1495,6 +1717,14 @@ export class VideoModel extends Model<VideoModel> {
1495 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1717 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1496 } 1718 }
1497 1719
1720 removeStreamingPlaylist (isRedundancy = false) {
1721 const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY
1722
1723 const filePath = join(baseDir, this.uuid)
1724 return remove(filePath)
1725 .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err }))
1726 }
1727
1498 isOutdated () { 1728 isOutdated () {
1499 if (this.isOwned()) return false 1729 if (this.isOwned()) return false
1500 1730
@@ -1506,6 +1736,12 @@ export class VideoModel extends Model<VideoModel> {
1506 (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL 1736 (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL
1507 } 1737 }
1508 1738
1739 setAsRefreshed () {
1740 this.changed('updatedAt', true)
1741
1742 return this.save()
1743 }
1744
1509 getBaseUrls () { 1745 getBaseUrls () {
1510 let baseUrlHttp 1746 let baseUrlHttp
1511 let baseUrlWs 1747 let baseUrlWs
@@ -1523,7 +1759,7 @@ export class VideoModel extends Model<VideoModel> {
1523 1759
1524 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { 1760 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
1525 const xs = this.getTorrentUrl(videoFile, baseUrlHttp) 1761 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1526 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 1762 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
1527 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] 1763 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
1528 1764
1529 const redundancies = videoFile.RedundancyVideos 1765 const redundancies = videoFile.RedundancyVideos
@@ -1540,6 +1776,10 @@ export class VideoModel extends Model<VideoModel> {
1540 return magnetUtil.encode(magnetHash) 1776 return magnetUtil.encode(magnetHash)
1541 } 1777 }
1542 1778
1779 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1780 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1781 }
1782
1543 getThumbnailUrl (baseUrlHttp: string) { 1783 getThumbnailUrl (baseUrlHttp: string) {
1544 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() 1784 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
1545 } 1785 }
@@ -1556,7 +1796,15 @@ export class VideoModel extends Model<VideoModel> {
1556 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) 1796 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1557 } 1797 }
1558 1798
1799 getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1800 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
1801 }
1802
1559 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1803 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1560 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1804 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1561 } 1805 }
1806
1807 getBandwidthBits (videoFile: VideoFileModel) {
1808 return Math.ceil((videoFile.size * 8) / this.duration)
1809 }
1562} 1810}