]>
Commit | Line | Data |
---|---|---|
2650d6d4 | 1 | import { join } from 'path' |
37a44fc9 | 2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
418d092a C |
3 | import { |
4 | AllowNull, | |
418d092a C |
5 | BelongsTo, |
6 | Column, | |
7 | CreatedAt, | |
8 | DataType, | |
9 | Default, | |
10 | ForeignKey, | |
11 | HasMany, | |
e8bafea3 | 12 | HasOne, |
418d092a C |
13 | Is, |
14 | IsUUID, | |
15 | Model, | |
16 | Scopes, | |
17 | Table, | |
822c7e61 | 18 | UpdatedAt |
418d092a | 19 | } from 'sequelize-typescript' |
2650d6d4 | 20 | import { MAccountId, MChannelId } from '@server/types/models' |
0628157f C |
21 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' |
22 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | |
6b5f72be | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
2650d6d4 C |
24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
25 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | |
418d092a | 26 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
2650d6d4 C |
27 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' |
28 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | |
29 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | |
30 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | |
418d092a C |
31 | import { |
32 | isVideoPlaylistDescriptionValid, | |
33 | isVideoPlaylistNameValid, | |
34 | isVideoPlaylistPrivacyValid | |
35 | } from '../../helpers/custom-validators/video-playlists' | |
df0b219d | 36 | import { |
9f79ade6 | 37 | ACTIVITY_PUB, |
df0b219d C |
38 | CONSTRAINTS_FIELDS, |
39 | STATIC_PATHS, | |
40 | THUMBNAILS_SIZE, | |
41 | VIDEO_PLAYLIST_PRIVACIES, | |
6dd9de95 C |
42 | VIDEO_PLAYLIST_TYPES, |
43 | WEBSERVER | |
74dc3bca | 44 | } from '../../initializers/constants' |
2650d6d4 | 45 | import { MThumbnail } from '../../types/models/video/thumbnail' |
453e83ea | 46 | import { |
822c7e61 C |
47 | MVideoPlaylistAccountThumbnail, |
48 | MVideoPlaylistAP, | |
1ca9f7c3 | 49 | MVideoPlaylistFormattable, |
453e83ea C |
50 | MVideoPlaylistFull, |
51 | MVideoPlaylistFullSummary, | |
52 | MVideoPlaylistIdWithElements | |
26d6bf65 | 53 | } from '../../types/models/video/video-playlist' |
2650d6d4 | 54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
7d9ba5c0 | 55 | import { ActorModel } from '../actor/actor' |
fa47956e | 56 | import { setAsUpdated } from '../shared' |
37a44fc9 C |
57 | import { |
58 | buildServerIdsFollowedBy, | |
59 | buildTrigramSearchIndex, | |
60 | buildWhereIdOrUUID, | |
61 | createSimilarityAttribute, | |
62 | getPlaylistSort, | |
63 | isOutdated, | |
64 | throwIfNotValid | |
65 | } from '../utils' | |
2650d6d4 C |
66 | import { ThumbnailModel } from './thumbnail' |
67 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | |
68 | import { VideoPlaylistElementModel } from './video-playlist-element' | |
418d092a C |
69 | |
70 | enum ScopeNames { | |
71 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | |
72 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', | |
df0b219d | 73 | WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', |
09979f89 | 74 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
e8bafea3 | 75 | WITH_THUMBNAIL = 'WITH_THUMBNAIL', |
09979f89 | 76 | WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' |
418d092a C |
77 | } |
78 | ||
79 | type AvailableForListOptions = { | |
fe19f600 | 80 | followerActorId?: number |
df0b219d C |
81 | type?: VideoPlaylistType |
82 | accountId?: number | |
418d092a | 83 | videoChannelId?: number |
a1587156 | 84 | listMyPlaylists?: boolean |
c06af501 | 85 | search?: string |
fa47956e | 86 | host?: string |
fbd67e7f | 87 | uuids?: string[] |
37a44fc9 C |
88 | withVideos?: boolean |
89 | } | |
90 | ||
91 | function getVideoLengthSelect () { | |
92 | return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"' | |
418d092a C |
93 | } |
94 | ||
3acc5084 | 95 | @Scopes(() => ({ |
a1587156 | 96 | [ScopeNames.WITH_THUMBNAIL]: { |
e8bafea3 C |
97 | include: [ |
98 | { | |
3acc5084 | 99 | model: ThumbnailModel, |
e8bafea3 C |
100 | required: false |
101 | } | |
102 | ] | |
103 | }, | |
a1587156 | 104 | [ScopeNames.WITH_VIDEOS_LENGTH]: { |
418d092a C |
105 | attributes: { |
106 | include: [ | |
1735c825 | 107 | [ |
37a44fc9 | 108 | literal(`(${getVideoLengthSelect()})`), |
418d092a C |
109 | 'videosLength' |
110 | ] | |
111 | ] | |
112 | } | |
3acc5084 | 113 | } as FindOptions, |
a1587156 | 114 | [ScopeNames.WITH_ACCOUNT]: { |
df0b219d C |
115 | include: [ |
116 | { | |
3acc5084 | 117 | model: AccountModel, |
df0b219d C |
118 | required: true |
119 | } | |
120 | ] | |
121 | }, | |
a1587156 | 122 | [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: { |
418d092a C |
123 | include: [ |
124 | { | |
3acc5084 | 125 | model: AccountModel.scope(AccountScopeNames.SUMMARY), |
418d092a C |
126 | required: true |
127 | }, | |
128 | { | |
3acc5084 | 129 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), |
418d092a C |
130 | required: false |
131 | } | |
132 | ] | |
133 | }, | |
a1587156 | 134 | [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: { |
09979f89 C |
135 | include: [ |
136 | { | |
3acc5084 | 137 | model: AccountModel, |
09979f89 C |
138 | required: true |
139 | }, | |
140 | { | |
3acc5084 | 141 | model: VideoChannelModel, |
09979f89 C |
142 | required: false |
143 | } | |
144 | ] | |
145 | }, | |
a1587156 | 146 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { |
fa47956e C |
147 | const whereAnd: WhereOptions[] = [] |
148 | ||
149 | const whereServer = options.host && options.host !== WEBSERVER.HOST | |
150 | ? { host: options.host } | |
151 | : undefined | |
152 | ||
6b0c3c7c | 153 | let whereActor: WhereOptions = {} |
418d092a | 154 | |
fa47956e C |
155 | if (options.host === WEBSERVER.HOST) { |
156 | whereActor = { | |
157 | [Op.and]: [ { serverId: null } ] | |
158 | } | |
159 | } | |
418d092a | 160 | |
6b0c3c7c | 161 | if (options.listMyPlaylists !== true) { |
418d092a C |
162 | whereAnd.push({ |
163 | privacy: VideoPlaylistPrivacy.PUBLIC | |
164 | }) | |
6b0c3c7c | 165 | |
fe19f600 RK |
166 | // Only list local playlists |
167 | const whereActorOr: WhereOptions[] = [ | |
168 | { | |
169 | serverId: null | |
170 | } | |
171 | ] | |
6b0c3c7c | 172 | |
fe19f600 RK |
173 | // … OR playlists that are on an instance followed by actorId |
174 | if (options.followerActorId) { | |
175 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | |
176 | ||
177 | whereActorOr.push({ | |
178 | serverId: { | |
179 | [Op.in]: literal(inQueryInstanceFollow) | |
6b0c3c7c | 180 | } |
fe19f600 RK |
181 | }) |
182 | } | |
183 | ||
fa47956e | 184 | Object.assign(whereActor, { [Op.or]: whereActorOr }) |
418d092a C |
185 | } |
186 | ||
187 | if (options.accountId) { | |
188 | whereAnd.push({ | |
189 | ownerAccountId: options.accountId | |
190 | }) | |
191 | } | |
192 | ||
193 | if (options.videoChannelId) { | |
194 | whereAnd.push({ | |
195 | videoChannelId: options.videoChannelId | |
196 | }) | |
197 | } | |
198 | ||
df0b219d C |
199 | if (options.type) { |
200 | whereAnd.push({ | |
201 | type: options.type | |
202 | }) | |
203 | } | |
204 | ||
fbd67e7f C |
205 | if (options.uuids) { |
206 | whereAnd.push({ | |
207 | uuid: { | |
208 | [Op.in]: options.uuids | |
209 | } | |
210 | }) | |
211 | } | |
212 | ||
37a44fc9 C |
213 | if (options.withVideos === true) { |
214 | whereAnd.push( | |
215 | literal(`(${getVideoLengthSelect()}) != 0`) | |
216 | ) | |
217 | } | |
218 | ||
fbd67e7f | 219 | let attributesInclude: any[] = [ literal('0 as similarity') ] |
37a44fc9 | 220 | |
c06af501 | 221 | if (options.search) { |
37a44fc9 C |
222 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) |
223 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | |
fbd67e7f | 224 | attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ] |
37a44fc9 | 225 | |
c06af501 | 226 | whereAnd.push({ |
37a44fc9 C |
227 | [Op.or]: [ |
228 | Sequelize.literal( | |
229 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | |
230 | ), | |
231 | Sequelize.literal( | |
232 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | |
233 | ) | |
234 | ] | |
c06af501 RK |
235 | }) |
236 | } | |
237 | ||
418d092a | 238 | const where = { |
1735c825 | 239 | [Op.and]: whereAnd |
418d092a C |
240 | } |
241 | ||
418d092a | 242 | return { |
37a44fc9 C |
243 | attributes: { |
244 | include: attributesInclude | |
245 | }, | |
418d092a C |
246 | where, |
247 | include: [ | |
248 | { | |
b49f22d8 | 249 | model: AccountModel.scope({ |
fa47956e | 250 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ] |
b49f22d8 | 251 | }), |
418d092a C |
252 | required: true |
253 | }, | |
254 | { | |
255 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | |
256 | required: false | |
257 | } | |
258 | ] | |
3acc5084 | 259 | } as FindOptions |
418d092a | 260 | } |
3acc5084 | 261 | })) |
418d092a C |
262 | |
263 | @Table({ | |
264 | tableName: 'videoPlaylist', | |
265 | indexes: [ | |
37a44fc9 C |
266 | buildTrigramSearchIndex('video_playlist_name_trigram', 'name'), |
267 | ||
418d092a C |
268 | { |
269 | fields: [ 'ownerAccountId' ] | |
270 | }, | |
271 | { | |
272 | fields: [ 'videoChannelId' ] | |
273 | }, | |
274 | { | |
275 | fields: [ 'url' ], | |
276 | unique: true | |
277 | } | |
278 | ] | |
279 | }) | |
16c016e8 | 280 | export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlaylistModel>>> { |
418d092a C |
281 | @CreatedAt |
282 | createdAt: Date | |
283 | ||
284 | @UpdatedAt | |
285 | updatedAt: Date | |
286 | ||
287 | @AllowNull(false) | |
288 | @Is('VideoPlaylistName', value => throwIfNotValid(value, isVideoPlaylistNameValid, 'name')) | |
289 | @Column | |
290 | name: string | |
291 | ||
292 | @AllowNull(true) | |
1735c825 | 293 | @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true)) |
1c320673 | 294 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.DESCRIPTION.max)) |
418d092a C |
295 | description: string |
296 | ||
297 | @AllowNull(false) | |
298 | @Is('VideoPlaylistPrivacy', value => throwIfNotValid(value, isVideoPlaylistPrivacyValid, 'privacy')) | |
299 | @Column | |
300 | privacy: VideoPlaylistPrivacy | |
301 | ||
302 | @AllowNull(false) | |
303 | @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | |
304 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max)) | |
305 | url: string | |
306 | ||
307 | @AllowNull(false) | |
308 | @Default(DataType.UUIDV4) | |
309 | @IsUUID(4) | |
310 | @Column(DataType.UUID) | |
311 | uuid: string | |
312 | ||
df0b219d C |
313 | @AllowNull(false) |
314 | @Default(VideoPlaylistType.REGULAR) | |
315 | @Column | |
316 | type: VideoPlaylistType | |
317 | ||
418d092a C |
318 | @ForeignKey(() => AccountModel) |
319 | @Column | |
320 | ownerAccountId: number | |
321 | ||
322 | @BelongsTo(() => AccountModel, { | |
323 | foreignKey: { | |
324 | allowNull: false | |
325 | }, | |
326 | onDelete: 'CASCADE' | |
327 | }) | |
328 | OwnerAccount: AccountModel | |
329 | ||
330 | @ForeignKey(() => VideoChannelModel) | |
331 | @Column | |
332 | videoChannelId: number | |
333 | ||
334 | @BelongsTo(() => VideoChannelModel, { | |
335 | foreignKey: { | |
07b1a18a | 336 | allowNull: true |
418d092a C |
337 | }, |
338 | onDelete: 'CASCADE' | |
339 | }) | |
340 | VideoChannel: VideoChannelModel | |
341 | ||
342 | @HasMany(() => VideoPlaylistElementModel, { | |
343 | foreignKey: { | |
344 | name: 'videoPlaylistId', | |
345 | allowNull: false | |
346 | }, | |
df0b219d | 347 | onDelete: 'CASCADE' |
418d092a C |
348 | }) |
349 | VideoPlaylistElements: VideoPlaylistElementModel[] | |
350 | ||
e8bafea3 C |
351 | @HasOne(() => ThumbnailModel, { |
352 | foreignKey: { | |
353 | name: 'videoPlaylistId', | |
354 | allowNull: true | |
355 | }, | |
356 | onDelete: 'CASCADE', | |
357 | hooks: true | |
358 | }) | |
359 | Thumbnail: ThumbnailModel | |
418d092a | 360 | |
9c9a236b | 361 | static listForApi (options: AvailableForListOptions & { |
a1587156 C |
362 | start: number |
363 | count: number | |
364 | sort: string | |
418d092a C |
365 | }) { |
366 | const query = { | |
367 | offset: options.start, | |
368 | limit: options.count, | |
49cff3a4 | 369 | order: getPlaylistSort(options.sort) |
418d092a C |
370 | } |
371 | ||
3acc5084 | 372 | const scopes: (string | ScopeOptions)[] = [ |
418d092a C |
373 | { |
374 | method: [ | |
375 | ScopeNames.AVAILABLE_FOR_LIST, | |
376 | { | |
9c9a236b C |
377 | ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]), |
378 | ||
37a44fc9 | 379 | withVideos: options.withVideos || false |
418d092a C |
380 | } as AvailableForListOptions |
381 | ] | |
3acc5084 | 382 | }, |
e8bafea3 C |
383 | ScopeNames.WITH_VIDEOS_LENGTH, |
384 | ScopeNames.WITH_THUMBNAIL | |
418d092a C |
385 | ] |
386 | ||
387 | return VideoPlaylistModel | |
388 | .scope(scopes) | |
389 | .findAndCountAll(query) | |
390 | .then(({ rows, count }) => { | |
391 | return { total: count, data: rows } | |
392 | }) | |
393 | } | |
394 | ||
9c9a236b | 395 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { |
37a44fc9 C |
396 | start: number |
397 | count: number | |
398 | sort: string | |
37a44fc9 C |
399 | }) { |
400 | return VideoPlaylistModel.listForApi({ | |
401 | ...options, | |
9c9a236b | 402 | |
37a44fc9 C |
403 | type: VideoPlaylistType.REGULAR, |
404 | listMyPlaylists: false, | |
405 | withVideos: true | |
406 | }) | |
407 | } | |
408 | ||
7405b6ba C |
409 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { |
410 | const where = { | |
411 | privacy: VideoPlaylistPrivacy.PUBLIC | |
412 | } | |
413 | ||
414 | if (options.account) { | |
415 | Object.assign(where, { ownerAccountId: options.account.id }) | |
416 | } | |
417 | ||
418 | if (options.channel) { | |
419 | Object.assign(where, { videoChannelId: options.channel.id }) | |
420 | } | |
421 | ||
418d092a C |
422 | const query = { |
423 | attributes: [ 'url' ], | |
424 | offset: start, | |
425 | limit: count, | |
7405b6ba | 426 | where |
418d092a C |
427 | } |
428 | ||
429 | return VideoPlaylistModel.findAndCountAll(query) | |
430 | .then(({ rows, count }) => { | |
431 | return { total: count, data: rows.map(p => p.url) } | |
432 | }) | |
433 | } | |
434 | ||
b49f22d8 | 435 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { |
f0a39880 C |
436 | const query = { |
437 | attributes: [ 'id' ], | |
438 | where: { | |
439 | ownerAccountId: accountId | |
440 | }, | |
441 | include: [ | |
442 | { | |
bfbd9128 | 443 | attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ], |
f0a39880 C |
444 | model: VideoPlaylistElementModel.unscoped(), |
445 | where: { | |
446 | videoId: { | |
0374b6b5 | 447 | [Op.in]: videoIds |
f0a39880 C |
448 | } |
449 | }, | |
450 | required: true | |
451 | } | |
452 | ] | |
453 | } | |
454 | ||
455 | return VideoPlaylistModel.findAll(query) | |
456 | } | |
457 | ||
418d092a C |
458 | static doesPlaylistExist (url: string) { |
459 | const query = { | |
b49f22d8 | 460 | attributes: [ 'id' ], |
418d092a C |
461 | where: { |
462 | url | |
463 | } | |
464 | } | |
465 | ||
466 | return VideoPlaylistModel | |
467 | .findOne(query) | |
468 | .then(e => !!e) | |
469 | } | |
470 | ||
b49f22d8 | 471 | static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction): Promise<MVideoPlaylistFullSummary> { |
418d092a C |
472 | const where = buildWhereIdOrUUID(id) |
473 | ||
474 | const query = { | |
475 | where, | |
476 | transaction | |
477 | } | |
478 | ||
479 | return VideoPlaylistModel | |
e8bafea3 | 480 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) |
09979f89 C |
481 | .findOne(query) |
482 | } | |
483 | ||
b49f22d8 | 484 | static loadWithAccountAndChannel (id: number | string, transaction: Transaction): Promise<MVideoPlaylistFull> { |
09979f89 C |
485 | const where = buildWhereIdOrUUID(id) |
486 | ||
487 | const query = { | |
488 | where, | |
489 | transaction | |
490 | } | |
491 | ||
492 | return VideoPlaylistModel | |
e8bafea3 | 493 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) |
418d092a C |
494 | .findOne(query) |
495 | } | |
496 | ||
b49f22d8 | 497 | static loadByUrlAndPopulateAccount (url: string): Promise<MVideoPlaylistAccountThumbnail> { |
df0b219d C |
498 | const query = { |
499 | where: { | |
500 | url | |
501 | } | |
502 | } | |
503 | ||
e8bafea3 | 504 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
df0b219d C |
505 | } |
506 | ||
37a44fc9 C |
507 | static loadByUrlWithAccountAndChannelSummary (url: string): Promise<MVideoPlaylistFullSummary> { |
508 | const query = { | |
509 | where: { | |
510 | url | |
511 | } | |
512 | } | |
513 | ||
514 | return VideoPlaylistModel | |
515 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) | |
516 | .findOne(query) | |
517 | } | |
518 | ||
418d092a C |
519 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
520 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' | |
521 | } | |
522 | ||
df0b219d C |
523 | static getTypeLabel (type: VideoPlaylistType) { |
524 | return VIDEO_PLAYLIST_TYPES[type] || 'Unknown' | |
525 | } | |
526 | ||
1735c825 | 527 | static resetPlaylistsOfChannel (videoChannelId: number, transaction: Transaction) { |
df0b219d C |
528 | const query = { |
529 | where: { | |
530 | videoChannelId | |
531 | }, | |
532 | transaction | |
533 | } | |
534 | ||
535 | return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) | |
536 | } | |
537 | ||
453e83ea | 538 | async setAndSaveThumbnail (thumbnail: MThumbnail, t: Transaction) { |
3acc5084 | 539 | thumbnail.videoPlaylistId = this.id |
e8bafea3 | 540 | |
3acc5084 | 541 | this.Thumbnail = await thumbnail.save({ transaction: t }) |
e8bafea3 C |
542 | } |
543 | ||
544 | hasThumbnail () { | |
545 | return !!this.Thumbnail | |
546 | } | |
547 | ||
65af03a2 C |
548 | hasGeneratedThumbnail () { |
549 | return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true | |
550 | } | |
551 | ||
e8bafea3 | 552 | generateThumbnailName () { |
418d092a C |
553 | const extension = '.jpg' |
554 | ||
d4a8e7a6 | 555 | return 'playlist-' + buildUUID() + extension |
418d092a C |
556 | } |
557 | ||
558 | getThumbnailUrl () { | |
e8bafea3 C |
559 | if (!this.hasThumbnail()) return null |
560 | ||
3acc5084 | 561 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename |
418d092a C |
562 | } |
563 | ||
564 | getThumbnailStaticPath () { | |
e8bafea3 | 565 | if (!this.hasThumbnail()) return null |
418d092a | 566 | |
3acc5084 | 567 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) |
418d092a C |
568 | } |
569 | ||
15a7eafb C |
570 | getWatchStaticPath () { |
571 | return buildPlaylistWatchPath({ shortUUID: uuidToShort(this.uuid) }) | |
8d987ec6 K |
572 | } |
573 | ||
6fad8e51 | 574 | getEmbedStaticPath () { |
15a7eafb | 575 | return buildPlaylistEmbedPath(this) |
6fad8e51 C |
576 | } |
577 | ||
fe19f600 RK |
578 | static async getStats () { |
579 | const totalLocalPlaylists = await VideoPlaylistModel.count({ | |
580 | include: [ | |
581 | { | |
582 | model: AccountModel, | |
583 | required: true, | |
584 | include: [ | |
585 | { | |
586 | model: ActorModel, | |
587 | required: true, | |
588 | where: { | |
589 | serverId: null | |
590 | } | |
591 | } | |
592 | ] | |
593 | } | |
594 | ], | |
595 | where: { | |
596 | privacy: VideoPlaylistPrivacy.PUBLIC | |
597 | } | |
598 | }) | |
599 | ||
600 | return { | |
601 | totalLocalPlaylists | |
602 | } | |
603 | } | |
604 | ||
9f79ade6 | 605 | setAsRefreshed () { |
49af5ac8 | 606 | return setAsUpdated('videoPlaylist', this.id) |
9f79ade6 C |
607 | } |
608 | ||
37a44fc9 C |
609 | setVideosLength (videosLength: number) { |
610 | this.set('videosLength' as any, videosLength, { raw: true }) | |
611 | } | |
612 | ||
418d092a C |
613 | isOwned () { |
614 | return this.OwnerAccount.isOwned() | |
615 | } | |
616 | ||
9f79ade6 C |
617 | isOutdated () { |
618 | if (this.isOwned()) return false | |
619 | ||
620 | return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL) | |
621 | } | |
622 | ||
1ca9f7c3 | 623 | toFormattedJSON (this: MVideoPlaylistFormattable): VideoPlaylist { |
418d092a C |
624 | return { |
625 | id: this.id, | |
626 | uuid: this.uuid, | |
d4a8e7a6 C |
627 | shortUUID: uuidToShort(this.uuid), |
628 | ||
418d092a C |
629 | isLocal: this.isOwned(), |
630 | ||
37a44fc9 C |
631 | url: this.url, |
632 | ||
418d092a C |
633 | displayName: this.name, |
634 | description: this.description, | |
635 | privacy: { | |
636 | id: this.privacy, | |
637 | label: VideoPlaylistModel.getPrivacyLabel(this.privacy) | |
638 | }, | |
639 | ||
640 | thumbnailPath: this.getThumbnailStaticPath(), | |
951b582f | 641 | embedPath: this.getEmbedStaticPath(), |
418d092a | 642 | |
df0b219d C |
643 | type: { |
644 | id: this.type, | |
645 | label: VideoPlaylistModel.getTypeLabel(this.type) | |
646 | }, | |
647 | ||
1735c825 | 648 | videosLength: this.get('videosLength') as number, |
418d092a C |
649 | |
650 | createdAt: this.createdAt, | |
651 | updatedAt: this.updatedAt, | |
652 | ||
653 | ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(), | |
c1843150 C |
654 | videoChannel: this.VideoChannel |
655 | ? this.VideoChannel.toFormattedSummaryJSON() | |
656 | : null | |
418d092a C |
657 | } |
658 | } | |
659 | ||
b5fecbf4 | 660 | toActivityPubObject (this: MVideoPlaylistAP, page: number, t: Transaction): Promise<PlaylistObject> { |
418d092a | 661 | const handler = (start: number, count: number) => { |
df0b219d | 662 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) |
418d092a C |
663 | } |
664 | ||
e8bafea3 C |
665 | let icon: ActivityIconObject |
666 | if (this.hasThumbnail()) { | |
667 | icon = { | |
668 | type: 'Image' as 'Image', | |
669 | url: this.getThumbnailUrl(), | |
670 | mediaType: 'image/jpeg' as 'image/jpeg', | |
671 | width: THUMBNAILS_SIZE.width, | |
672 | height: THUMBNAILS_SIZE.height | |
673 | } | |
674 | } | |
675 | ||
df0b219d | 676 | return activityPubCollectionPagination(this.url, handler, page) |
418d092a C |
677 | .then(o => { |
678 | return Object.assign(o, { | |
679 | type: 'Playlist' as 'Playlist', | |
680 | name: this.name, | |
681 | content: this.description, | |
3726c372 | 682 | mediaType: 'text/markdown' as 'text/markdown', |
418d092a | 683 | uuid: this.uuid, |
df0b219d C |
684 | published: this.createdAt.toISOString(), |
685 | updated: this.updatedAt.toISOString(), | |
418d092a | 686 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], |
e8bafea3 | 687 | icon |
418d092a C |
688 | }) |
689 | }) | |
690 | } | |
691 | } |