diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account.ts | 8 | ||||
-rw-r--r-- | server/models/actor/actor-follow.ts | 36 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 4 | ||||
-rw-r--r-- | server/models/shared/index.ts | 2 | ||||
-rw-r--r-- | server/models/shared/query.ts | 17 | ||||
-rw-r--r-- | server/models/shared/update.ts | 18 | ||||
-rw-r--r-- | server/models/user/user-notification.ts | 3 | ||||
-rw-r--r-- | server/models/video/formatter/video-format-utils.ts | 8 | ||||
-rw-r--r-- | server/models/video/sql/shared/video-tables.ts | 3 | ||||
-rw-r--r-- | server/models/video/sql/videos-id-list-query-builder.ts | 35 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 165 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 45 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 64 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 85 | ||||
-rw-r--r-- | server/models/video/video.ts | 131 |
15 files changed, 402 insertions, 222 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 665ecd595..37194a119 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -52,6 +52,7 @@ export enum ScopeNames { | |||
52 | export type SummaryOptions = { | 52 | export type SummaryOptions = { |
53 | actorRequired?: boolean // Default: true | 53 | actorRequired?: boolean // Default: true |
54 | whereActor?: WhereOptions | 54 | whereActor?: WhereOptions |
55 | whereServer?: WhereOptions | ||
55 | withAccountBlockerIds?: number[] | 56 | withAccountBlockerIds?: number[] |
56 | } | 57 | } |
57 | 58 | ||
@@ -65,12 +66,11 @@ export type SummaryOptions = { | |||
65 | })) | 66 | })) |
66 | @Scopes(() => ({ | 67 | @Scopes(() => ({ |
67 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | 68 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
68 | const whereActor = options.whereActor || undefined | ||
69 | |||
70 | const serverInclude: IncludeOptions = { | 69 | const serverInclude: IncludeOptions = { |
71 | attributes: [ 'host' ], | 70 | attributes: [ 'host' ], |
72 | model: ServerModel.unscoped(), | 71 | model: ServerModel.unscoped(), |
73 | required: false | 72 | required: !!options.whereServer, |
73 | where: options.whereServer | ||
74 | } | 74 | } |
75 | 75 | ||
76 | const queryInclude: Includeable[] = [ | 76 | const queryInclude: Includeable[] = [ |
@@ -78,7 +78,7 @@ export type SummaryOptions = { | |||
78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
79 | model: ActorModel.unscoped(), | 79 | model: ActorModel.unscoped(), |
80 | required: options.actorRequired ?? true, | 80 | required: options.actorRequired ?? true, |
81 | where: whereActor, | 81 | where: options.whereActor, |
82 | include: [ | 82 | include: [ |
83 | serverInclude, | 83 | serverInclude, |
84 | 84 | ||
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 3a09e51d6..283856d3f 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -20,7 +20,6 @@ import { | |||
20 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' |
22 | import { getServerActor } from '@server/models/application/application' | 22 | import { getServerActor } from '@server/models/application/application' |
23 | import { VideoModel } from '@server/models/video/video' | ||
24 | import { | 23 | import { |
25 | MActorFollowActorsDefault, | 24 | MActorFollowActorsDefault, |
26 | MActorFollowActorsDefaultSubscription, | 25 | MActorFollowActorsDefaultSubscription, |
@@ -36,6 +35,7 @@ import { logger } from '../../helpers/logger' | |||
36 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 35 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' |
37 | import { AccountModel } from '../account/account' | 36 | import { AccountModel } from '../account/account' |
38 | import { ServerModel } from '../server/server' | 37 | import { ServerModel } from '../server/server' |
38 | import { doesExist } from '../shared/query' | ||
39 | import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' | 39 | import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' |
40 | import { VideoChannelModel } from '../video/video-channel' | 40 | import { VideoChannelModel } from '../video/video-channel' |
41 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 41 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
@@ -166,14 +166,8 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
166 | 166 | ||
167 | static isFollowedBy (actorId: number, followerActorId: number) { | 167 | static isFollowedBy (actorId: number, followerActorId: number) { |
168 | const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' | 168 | const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' |
169 | const options = { | ||
170 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
171 | bind: { actorId, followerActorId }, | ||
172 | raw: true | ||
173 | } | ||
174 | 169 | ||
175 | return VideoModel.sequelize.query(query, options) | 170 | return doesExist(query, { actorId, followerActorId }) |
176 | .then(results => results.length === 1) | ||
177 | } | 171 | } |
178 | 172 | ||
179 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | 173 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { |
@@ -324,13 +318,13 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
324 | 318 | ||
325 | const followWhere = state ? { state } : {} | 319 | const followWhere = state ? { state } : {} |
326 | const followingWhere: WhereOptions = {} | 320 | const followingWhere: WhereOptions = {} |
327 | const followingServerWhere: WhereOptions = {} | ||
328 | 321 | ||
329 | if (search) { | 322 | if (search) { |
330 | Object.assign(followingServerWhere, { | 323 | Object.assign(followWhere, { |
331 | host: { | 324 | [Op.or]: [ |
332 | [Op.iLike]: '%' + search + '%' | 325 | searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), |
333 | } | 326 | searchAttribute(options.search, '$ActorFollowing.Server.host$') |
327 | ] | ||
334 | }) | 328 | }) |
335 | } | 329 | } |
336 | 330 | ||
@@ -361,8 +355,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
361 | include: [ | 355 | include: [ |
362 | { | 356 | { |
363 | model: ServerModel, | 357 | model: ServerModel, |
364 | required: true, | 358 | required: true |
365 | where: followingServerWhere | ||
366 | } | 359 | } |
367 | ] | 360 | ] |
368 | } | 361 | } |
@@ -391,13 +384,13 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
391 | 384 | ||
392 | const followWhere = state ? { state } : {} | 385 | const followWhere = state ? { state } : {} |
393 | const followerWhere: WhereOptions = {} | 386 | const followerWhere: WhereOptions = {} |
394 | const followerServerWhere: WhereOptions = {} | ||
395 | 387 | ||
396 | if (search) { | 388 | if (search) { |
397 | Object.assign(followerServerWhere, { | 389 | Object.assign(followWhere, { |
398 | host: { | 390 | [Op.or]: [ |
399 | [Op.iLike]: '%' + search + '%' | 391 | searchAttribute(search, '$ActorFollower.preferredUsername$'), |
400 | } | 392 | searchAttribute(search, '$ActorFollower.Server.host$') |
393 | ] | ||
401 | }) | 394 | }) |
402 | } | 395 | } |
403 | 396 | ||
@@ -420,8 +413,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
420 | include: [ | 413 | include: [ |
421 | { | 414 | { |
422 | model: ServerModel, | 415 | model: ServerModel, |
423 | required: true, | 416 | required: true |
424 | where: followerServerWhere | ||
425 | } | 417 | } |
426 | ] | 418 | ] |
427 | }, | 419 | }, |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index ccda023e0..d645be248 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -160,8 +160,8 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu | |||
160 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 160 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
161 | logger.info('Removing duplicated video file %s.', logIdentifier) | 161 | logger.info('Removing duplicated video file %s.', logIdentifier) |
162 | 162 | ||
163 | videoFile.Video.removeFile(videoFile, true) | 163 | videoFile.Video.removeFileAndTorrent(videoFile, true) |
164 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 164 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
165 | } | 165 | } |
166 | 166 | ||
167 | if (instance.videoStreamingPlaylistId) { | 167 | if (instance.videoStreamingPlaylistId) { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts new file mode 100644 index 000000000..5b97510e0 --- /dev/null +++ b/server/models/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './query' | ||
2 | export * from './update' | ||
diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts new file mode 100644 index 000000000..036cc13c6 --- /dev/null +++ b/server/models/shared/query.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | import { BindOrReplacements, QueryTypes } from 'sequelize' | ||
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | |||
4 | function doesExist (query: string, bind?: BindOrReplacements) { | ||
5 | const options = { | ||
6 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
7 | bind, | ||
8 | raw: true | ||
9 | } | ||
10 | |||
11 | return sequelizeTypescript.query(query, options) | ||
12 | .then(results => results.length === 1) | ||
13 | } | ||
14 | |||
15 | export { | ||
16 | doesExist | ||
17 | } | ||
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts new file mode 100644 index 000000000..d338211e3 --- /dev/null +++ b/server/models/shared/update.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { QueryTypes, Transaction } from 'sequelize' | ||
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | |||
4 | // Sequelize always skip the update if we only update updatedAt field | ||
5 | function setAsUpdated (table: string, id: number, transaction?: Transaction) { | ||
6 | return sequelizeTypescript.query( | ||
7 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | ||
8 | { | ||
9 | replacements: { table, id, updatedAt: new Date() }, | ||
10 | type: QueryTypes.UPDATE, | ||
11 | transaction | ||
12 | } | ||
13 | ) | ||
14 | } | ||
15 | |||
16 | export { | ||
17 | setAsUpdated | ||
18 | } | ||
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index a7f84e9ca..04c5513a9 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | 1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { uuidToShort } from '@server/helpers/uuid' | ||
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 4 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' |
4 | import { AttributesOnly } from '@shared/core-utils' | 5 | import { AttributesOnly } from '@shared/core-utils' |
5 | import { UserNotification, UserNotificationType } from '../../../shared' | 6 | import { UserNotification, UserNotificationType } from '../../../shared' |
@@ -615,6 +616,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
615 | return { | 616 | return { |
616 | id: video.id, | 617 | id: video.id, |
617 | uuid: video.uuid, | 618 | uuid: video.uuid, |
619 | shortUUID: uuidToShort(video.uuid), | ||
618 | name: video.name | 620 | name: video.name |
619 | } | 621 | } |
620 | } | 622 | } |
@@ -628,6 +630,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
628 | ? { | 630 | ? { |
629 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, | 631 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, |
630 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, | 632 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, |
633 | shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid), | ||
631 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid | 634 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid |
632 | } | 635 | } |
633 | : undefined | 636 | : undefined |
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index ab4cf53a8..8a54de3b0 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -182,8 +182,8 @@ function streamingPlaylistsModelToFormattedJSON ( | |||
182 | return { | 182 | return { |
183 | id: playlist.id, | 183 | id: playlist.id, |
184 | type: playlist.type, | 184 | type: playlist.type, |
185 | playlistUrl: playlist.playlistUrl, | 185 | playlistUrl: playlist.getMasterPlaylistUrl(video), |
186 | segmentsSha256Url: playlist.segmentsSha256Url, | 186 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), |
187 | redundancies, | 187 | redundancies, |
188 | files | 188 | files |
189 | } | 189 | } |
@@ -331,7 +331,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
331 | type: 'Link', | 331 | type: 'Link', |
332 | name: 'sha256', | 332 | name: 'sha256', |
333 | mediaType: 'application/json' as 'application/json', | 333 | mediaType: 'application/json' as 'application/json', |
334 | href: playlist.segmentsSha256Url | 334 | href: playlist.getSha256SegmentsUrl(video) |
335 | }) | 335 | }) |
336 | 336 | ||
337 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) | 337 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) |
@@ -339,7 +339,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
339 | url.push({ | 339 | url.push({ |
340 | type: 'Link', | 340 | type: 'Link', |
341 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | 341 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', |
342 | href: playlist.playlistUrl, | 342 | href: playlist.getMasterPlaylistUrl(video), |
343 | tag | 343 | tag |
344 | }) | 344 | }) |
345 | } | 345 | } |
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts index abdd22188..742d19099 100644 --- a/server/models/video/sql/shared/video-tables.ts +++ b/server/models/video/sql/shared/video-tables.ts | |||
@@ -92,12 +92,13 @@ export class VideoTables { | |||
92 | } | 92 | } |
93 | 93 | ||
94 | getStreamingPlaylistAttributes () { | 94 | getStreamingPlaylistAttributes () { |
95 | let playlistKeys = [ 'id', 'playlistUrl', 'type' ] | 95 | let playlistKeys = [ 'id', 'playlistUrl', 'playlistFilename', 'type' ] |
96 | 96 | ||
97 | if (this.mode === 'get') { | 97 | if (this.mode === 'get') { |
98 | playlistKeys = playlistKeys.concat([ | 98 | playlistKeys = playlistKeys.concat([ |
99 | 'p2pMediaLoaderInfohashes', | 99 | 'p2pMediaLoaderInfohashes', |
100 | 'p2pMediaLoaderPeerVersion', | 100 | 'p2pMediaLoaderPeerVersion', |
101 | 'segmentsSha256Filename', | ||
101 | 'segmentsSha256Url', | 102 | 'segmentsSha256Url', |
102 | 'videoId', | 103 | 'videoId', |
103 | 'createdAt', | 104 | 'createdAt', |
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts index 30b251f0f..7625c003d 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/videos-id-list-query-builder.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { exists } from '@server/helpers/custom-validators/misc' | 3 | import { exists } from '@server/helpers/custom-validators/misc' |
4 | import { WEBSERVER } from '@server/initializers/constants' | ||
4 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | 5 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' |
5 | import { MUserAccountId, MUserId } from '@server/types/models' | 6 | import { MUserAccountId, MUserId } from '@server/types/models' |
6 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' | 7 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' |
@@ -25,6 +26,7 @@ export type BuildVideosListQueryOptions = { | |||
25 | 26 | ||
26 | nsfw?: boolean | 27 | nsfw?: boolean |
27 | filter?: VideoFilter | 28 | filter?: VideoFilter |
29 | host?: string | ||
28 | isLive?: boolean | 30 | isLive?: boolean |
29 | 31 | ||
30 | categoryOneOf?: number[] | 32 | categoryOneOf?: number[] |
@@ -33,6 +35,8 @@ export type BuildVideosListQueryOptions = { | |||
33 | tagsOneOf?: string[] | 35 | tagsOneOf?: string[] |
34 | tagsAllOf?: string[] | 36 | tagsAllOf?: string[] |
35 | 37 | ||
38 | uuids?: string[] | ||
39 | |||
36 | withFiles?: boolean | 40 | withFiles?: boolean |
37 | 41 | ||
38 | accountId?: number | 42 | accountId?: number |
@@ -131,6 +135,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
131 | this.whereOnlyLocal() | 135 | this.whereOnlyLocal() |
132 | } | 136 | } |
133 | 137 | ||
138 | if (options.host) { | ||
139 | this.whereHost(options.host) | ||
140 | } | ||
141 | |||
134 | if (options.accountId) { | 142 | if (options.accountId) { |
135 | this.whereAccountId(options.accountId) | 143 | this.whereAccountId(options.accountId) |
136 | } | 144 | } |
@@ -155,6 +163,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
155 | this.whereTagsAllOf(options.tagsAllOf) | 163 | this.whereTagsAllOf(options.tagsAllOf) |
156 | } | 164 | } |
157 | 165 | ||
166 | if (options.uuids) { | ||
167 | this.whereUUIDs(options.uuids) | ||
168 | } | ||
169 | |||
158 | if (options.nsfw === true) { | 170 | if (options.nsfw === true) { |
159 | this.whereNSFW() | 171 | this.whereNSFW() |
160 | } else if (options.nsfw === false) { | 172 | } else if (options.nsfw === false) { |
@@ -291,6 +303,19 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
291 | this.and.push('"video"."remote" IS FALSE') | 303 | this.and.push('"video"."remote" IS FALSE') |
292 | } | 304 | } |
293 | 305 | ||
306 | private whereHost (host: string) { | ||
307 | // Local instance | ||
308 | if (host === WEBSERVER.HOST) { | ||
309 | this.and.push('"accountActor"."serverId" IS NULL') | ||
310 | return | ||
311 | } | ||
312 | |||
313 | this.joins.push('INNER JOIN "server" ON "server"."id" = "accountActor"."serverId"') | ||
314 | |||
315 | this.and.push('"server"."host" = :host') | ||
316 | this.replacements.host = host | ||
317 | } | ||
318 | |||
294 | private whereAccountId (accountId: number) { | 319 | private whereAccountId (accountId: number) { |
295 | this.and.push('"account"."id" = :accountId') | 320 | this.and.push('"account"."id" = :accountId') |
296 | this.replacements.accountId = accountId | 321 | this.replacements.accountId = accountId |
@@ -304,16 +329,16 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
304 | private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { | 329 | private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { |
305 | let query = | 330 | let query = |
306 | '(' + | 331 | '(' + |
307 | ' EXISTS (' + | 332 | ' EXISTS (' + // Videos shared by actors we follow |
308 | ' SELECT 1 FROM "videoShare" ' + | 333 | ' SELECT 1 FROM "videoShare" ' + |
309 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + | 334 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + |
310 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + | 335 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + |
311 | ' WHERE "videoShare"."videoId" = "video"."id"' + | 336 | ' WHERE "videoShare"."videoId" = "video"."id"' + |
312 | ' )' + | 337 | ' )' + |
313 | ' OR' + | 338 | ' OR' + |
314 | ' EXISTS (' + | 339 | ' EXISTS (' + // Videos published by accounts we follow |
315 | ' SELECT 1 from "actorFollow" ' + | 340 | ' SELECT 1 from "actorFollow" ' + |
316 | ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + | 341 | ' WHERE "actorFollow"."targetActorId" = "account"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + |
317 | ' AND "actorFollow"."state" = \'accepted\'' + | 342 | ' AND "actorFollow"."state" = \'accepted\'' + |
318 | ' )' | 343 | ' )' |
319 | 344 | ||
@@ -367,6 +392,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
367 | ) | 392 | ) |
368 | } | 393 | } |
369 | 394 | ||
395 | private whereUUIDs (uuids: string[]) { | ||
396 | this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') | ||
397 | } | ||
398 | |||
370 | private whereCategoryOneOf (categoryOneOf: number[]) { | 399 | private whereCategoryOneOf (categoryOneOf: number[]) { |
371 | this.and.push('"video"."category" IN (:categoryOneOf)') | 400 | this.and.push('"video"."category" IN (:categoryOneOf)') |
372 | this.replacements.categoryOneOf = categoryOneOf | 401 | this.replacements.categoryOneOf = categoryOneOf |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 183e7448c..9f04a57c6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BeforeDestroy, | 4 | BeforeDestroy, |
@@ -17,9 +17,8 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
21 | import { MAccountActor } from '@server/types/models' | 20 | import { MAccountActor } from '@server/types/models' |
22 | import { AttributesOnly } from '@shared/core-utils' | 21 | import { AttributesOnly, pick } from '@shared/core-utils' |
23 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 22 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
24 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' | 23 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' |
25 | import { | 24 | import { |
@@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | |||
41 | import { ActorFollowModel } from '../actor/actor-follow' | 40 | import { ActorFollowModel } from '../actor/actor-follow' |
42 | import { ActorImageModel } from '../actor/actor-image' | 41 | import { ActorImageModel } from '../actor/actor-image' |
43 | import { ServerModel } from '../server/server' | 42 | import { ServerModel } from '../server/server' |
43 | import { setAsUpdated } from '../shared' | ||
44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
45 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
46 | import { VideoPlaylistModel } from './video-playlist' | 46 | import { VideoPlaylistModel } from './video-playlist' |
@@ -58,6 +58,8 @@ export enum ScopeNames { | |||
58 | type AvailableForListOptions = { | 58 | type AvailableForListOptions = { |
59 | actorId: number | 59 | actorId: number |
60 | search?: string | 60 | search?: string |
61 | host?: string | ||
62 | handles?: string[] | ||
61 | } | 63 | } |
62 | 64 | ||
63 | type AvailableWithStatsOptions = { | 65 | type AvailableWithStatsOptions = { |
@@ -83,7 +85,62 @@ export type SummaryOptions = { | |||
83 | // Only list local channels OR channels that are on an instance followed by actorId | 85 | // Only list local channels OR channels that are on an instance followed by actorId |
84 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) | 86 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) |
85 | 87 | ||
88 | const whereActorAnd: WhereOptions[] = [ | ||
89 | { | ||
90 | [Op.or]: [ | ||
91 | { | ||
92 | serverId: null | ||
93 | }, | ||
94 | { | ||
95 | serverId: { | ||
96 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | ||
97 | } | ||
98 | } | ||
99 | ] | ||
100 | } | ||
101 | ] | ||
102 | |||
103 | let serverRequired = false | ||
104 | let whereServer: WhereOptions | ||
105 | |||
106 | if (options.host && options.host !== WEBSERVER.HOST) { | ||
107 | serverRequired = true | ||
108 | whereServer = { host: options.host } | ||
109 | } | ||
110 | |||
111 | if (options.host === WEBSERVER.HOST) { | ||
112 | whereActorAnd.push({ | ||
113 | serverId: null | ||
114 | }) | ||
115 | } | ||
116 | |||
117 | let rootWhere: WhereOptions | ||
118 | if (options.handles) { | ||
119 | const or: WhereOptions[] = [] | ||
120 | |||
121 | for (const handle of options.handles || []) { | ||
122 | const [ preferredUsername, host ] = handle.split('@') | ||
123 | |||
124 | if (!host) { | ||
125 | or.push({ | ||
126 | '$Actor.preferredUsername$': preferredUsername, | ||
127 | '$Actor.serverId$': null | ||
128 | }) | ||
129 | } else { | ||
130 | or.push({ | ||
131 | '$Actor.preferredUsername$': preferredUsername, | ||
132 | '$Actor.Server.host$': host | ||
133 | }) | ||
134 | } | ||
135 | } | ||
136 | |||
137 | rootWhere = { | ||
138 | [Op.or]: or | ||
139 | } | ||
140 | } | ||
141 | |||
86 | return { | 142 | return { |
143 | where: rootWhere, | ||
87 | include: [ | 144 | include: [ |
88 | { | 145 | { |
89 | attributes: { | 146 | attributes: { |
@@ -91,19 +148,20 @@ export type SummaryOptions = { | |||
91 | }, | 148 | }, |
92 | model: ActorModel, | 149 | model: ActorModel, |
93 | where: { | 150 | where: { |
94 | [Op.or]: [ | 151 | [Op.and]: whereActorAnd |
95 | { | ||
96 | serverId: null | ||
97 | }, | ||
98 | { | ||
99 | serverId: { | ||
100 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | ||
101 | } | ||
102 | } | ||
103 | ] | ||
104 | }, | 152 | }, |
105 | include: [ | 153 | include: [ |
106 | { | 154 | { |
155 | model: ServerModel, | ||
156 | required: serverRequired, | ||
157 | where: whereServer | ||
158 | }, | ||
159 | { | ||
160 | model: ActorImageModel, | ||
161 | as: 'Avatar', | ||
162 | required: false | ||
163 | }, | ||
164 | { | ||
107 | model: ActorImageModel, | 165 | model: ActorImageModel, |
108 | as: 'Banner', | 166 | as: 'Banner', |
109 | required: false | 167 | required: false |
@@ -380,30 +438,6 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
380 | } | 438 | } |
381 | } | 439 | } |
382 | 440 | ||
383 | static listForApi (parameters: { | ||
384 | actorId: number | ||
385 | start: number | ||
386 | count: number | ||
387 | sort: string | ||
388 | }) { | ||
389 | const { actorId } = parameters | ||
390 | |||
391 | const query = { | ||
392 | offset: parameters.start, | ||
393 | limit: parameters.count, | ||
394 | order: getSort(parameters.sort) | ||
395 | } | ||
396 | |||
397 | return VideoChannelModel | ||
398 | .scope({ | ||
399 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | ||
400 | }) | ||
401 | .findAndCountAll(query) | ||
402 | .then(({ rows, count }) => { | ||
403 | return { total: count, data: rows } | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> { | 441 | static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> { |
408 | const query = { | 442 | const query = { |
409 | attributes: [ ], | 443 | attributes: [ ], |
@@ -425,26 +459,43 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
425 | .findAll(query) | 459 | .findAll(query) |
426 | } | 460 | } |
427 | 461 | ||
428 | static searchForApi (options: { | 462 | static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & { |
429 | actorId: number | ||
430 | search: string | ||
431 | start: number | 463 | start: number |
432 | count: number | 464 | count: number |
433 | sort: string | 465 | sort: string |
434 | }) { | 466 | }) { |
435 | const attributesInclude = [] | 467 | const { actorId } = parameters |
436 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) | ||
437 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') | ||
438 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | ||
439 | 468 | ||
440 | const query = { | 469 | const query = { |
441 | attributes: { | 470 | offset: parameters.start, |
442 | include: attributesInclude | 471 | limit: parameters.count, |
443 | }, | 472 | order: getSort(parameters.sort) |
444 | offset: options.start, | 473 | } |
445 | limit: options.count, | 474 | |
446 | order: getSort(options.sort), | 475 | return VideoChannelModel |
447 | where: { | 476 | .scope({ |
477 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | ||
478 | }) | ||
479 | .findAndCountAll(query) | ||
480 | .then(({ rows, count }) => { | ||
481 | return { total: count, data: rows } | ||
482 | }) | ||
483 | } | ||
484 | |||
485 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | ||
486 | start: number | ||
487 | count: number | ||
488 | sort: string | ||
489 | }) { | ||
490 | let attributesInclude: any[] = [ literal('0 as similarity') ] | ||
491 | let where: WhereOptions | ||
492 | |||
493 | if (options.search) { | ||
494 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) | ||
495 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') | ||
496 | attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ] | ||
497 | |||
498 | where = { | ||
448 | [Op.or]: [ | 499 | [Op.or]: [ |
449 | Sequelize.literal( | 500 | Sequelize.literal( |
450 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | 501 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
@@ -456,9 +507,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
456 | } | 507 | } |
457 | } | 508 | } |
458 | 509 | ||
510 | const query = { | ||
511 | attributes: { | ||
512 | include: attributesInclude | ||
513 | }, | ||
514 | offset: options.start, | ||
515 | limit: options.count, | ||
516 | order: getSort(options.sort), | ||
517 | where | ||
518 | } | ||
519 | |||
459 | return VideoChannelModel | 520 | return VideoChannelModel |
460 | .scope({ | 521 | .scope({ |
461 | method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ] | 522 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] |
462 | }) | 523 | }) |
463 | .findAndCountAll(query) | 524 | .findAndCountAll(query) |
464 | .then(({ rows, count }) => { | 525 | .then(({ rows, count }) => { |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 22cf63804..09fc5288b 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import * as memoizee from 'memoizee' | 2 | import * as memoizee from 'memoizee' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | 4 | import { FindOptions, Op, Transaction } from 'sequelize' |
5 | import { | 5 | import { |
6 | AllowNull, | 6 | AllowNull, |
7 | BelongsTo, | 7 | BelongsTo, |
@@ -44,6 +44,7 @@ import { | |||
44 | } from '../../initializers/constants' | 44 | } from '../../initializers/constants' |
45 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | 45 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' |
46 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 46 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
47 | import { doesExist } from '../shared' | ||
47 | import { parseAggregateResult, throwIfNotValid } from '../utils' | 48 | import { parseAggregateResult, throwIfNotValid } from '../utils' |
48 | import { VideoModel } from './video' | 49 | import { VideoModel } from './video' |
49 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 50 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
@@ -250,14 +251,8 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
250 | 251 | ||
251 | static doesInfohashExist (infoHash: string) { | 252 | static doesInfohashExist (infoHash: string) { |
252 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 253 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
253 | const options = { | ||
254 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
255 | bind: { infoHash }, | ||
256 | raw: true | ||
257 | } | ||
258 | 254 | ||
259 | return VideoModel.sequelize.query(query, options) | 255 | return doesExist(query, { infoHash }) |
260 | .then(results => results.length === 1) | ||
261 | } | 256 | } |
262 | 257 | ||
263 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | 258 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { |
@@ -266,6 +261,33 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
266 | return !!videoFile | 261 | return !!videoFile |
267 | } | 262 | } |
268 | 263 | ||
264 | static async doesOwnedTorrentFileExist (filename: string) { | ||
265 | const query = 'SELECT 1 FROM "videoFile" ' + | ||
266 | 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' + | ||
267 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + | ||
268 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | ||
269 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | ||
270 | |||
271 | return doesExist(query, { filename }) | ||
272 | } | ||
273 | |||
274 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | ||
275 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | ||
276 | 'WHERE "filename" = $filename LIMIT 1' | ||
277 | |||
278 | return doesExist(query, { filename }) | ||
279 | } | ||
280 | |||
281 | static loadByFilename (filename: string) { | ||
282 | const query = { | ||
283 | where: { | ||
284 | filename | ||
285 | } | ||
286 | } | ||
287 | |||
288 | return VideoFileModel.findOne(query) | ||
289 | } | ||
290 | |||
269 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { | 291 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { |
270 | const query = { | 292 | const query = { |
271 | where: { | 293 | where: { |
@@ -443,10 +465,9 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
443 | } | 465 | } |
444 | 466 | ||
445 | getFileDownloadUrl (video: MVideoWithHost) { | 467 | getFileDownloadUrl (video: MVideoWithHost) { |
446 | const basePath = this.isHLS() | 468 | const path = this.isHLS() |
447 | ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS | 469 | ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`) |
448 | : STATIC_DOWNLOAD_PATHS.VIDEOS | 470 | : join(STATIC_DOWNLOAD_PATHS.VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`) |
449 | const path = join(basePath, this.filename) | ||
450 | 471 | ||
451 | if (video.isOwned()) return WEBSERVER.URL + path | 472 | if (video.isOwned()) return WEBSERVER.URL + path |
452 | 473 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index af81c9906..630684a88 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -17,10 +17,9 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
21 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' | 20 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' |
22 | import { MAccountId, MChannelId } from '@server/types/models' | 21 | import { MAccountId, MChannelId } from '@server/types/models' |
23 | import { AttributesOnly } from '@shared/core-utils' | 22 | import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' |
24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | 23 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
25 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 24 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
26 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 25 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
@@ -53,6 +52,7 @@ import { | |||
53 | } from '../../types/models/video/video-playlist' | 52 | } from '../../types/models/video/video-playlist' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 53 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 54 | import { ActorModel } from '../actor/actor' |
55 | import { setAsUpdated } from '../shared' | ||
56 | import { | 56 | import { |
57 | buildServerIdsFollowedBy, | 57 | buildServerIdsFollowedBy, |
58 | buildTrigramSearchIndex, | 58 | buildTrigramSearchIndex, |
@@ -82,6 +82,8 @@ type AvailableForListOptions = { | |||
82 | videoChannelId?: number | 82 | videoChannelId?: number |
83 | listMyPlaylists?: boolean | 83 | listMyPlaylists?: boolean |
84 | search?: string | 84 | search?: string |
85 | host?: string | ||
86 | uuids?: string[] | ||
85 | withVideos?: boolean | 87 | withVideos?: boolean |
86 | } | 88 | } |
87 | 89 | ||
@@ -141,9 +143,19 @@ function getVideoLengthSelect () { | |||
141 | ] | 143 | ] |
142 | }, | 144 | }, |
143 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { | 145 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { |
146 | const whereAnd: WhereOptions[] = [] | ||
147 | |||
148 | const whereServer = options.host && options.host !== WEBSERVER.HOST | ||
149 | ? { host: options.host } | ||
150 | : undefined | ||
151 | |||
144 | let whereActor: WhereOptions = {} | 152 | let whereActor: WhereOptions = {} |
145 | 153 | ||
146 | const whereAnd: WhereOptions[] = [] | 154 | if (options.host === WEBSERVER.HOST) { |
155 | whereActor = { | ||
156 | [Op.and]: [ { serverId: null } ] | ||
157 | } | ||
158 | } | ||
147 | 159 | ||
148 | if (options.listMyPlaylists !== true) { | 160 | if (options.listMyPlaylists !== true) { |
149 | whereAnd.push({ | 161 | whereAnd.push({ |
@@ -168,9 +180,7 @@ function getVideoLengthSelect () { | |||
168 | }) | 180 | }) |
169 | } | 181 | } |
170 | 182 | ||
171 | whereActor = { | 183 | Object.assign(whereActor, { [Op.or]: whereActorOr }) |
172 | [Op.or]: whereActorOr | ||
173 | } | ||
174 | } | 184 | } |
175 | 185 | ||
176 | if (options.accountId) { | 186 | if (options.accountId) { |
@@ -191,18 +201,26 @@ function getVideoLengthSelect () { | |||
191 | }) | 201 | }) |
192 | } | 202 | } |
193 | 203 | ||
204 | if (options.uuids) { | ||
205 | whereAnd.push({ | ||
206 | uuid: { | ||
207 | [Op.in]: options.uuids | ||
208 | } | ||
209 | }) | ||
210 | } | ||
211 | |||
194 | if (options.withVideos === true) { | 212 | if (options.withVideos === true) { |
195 | whereAnd.push( | 213 | whereAnd.push( |
196 | literal(`(${getVideoLengthSelect()}) != 0`) | 214 | literal(`(${getVideoLengthSelect()}) != 0`) |
197 | ) | 215 | ) |
198 | } | 216 | } |
199 | 217 | ||
200 | const attributesInclude = [] | 218 | let attributesInclude: any[] = [ literal('0 as similarity') ] |
201 | 219 | ||
202 | if (options.search) { | 220 | if (options.search) { |
203 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) | 221 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) |
204 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | 222 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') |
205 | attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) | 223 | attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ] |
206 | 224 | ||
207 | whereAnd.push({ | 225 | whereAnd.push({ |
208 | [Op.or]: [ | 226 | [Op.or]: [ |
@@ -228,7 +246,7 @@ function getVideoLengthSelect () { | |||
228 | include: [ | 246 | include: [ |
229 | { | 247 | { |
230 | model: AccountModel.scope({ | 248 | model: AccountModel.scope({ |
231 | method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] | 249 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ] |
232 | }), | 250 | }), |
233 | required: true | 251 | required: true |
234 | }, | 252 | }, |
@@ -339,17 +357,10 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
339 | }) | 357 | }) |
340 | Thumbnail: ThumbnailModel | 358 | Thumbnail: ThumbnailModel |
341 | 359 | ||
342 | static listForApi (options: { | 360 | static listForApi (options: AvailableForListOptions & { |
343 | followerActorId: number | ||
344 | start: number | 361 | start: number |
345 | count: number | 362 | count: number |
346 | sort: string | 363 | sort: string |
347 | type?: VideoPlaylistType | ||
348 | accountId?: number | ||
349 | videoChannelId?: number | ||
350 | listMyPlaylists?: boolean | ||
351 | search?: string | ||
352 | withVideos?: boolean // false by default | ||
353 | }) { | 364 | }) { |
354 | const query = { | 365 | const query = { |
355 | offset: options.start, | 366 | offset: options.start, |
@@ -362,12 +373,8 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
362 | method: [ | 373 | method: [ |
363 | ScopeNames.AVAILABLE_FOR_LIST, | 374 | ScopeNames.AVAILABLE_FOR_LIST, |
364 | { | 375 | { |
365 | type: options.type, | 376 | ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]), |
366 | followerActorId: options.followerActorId, | 377 | |
367 | accountId: options.accountId, | ||
368 | videoChannelId: options.videoChannelId, | ||
369 | listMyPlaylists: options.listMyPlaylists, | ||
370 | search: options.search, | ||
371 | withVideos: options.withVideos || false | 378 | withVideos: options.withVideos || false |
372 | } as AvailableForListOptions | 379 | } as AvailableForListOptions |
373 | ] | 380 | ] |
@@ -384,15 +391,14 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
384 | }) | 391 | }) |
385 | } | 392 | } |
386 | 393 | ||
387 | static searchForApi (options: { | 394 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { |
388 | followerActorId: number | ||
389 | start: number | 395 | start: number |
390 | count: number | 396 | count: number |
391 | sort: string | 397 | sort: string |
392 | search?: string | ||
393 | }) { | 398 | }) { |
394 | return VideoPlaylistModel.listForApi({ | 399 | return VideoPlaylistModel.listForApi({ |
395 | ...options, | 400 | ...options, |
401 | |||
396 | type: VideoPlaylistType.REGULAR, | 402 | type: VideoPlaylistType.REGULAR, |
397 | listMyPlaylists: false, | 403 | listMyPlaylists: false, |
398 | withVideos: true | 404 | withVideos: true |
@@ -560,12 +566,12 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
560 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) | 566 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) |
561 | } | 567 | } |
562 | 568 | ||
563 | getWatchUrl () { | 569 | getWatchStaticPath () { |
564 | return WEBSERVER.URL + '/w/p/' + this.uuid | 570 | return buildPlaylistWatchPath({ shortUUID: uuidToShort(this.uuid) }) |
565 | } | 571 | } |
566 | 572 | ||
567 | getEmbedStaticPath () { | 573 | getEmbedStaticPath () { |
568 | return '/video-playlists/embed/' + this.uuid | 574 | return buildPlaylistEmbedPath(this) |
569 | } | 575 | } |
570 | 576 | ||
571 | static async getStats () { | 577 | static async getStats () { |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index d627e8c9d..d591a3134 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -1,19 +1,27 @@ | |||
1 | import * as memoizee from 'memoizee' | 1 | import * as memoizee from 'memoizee' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { Op, QueryTypes } from 'sequelize' | 3 | import { Op } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { VideoFileModel } from '@server/models/video/video-file' | 5 | import { VideoFileModel } from '@server/models/video/video-file' |
6 | import { MStreamingPlaylist } from '@server/types/models' | 6 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
7 | import { AttributesOnly } from '@shared/core-utils' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 8 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
8 | import { sha1 } from '../../helpers/core-utils' | 9 | import { sha1 } from '../../helpers/core-utils' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { isArrayOf } from '../../helpers/custom-validators/misc' | 11 | import { isArrayOf } from '../../helpers/custom-validators/misc' |
11 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 12 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
12 | import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' | 13 | import { |
14 | CONSTRAINTS_FIELDS, | ||
15 | MEMOIZE_LENGTH, | ||
16 | MEMOIZE_TTL, | ||
17 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
18 | STATIC_PATHS, | ||
19 | WEBSERVER | ||
20 | } from '../../initializers/constants' | ||
13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 21 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
22 | import { doesExist } from '../shared' | ||
14 | import { throwIfNotValid } from '../utils' | 23 | import { throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 24 | import { VideoModel } from './video' |
16 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | 25 | ||
18 | @Table({ | 26 | @Table({ |
19 | tableName: 'videoStreamingPlaylist', | 27 | tableName: 'videoStreamingPlaylist', |
@@ -43,7 +51,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
43 | type: VideoStreamingPlaylistType | 51 | type: VideoStreamingPlaylistType |
44 | 52 | ||
45 | @AllowNull(false) | 53 | @AllowNull(false) |
46 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) | 54 | @Column |
55 | playlistFilename: string | ||
56 | |||
57 | @AllowNull(true) | ||
58 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true)) | ||
47 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | 59 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) |
48 | playlistUrl: string | 60 | playlistUrl: string |
49 | 61 | ||
@@ -57,7 +69,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
57 | p2pMediaLoaderPeerVersion: number | 69 | p2pMediaLoaderPeerVersion: number |
58 | 70 | ||
59 | @AllowNull(false) | 71 | @AllowNull(false) |
60 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) | 72 | @Column |
73 | segmentsSha256Filename: string | ||
74 | |||
75 | @AllowNull(true) | ||
76 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true)) | ||
61 | @Column | 77 | @Column |
62 | segmentsSha256Url: string | 78 | segmentsSha256Url: string |
63 | 79 | ||
@@ -98,14 +114,8 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
98 | 114 | ||
99 | static doesInfohashExist (infoHash: string) { | 115 | static doesInfohashExist (infoHash: string) { |
100 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | 116 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' |
101 | const options = { | ||
102 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
103 | bind: { infoHash }, | ||
104 | raw: true | ||
105 | } | ||
106 | 117 | ||
107 | return VideoModel.sequelize.query<object>(query, options) | 118 | return doesExist(query, { infoHash }) |
108 | .then(results => results.length === 1) | ||
109 | } | 119 | } |
110 | 120 | ||
111 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { | 121 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
@@ -125,7 +135,13 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
125 | p2pMediaLoaderPeerVersion: { | 135 | p2pMediaLoaderPeerVersion: { |
126 | [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION | 136 | [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION |
127 | } | 137 | } |
128 | } | 138 | }, |
139 | include: [ | ||
140 | { | ||
141 | model: VideoModel.unscoped(), | ||
142 | required: true | ||
143 | } | ||
144 | ] | ||
129 | } | 145 | } |
130 | 146 | ||
131 | return VideoStreamingPlaylistModel.findAll(query) | 147 | return VideoStreamingPlaylistModel.findAll(query) |
@@ -144,7 +160,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
144 | return VideoStreamingPlaylistModel.findByPk(id, options) | 160 | return VideoStreamingPlaylistModel.findByPk(id, options) |
145 | } | 161 | } |
146 | 162 | ||
147 | static loadHLSPlaylistByVideo (videoId: number) { | 163 | static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> { |
148 | const options = { | 164 | const options = { |
149 | where: { | 165 | where: { |
150 | type: VideoStreamingPlaylistType.HLS, | 166 | type: VideoStreamingPlaylistType.HLS, |
@@ -155,30 +171,29 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
155 | return VideoStreamingPlaylistModel.findOne(options) | 171 | return VideoStreamingPlaylistModel.findOne(options) |
156 | } | 172 | } |
157 | 173 | ||
158 | static getHlsPlaylistFilename (resolution: number) { | 174 | static async loadOrGenerate (video: MVideo) { |
159 | return resolution + '.m3u8' | 175 | let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
160 | } | 176 | if (!playlist) playlist = new VideoStreamingPlaylistModel() |
161 | 177 | ||
162 | static getMasterHlsPlaylistFilename () { | 178 | return Object.assign(playlist, { videoId: video.id, Video: video }) |
163 | return 'master.m3u8' | ||
164 | } | 179 | } |
165 | 180 | ||
166 | static getHlsSha256SegmentsFilename () { | 181 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { |
167 | return 'segments-sha256.json' | 182 | const masterPlaylistUrl = this.getMasterPlaylistUrl(video) |
168 | } | ||
169 | 183 | ||
170 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | 184 | this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files) |
171 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
172 | } | 185 | } |
173 | 186 | ||
174 | static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { | 187 | getMasterPlaylistUrl (video: MVideo) { |
175 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | 188 | if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid) |
189 | |||
190 | return this.playlistUrl | ||
176 | } | 191 | } |
177 | 192 | ||
178 | static getHlsSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { | 193 | getSha256SegmentsUrl (video: MVideo) { |
179 | if (isLive) return join('/live', 'segments-sha256', videoUUID) | 194 | if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive) |
180 | 195 | ||
181 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | 196 | return this.segmentsSha256Url |
182 | } | 197 | } |
183 | 198 | ||
184 | getStringType () { | 199 | getStringType () { |
@@ -195,4 +210,14 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
195 | return this.type === other.type && | 210 | return this.type === other.type && |
196 | this.videoId === other.videoId | 211 | this.videoId === other.videoId |
197 | } | 212 | } |
213 | |||
214 | private getMasterPlaylistStaticPath (videoUUID: string) { | ||
215 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) | ||
216 | } | ||
217 | |||
218 | private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { | ||
219 | if (isLive) return join('/live', 'segments-sha256', videoUUID) | ||
220 | |||
221 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename) | ||
222 | } | ||
198 | } | 223 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1e5648a36..56a5b0e18 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -24,14 +24,14 @@ import { | |||
24 | Table, | 24 | Table, |
25 | UpdatedAt | 25 | UpdatedAt |
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
28 | import { uuidToShort } from '@server/helpers/uuid' | ||
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live/live-manager' | 30 | import { LiveManager } from '@server/lib/live/live-manager' |
31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' | 31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' |
32 | import { getServerActor } from '@server/models/application/application' | 32 | import { getServerActor } from '@server/models/application/application' |
33 | import { ModelCache } from '@server/models/model-cache' | 33 | import { ModelCache } from '@server/models/model-cache' |
34 | import { AttributesOnly } from '@shared/core-utils' | 34 | import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' |
35 | import { VideoFile } from '@shared/models/videos/video-file.model' | 35 | import { VideoFile } from '@shared/models/videos/video-file.model' |
36 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | 36 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' |
37 | import { VideoObject } from '../../../shared/models/activitypub/objects' | 37 | import { VideoObject } from '../../../shared/models/activitypub/objects' |
@@ -91,6 +91,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy' | |||
91 | import { ServerModel } from '../server/server' | 91 | import { ServerModel } from '../server/server' |
92 | import { TrackerModel } from '../server/tracker' | 92 | import { TrackerModel } from '../server/tracker' |
93 | import { VideoTrackerModel } from '../server/video-tracker' | 93 | import { VideoTrackerModel } from '../server/video-tracker' |
94 | import { setAsUpdated } from '../shared' | ||
94 | import { UserModel } from '../user/user' | 95 | import { UserModel } from '../user/user' |
95 | import { UserVideoHistoryModel } from '../user/user-video-history' | 96 | import { UserVideoHistoryModel } from '../user/user-video-history' |
96 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | 97 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' |
@@ -762,8 +763,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
762 | 763 | ||
763 | // Remove physical files and torrents | 764 | // Remove physical files and torrents |
764 | instance.VideoFiles.forEach(file => { | 765 | instance.VideoFiles.forEach(file => { |
765 | tasks.push(instance.removeFile(file)) | 766 | tasks.push(instance.removeFileAndTorrent(file)) |
766 | tasks.push(file.removeTorrent()) | ||
767 | }) | 767 | }) |
768 | 768 | ||
769 | // Remove playlists file | 769 | // Remove playlists file |
@@ -1070,7 +1070,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1070 | const trendingDays = options.sort.endsWith('trending') | 1070 | const trendingDays = options.sort.endsWith('trending') |
1071 | ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | 1071 | ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS |
1072 | : undefined | 1072 | : undefined |
1073 | let trendingAlgorithm | 1073 | |
1074 | let trendingAlgorithm: string | ||
1074 | if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot' | 1075 | if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot' |
1075 | if (options.sort.endsWith('best')) trendingAlgorithm = 'best' | 1076 | if (options.sort.endsWith('best')) trendingAlgorithm = 'best' |
1076 | 1077 | ||
@@ -1082,40 +1083,44 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1082 | : serverActor.id | 1083 | : serverActor.id |
1083 | 1084 | ||
1084 | const queryOptions = { | 1085 | const queryOptions = { |
1085 | start: options.start, | 1086 | ...pick(options, [ |
1086 | count: options.count, | 1087 | 'start', |
1087 | sort: options.sort, | 1088 | 'count', |
1089 | 'sort', | ||
1090 | 'nsfw', | ||
1091 | 'isLive', | ||
1092 | 'categoryOneOf', | ||
1093 | 'licenceOneOf', | ||
1094 | 'languageOneOf', | ||
1095 | 'tagsOneOf', | ||
1096 | 'tagsAllOf', | ||
1097 | 'filter', | ||
1098 | 'withFiles', | ||
1099 | 'accountId', | ||
1100 | 'videoChannelId', | ||
1101 | 'videoPlaylistId', | ||
1102 | 'includeLocalVideos', | ||
1103 | 'user', | ||
1104 | 'historyOfUser', | ||
1105 | 'search' | ||
1106 | ]), | ||
1107 | |||
1088 | followerActorId, | 1108 | followerActorId, |
1089 | serverAccountId: serverActor.Account.id, | 1109 | serverAccountId: serverActor.Account.id, |
1090 | nsfw: options.nsfw, | ||
1091 | isLive: options.isLive, | ||
1092 | categoryOneOf: options.categoryOneOf, | ||
1093 | licenceOneOf: options.licenceOneOf, | ||
1094 | languageOneOf: options.languageOneOf, | ||
1095 | tagsOneOf: options.tagsOneOf, | ||
1096 | tagsAllOf: options.tagsAllOf, | ||
1097 | filter: options.filter, | ||
1098 | withFiles: options.withFiles, | ||
1099 | accountId: options.accountId, | ||
1100 | videoChannelId: options.videoChannelId, | ||
1101 | videoPlaylistId: options.videoPlaylistId, | ||
1102 | includeLocalVideos: options.includeLocalVideos, | ||
1103 | user: options.user, | ||
1104 | historyOfUser: options.historyOfUser, | ||
1105 | trendingDays, | 1110 | trendingDays, |
1106 | trendingAlgorithm, | 1111 | trendingAlgorithm |
1107 | search: options.search | ||
1108 | } | 1112 | } |
1109 | 1113 | ||
1110 | return VideoModel.getAvailableForApi(queryOptions, options.countVideos) | 1114 | return VideoModel.getAvailableForApi(queryOptions, options.countVideos) |
1111 | } | 1115 | } |
1112 | 1116 | ||
1113 | static async searchAndPopulateAccountAndServer (options: { | 1117 | static async searchAndPopulateAccountAndServer (options: { |
1118 | start: number | ||
1119 | count: number | ||
1120 | sort: string | ||
1114 | includeLocalVideos: boolean | 1121 | includeLocalVideos: boolean |
1115 | search?: string | 1122 | search?: string |
1116 | start?: number | 1123 | host?: string |
1117 | count?: number | ||
1118 | sort?: string | ||
1119 | startDate?: string // ISO 8601 | 1124 | startDate?: string // ISO 8601 |
1120 | endDate?: string // ISO 8601 | 1125 | endDate?: string // ISO 8601 |
1121 | originallyPublishedStartDate?: string | 1126 | originallyPublishedStartDate?: string |
@@ -1131,41 +1136,38 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1131 | durationMax?: number // seconds | 1136 | durationMax?: number // seconds |
1132 | user?: MUserAccountId | 1137 | user?: MUserAccountId |
1133 | filter?: VideoFilter | 1138 | filter?: VideoFilter |
1139 | uuids?: string[] | ||
1134 | }) { | 1140 | }) { |
1135 | const serverActor = await getServerActor() | 1141 | const serverActor = await getServerActor() |
1136 | 1142 | ||
1137 | const queryOptions = { | 1143 | const queryOptions = { |
1138 | followerActorId: serverActor.id, | 1144 | ...pick(options, [ |
1139 | serverAccountId: serverActor.Account.id, | 1145 | 'includeLocalVideos', |
1140 | 1146 | 'nsfw', | |
1141 | includeLocalVideos: options.includeLocalVideos, | 1147 | 'isLive', |
1142 | nsfw: options.nsfw, | 1148 | 'categoryOneOf', |
1143 | isLive: options.isLive, | 1149 | 'licenceOneOf', |
1144 | 1150 | 'languageOneOf', | |
1145 | categoryOneOf: options.categoryOneOf, | 1151 | 'tagsOneOf', |
1146 | licenceOneOf: options.licenceOneOf, | 1152 | 'tagsAllOf', |
1147 | languageOneOf: options.languageOneOf, | 1153 | 'user', |
1154 | 'filter', | ||
1155 | 'host', | ||
1156 | 'start', | ||
1157 | 'count', | ||
1158 | 'sort', | ||
1159 | 'startDate', | ||
1160 | 'endDate', | ||
1161 | 'originallyPublishedStartDate', | ||
1162 | 'originallyPublishedEndDate', | ||
1163 | 'durationMin', | ||
1164 | 'durationMax', | ||
1165 | 'uuids', | ||
1166 | 'search' | ||
1167 | ]), | ||
1148 | 1168 | ||
1149 | tagsOneOf: options.tagsOneOf, | 1169 | followerActorId: serverActor.id, |
1150 | tagsAllOf: options.tagsAllOf, | 1170 | serverAccountId: serverActor.Account.id |
1151 | |||
1152 | user: options.user, | ||
1153 | filter: options.filter, | ||
1154 | |||
1155 | start: options.start, | ||
1156 | count: options.count, | ||
1157 | sort: options.sort, | ||
1158 | |||
1159 | startDate: options.startDate, | ||
1160 | endDate: options.endDate, | ||
1161 | |||
1162 | originallyPublishedStartDate: options.originallyPublishedStartDate, | ||
1163 | originallyPublishedEndDate: options.originallyPublishedEndDate, | ||
1164 | |||
1165 | durationMin: options.durationMin, | ||
1166 | durationMax: options.durationMax, | ||
1167 | |||
1168 | search: options.search | ||
1169 | } | 1171 | } |
1170 | 1172 | ||
1171 | return VideoModel.getAvailableForApi(queryOptions) | 1173 | return VideoModel.getAvailableForApi(queryOptions) |
@@ -1579,11 +1581,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1579 | } | 1581 | } |
1580 | 1582 | ||
1581 | getWatchStaticPath () { | 1583 | getWatchStaticPath () { |
1582 | return '/w/' + this.uuid | 1584 | return buildVideoWatchPath({ shortUUID: uuidToShort(this.uuid) }) |
1583 | } | 1585 | } |
1584 | 1586 | ||
1585 | getEmbedStaticPath () { | 1587 | getEmbedStaticPath () { |
1586 | return '/videos/embed/' + this.uuid | 1588 | return buildVideoEmbedPath(this) |
1587 | } | 1589 | } |
1588 | 1590 | ||
1589 | getMiniatureStaticPath () { | 1591 | getMiniatureStaticPath () { |
@@ -1670,10 +1672,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1670 | .concat(toAdd) | 1672 | .concat(toAdd) |
1671 | } | 1673 | } |
1672 | 1674 | ||
1673 | removeFile (videoFile: MVideoFile, isRedundancy = false) { | 1675 | removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { |
1674 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) | 1676 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) |
1675 | return remove(filePath) | 1677 | |
1676 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1678 | const promises: Promise<any>[] = [ remove(filePath) ] |
1679 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) | ||
1680 | |||
1681 | return Promise.all(promises) | ||
1677 | } | 1682 | } |
1678 | 1683 | ||
1679 | async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { | 1684 | async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { |