diff options
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/video-channel.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-playlist-element.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 92 | ||||
-rw-r--r-- | server/models/video/video-share.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 38 |
5 files changed, 109 insertions, 39 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index c077fb518..ca06048d1 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -67,9 +67,9 @@ type AvailableForListOptions = { | |||
67 | ] | 67 | ] |
68 | }) | 68 | }) |
69 | @Scopes({ | 69 | @Scopes({ |
70 | [ScopeNames.SUMMARY]: (required: boolean, withAccount: boolean) => { | 70 | [ScopeNames.SUMMARY]: (withAccount = false) => { |
71 | const base: IFindOptions<VideoChannelModel> = { | 71 | const base: IFindOptions<VideoChannelModel> = { |
72 | attributes: [ 'name', 'description', 'id' ], | 72 | attributes: [ 'name', 'description', 'id', 'actorId' ], |
73 | include: [ | 73 | include: [ |
74 | { | 74 | { |
75 | attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 75 | attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
@@ -225,7 +225,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
225 | foreignKey: { | 225 | foreignKey: { |
226 | allowNull: true | 226 | allowNull: true |
227 | }, | 227 | }, |
228 | onDelete: 'cascade', | 228 | onDelete: 'CASCADE', |
229 | hooks: true | 229 | hooks: true |
230 | }) | 230 | }) |
231 | VideoPlaylists: VideoPlaylistModel[] | 231 | VideoPlaylists: VideoPlaylistModel[] |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index 5530e0492..a2bd225a1 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -20,6 +20,7 @@ import { getSort, throwIfNotValid } from '../utils' | |||
20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
21 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 21 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
22 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 22 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
23 | import * as validator from 'validator' | ||
23 | 24 | ||
24 | @Table({ | 25 | @Table({ |
25 | tableName: 'videoPlaylistElement', | 26 | tableName: 'videoPlaylistElement', |
@@ -35,10 +36,6 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object | |||
35 | unique: true | 36 | unique: true |
36 | }, | 37 | }, |
37 | { | 38 | { |
38 | fields: [ 'videoPlaylistId', 'position' ], | ||
39 | unique: true | ||
40 | }, | ||
41 | { | ||
42 | fields: [ 'url' ], | 39 | fields: [ 'url' ], |
43 | unique: true | 40 | unique: true |
44 | } | 41 | } |
@@ -143,7 +140,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
143 | return VideoPlaylistElementModel.findOne(query) | 140 | return VideoPlaylistElementModel.findOne(query) |
144 | } | 141 | } |
145 | 142 | ||
146 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number) { | 143 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Sequelize.Transaction) { |
147 | const query = { | 144 | const query = { |
148 | attributes: [ 'url' ], | 145 | attributes: [ 'url' ], |
149 | offset: start, | 146 | offset: start, |
@@ -151,7 +148,8 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
151 | order: getSort('position'), | 148 | order: getSort('position'), |
152 | where: { | 149 | where: { |
153 | videoPlaylistId | 150 | videoPlaylistId |
154 | } | 151 | }, |
152 | transaction: t | ||
155 | } | 153 | } |
156 | 154 | ||
157 | return VideoPlaylistElementModel | 155 | return VideoPlaylistElementModel |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 397887ebf..ce49f77ec 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -24,7 +24,14 @@ import { | |||
24 | isVideoPlaylistPrivacyValid | 24 | isVideoPlaylistPrivacyValid |
25 | } from '../../helpers/custom-validators/video-playlists' | 25 | } from '../../helpers/custom-validators/video-playlists' |
26 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 26 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
27 | import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers' | 27 | import { |
28 | CONFIG, | ||
29 | CONSTRAINTS_FIELDS, | ||
30 | STATIC_PATHS, | ||
31 | THUMBNAILS_SIZE, | ||
32 | VIDEO_PLAYLIST_PRIVACIES, | ||
33 | VIDEO_PLAYLIST_TYPES | ||
34 | } from '../../initializers' | ||
28 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | 35 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' |
29 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
30 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 37 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
@@ -34,22 +41,25 @@ import { PlaylistObject } from '../../../shared/models/activitypub/objects/playl | |||
34 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | 41 | import { activityPubCollectionPagination } from '../../helpers/activitypub' |
35 | import { remove } from 'fs-extra' | 42 | import { remove } from 'fs-extra' |
36 | import { logger } from '../../helpers/logger' | 43 | import { logger } from '../../helpers/logger' |
44 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | ||
37 | 45 | ||
38 | enum ScopeNames { | 46 | enum ScopeNames { |
39 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 47 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
40 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', | 48 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', |
41 | WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' | 49 | WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', |
50 | WITH_ACCOUNT = 'WITH_ACCOUNT' | ||
42 | } | 51 | } |
43 | 52 | ||
44 | type AvailableForListOptions = { | 53 | type AvailableForListOptions = { |
45 | followerActorId: number | 54 | followerActorId: number |
46 | accountId?: number, | 55 | type?: VideoPlaylistType |
56 | accountId?: number | ||
47 | videoChannelId?: number | 57 | videoChannelId?: number |
48 | privateAndUnlisted?: boolean | 58 | privateAndUnlisted?: boolean |
49 | } | 59 | } |
50 | 60 | ||
51 | @Scopes({ | 61 | @Scopes({ |
52 | [ScopeNames.WITH_VIDEOS_LENGTH]: { | 62 | [ ScopeNames.WITH_VIDEOS_LENGTH ]: { |
53 | attributes: { | 63 | attributes: { |
54 | include: [ | 64 | include: [ |
55 | [ | 65 | [ |
@@ -59,7 +69,15 @@ type AvailableForListOptions = { | |||
59 | ] | 69 | ] |
60 | } | 70 | } |
61 | }, | 71 | }, |
62 | [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: { | 72 | [ ScopeNames.WITH_ACCOUNT ]: { |
73 | include: [ | ||
74 | { | ||
75 | model: () => AccountModel, | ||
76 | required: true | ||
77 | } | ||
78 | ] | ||
79 | }, | ||
80 | [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { | ||
63 | include: [ | 81 | include: [ |
64 | { | 82 | { |
65 | model: () => AccountModel.scope(AccountScopeNames.SUMMARY), | 83 | model: () => AccountModel.scope(AccountScopeNames.SUMMARY), |
@@ -71,7 +89,7 @@ type AvailableForListOptions = { | |||
71 | } | 89 | } |
72 | ] | 90 | ] |
73 | }, | 91 | }, |
74 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { | 92 | [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { |
75 | // Only list local playlists OR playlists that are on an instance followed by actorId | 93 | // Only list local playlists OR playlists that are on an instance followed by actorId |
76 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | 94 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) |
77 | const actorWhere = { | 95 | const actorWhere = { |
@@ -107,6 +125,12 @@ type AvailableForListOptions = { | |||
107 | }) | 125 | }) |
108 | } | 126 | } |
109 | 127 | ||
128 | if (options.type) { | ||
129 | whereAnd.push({ | ||
130 | type: options.type | ||
131 | }) | ||
132 | } | ||
133 | |||
110 | const where = { | 134 | const where = { |
111 | [Sequelize.Op.and]: whereAnd | 135 | [Sequelize.Op.and]: whereAnd |
112 | } | 136 | } |
@@ -179,6 +203,11 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
179 | @Column(DataType.UUID) | 203 | @Column(DataType.UUID) |
180 | uuid: string | 204 | uuid: string |
181 | 205 | ||
206 | @AllowNull(false) | ||
207 | @Default(VideoPlaylistType.REGULAR) | ||
208 | @Column | ||
209 | type: VideoPlaylistType | ||
210 | |||
182 | @ForeignKey(() => AccountModel) | 211 | @ForeignKey(() => AccountModel) |
183 | @Column | 212 | @Column |
184 | ownerAccountId: number | 213 | ownerAccountId: number |
@@ -208,13 +237,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
208 | name: 'videoPlaylistId', | 237 | name: 'videoPlaylistId', |
209 | allowNull: false | 238 | allowNull: false |
210 | }, | 239 | }, |
211 | onDelete: 'cascade' | 240 | onDelete: 'CASCADE' |
212 | }) | 241 | }) |
213 | VideoPlaylistElements: VideoPlaylistElementModel[] | 242 | VideoPlaylistElements: VideoPlaylistElementModel[] |
214 | 243 | ||
215 | // Calculated field | ||
216 | videosLength?: number | ||
217 | |||
218 | @BeforeDestroy | 244 | @BeforeDestroy |
219 | static async removeFiles (instance: VideoPlaylistModel) { | 245 | static async removeFiles (instance: VideoPlaylistModel) { |
220 | logger.info('Removing files of video playlist %s.', instance.url) | 246 | logger.info('Removing files of video playlist %s.', instance.url) |
@@ -227,6 +253,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
227 | start: number, | 253 | start: number, |
228 | count: number, | 254 | count: number, |
229 | sort: string, | 255 | sort: string, |
256 | type?: VideoPlaylistType, | ||
230 | accountId?: number, | 257 | accountId?: number, |
231 | videoChannelId?: number, | 258 | videoChannelId?: number, |
232 | privateAndUnlisted?: boolean | 259 | privateAndUnlisted?: boolean |
@@ -242,6 +269,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
242 | method: [ | 269 | method: [ |
243 | ScopeNames.AVAILABLE_FOR_LIST, | 270 | ScopeNames.AVAILABLE_FOR_LIST, |
244 | { | 271 | { |
272 | type: options.type, | ||
245 | followerActorId: options.followerActorId, | 273 | followerActorId: options.followerActorId, |
246 | accountId: options.accountId, | 274 | accountId: options.accountId, |
247 | videoChannelId: options.videoChannelId, | 275 | videoChannelId: options.videoChannelId, |
@@ -289,7 +317,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
289 | .then(e => !!e) | 317 | .then(e => !!e) |
290 | } | 318 | } |
291 | 319 | ||
292 | static load (id: number | string, transaction: Sequelize.Transaction) { | 320 | static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) { |
293 | const where = buildWhereIdOrUUID(id) | 321 | const where = buildWhereIdOrUUID(id) |
294 | 322 | ||
295 | const query = { | 323 | const query = { |
@@ -298,14 +326,39 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
298 | } | 326 | } |
299 | 327 | ||
300 | return VideoPlaylistModel | 328 | return VideoPlaylistModel |
301 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ]) | 329 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ]) |
302 | .findOne(query) | 330 | .findOne(query) |
303 | } | 331 | } |
304 | 332 | ||
333 | static loadByUrlAndPopulateAccount (url: string) { | ||
334 | const query = { | ||
335 | where: { | ||
336 | url | ||
337 | } | ||
338 | } | ||
339 | |||
340 | return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query) | ||
341 | } | ||
342 | |||
305 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 343 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
306 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' | 344 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' |
307 | } | 345 | } |
308 | 346 | ||
347 | static getTypeLabel (type: VideoPlaylistType) { | ||
348 | return VIDEO_PLAYLIST_TYPES[type] || 'Unknown' | ||
349 | } | ||
350 | |||
351 | static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) { | ||
352 | const query = { | ||
353 | where: { | ||
354 | videoChannelId | ||
355 | }, | ||
356 | transaction | ||
357 | } | ||
358 | |||
359 | return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) | ||
360 | } | ||
361 | |||
309 | getThumbnailName () { | 362 | getThumbnailName () { |
310 | const extension = '.jpg' | 363 | const extension = '.jpg' |
311 | 364 | ||
@@ -345,7 +398,12 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
345 | 398 | ||
346 | thumbnailPath: this.getThumbnailStaticPath(), | 399 | thumbnailPath: this.getThumbnailStaticPath(), |
347 | 400 | ||
348 | videosLength: this.videosLength, | 401 | type: { |
402 | id: this.type, | ||
403 | label: VideoPlaylistModel.getTypeLabel(this.type) | ||
404 | }, | ||
405 | |||
406 | videosLength: this.get('videosLength'), | ||
349 | 407 | ||
350 | createdAt: this.createdAt, | 408 | createdAt: this.createdAt, |
351 | updatedAt: this.updatedAt, | 409 | updatedAt: this.updatedAt, |
@@ -355,18 +413,20 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
355 | } | 413 | } |
356 | } | 414 | } |
357 | 415 | ||
358 | toActivityPubObject (): Promise<PlaylistObject> { | 416 | toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> { |
359 | const handler = (start: number, count: number) => { | 417 | const handler = (start: number, count: number) => { |
360 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count) | 418 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) |
361 | } | 419 | } |
362 | 420 | ||
363 | return activityPubCollectionPagination(this.url, handler, null) | 421 | return activityPubCollectionPagination(this.url, handler, page) |
364 | .then(o => { | 422 | .then(o => { |
365 | return Object.assign(o, { | 423 | return Object.assign(o, { |
366 | type: 'Playlist' as 'Playlist', | 424 | type: 'Playlist' as 'Playlist', |
367 | name: this.name, | 425 | name: this.name, |
368 | content: this.description, | 426 | content: this.description, |
369 | uuid: this.uuid, | 427 | uuid: this.uuid, |
428 | published: this.createdAt.toISOString(), | ||
429 | updated: this.updatedAt.toISOString(), | ||
370 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], | 430 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], |
371 | icon: { | 431 | icon: { |
372 | type: 'Image' as 'Image', | 432 | type: 'Image' as 'Image', |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index c87f71277..7df0ed18d 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -125,7 +125,7 @@ export class VideoShareModel extends Model<VideoShareModel> { | |||
125 | .then(res => res.map(r => r.Actor)) | 125 | .then(res => res.map(r => r.Actor)) |
126 | } | 126 | } |
127 | 127 | ||
128 | static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> { | 128 | static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> { |
129 | const query = { | 129 | const query = { |
130 | attributes: [], | 130 | attributes: [], |
131 | include: [ | 131 | include: [ |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 7a102b058..a563f78ef 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -225,7 +225,7 @@ type AvailableForListIDsOptions = { | |||
225 | }, | 225 | }, |
226 | include: [ | 226 | include: [ |
227 | { | 227 | { |
228 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY) | 228 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }) |
229 | } | 229 | } |
230 | ] | 230 | ] |
231 | } | 231 | } |
@@ -1535,18 +1535,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1535 | 1535 | ||
1536 | if (ids.length === 0) return { data: [], total: count } | 1536 | if (ids.length === 0) return { data: [], total: count } |
1537 | 1537 | ||
1538 | // FIXME: typings | 1538 | const secondQuery: IFindOptions<VideoModel> = { |
1539 | const apiScope: any[] = [ | ||
1540 | { | ||
1541 | method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] | ||
1542 | } | ||
1543 | ] | ||
1544 | |||
1545 | if (options.user) { | ||
1546 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) | ||
1547 | } | ||
1548 | |||
1549 | const secondQuery = { | ||
1550 | offset: 0, | 1539 | offset: 0, |
1551 | limit: query.limit, | 1540 | limit: query.limit, |
1552 | attributes: query.attributes, | 1541 | attributes: query.attributes, |
@@ -1556,6 +1545,29 @@ export class VideoModel extends Model<VideoModel> { | |||
1556 | ) | 1545 | ) |
1557 | ] | 1546 | ] |
1558 | } | 1547 | } |
1548 | |||
1549 | // FIXME: typing | ||
1550 | const apiScope: any[] = [] | ||
1551 | |||
1552 | if (options.user) { | ||
1553 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) | ||
1554 | |||
1555 | // Even if the relation is n:m, we know that a user only have 0..1 video history | ||
1556 | // So we won't have multiple rows for the same video | ||
1557 | // A subquery adds some bugs in our query so disable it | ||
1558 | secondQuery.subQuery = false | ||
1559 | } | ||
1560 | |||
1561 | apiScope.push({ | ||
1562 | method: [ | ||
1563 | ScopeNames.FOR_API, { | ||
1564 | ids, withFiles: | ||
1565 | options.withFiles, | ||
1566 | videoPlaylistId: options.videoPlaylistId | ||
1567 | } as ForAPIOptions | ||
1568 | ] | ||
1569 | }) | ||
1570 | |||
1559 | const rows = await VideoModel.scope(apiScope).findAll(secondQuery) | 1571 | const rows = await VideoModel.scope(apiScope).findAll(secondQuery) |
1560 | 1572 | ||
1561 | return { | 1573 | return { |