X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-playlist.ts;h=9f9e0b069a03197a8fd8ee06fa007121a1357d64;hb=2650d6d489f775a38c5c3fdb65daabc7d55c15b5;hp=3e436acfc23a1bee1007a14d44ee3030557ec782;hpb=1735c825726edaa0af5035cb6cbb0cc0db502c6d;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 3e436acfc..9f9e0b069 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -1,3 +1,6 @@ +import * as Bluebird from 'bluebird' +import { join } from 'path' +import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' import { AllowNull, BelongsTo, @@ -15,14 +18,19 @@ import { Table, UpdatedAt } from 'sequelize-typescript' +import { MAccountId, MChannelId } from '@server/types/models' +import { ActivityIconObject } from '../../../shared/models/activitypub/objects' +import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' -import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils' +import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' +import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' +import { activityPubCollectionPagination } from '../../helpers/activitypub' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isVideoPlaylistDescriptionValid, isVideoPlaylistNameValid, isVideoPlaylistPrivacyValid } from '../../helpers/custom-validators/video-playlists' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { ACTIVITY_PUB, CONSTRAINTS_FIELDS, @@ -32,17 +40,20 @@ import { VIDEO_PLAYLIST_TYPES, WEBSERVER } from '../../initializers/constants' -import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' -import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' +import { MThumbnail } from '../../types/models/video/thumbnail' +import { + MVideoPlaylistAccountThumbnail, + MVideoPlaylistAP, + MVideoPlaylistFormattable, + MVideoPlaylistFull, + MVideoPlaylistFullSummary, + MVideoPlaylistIdWithElements +} from '../../types/models/video/video-playlist' +import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' +import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' +import { ThumbnailModel } from './thumbnail' import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' -import { join } from 'path' import { VideoPlaylistElementModel } from './video-playlist-element' -import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' -import { activityPubCollectionPagination } from '../../helpers/activitypub' -import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' -import { ThumbnailModel } from './thumbnail' -import { ActivityIconObject } from '../../../shared/models/activitypub/objects' -import { fn, literal, Op, Transaction } from 'sequelize' enum ScopeNames { AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', @@ -58,86 +69,87 @@ type AvailableForListOptions = { type?: VideoPlaylistType accountId?: number videoChannelId?: number - privateAndUnlisted?: boolean + listMyPlaylists?: boolean + search?: string } -@Scopes({ - [ ScopeNames.WITH_THUMBNAIL ]: { +@Scopes(() => ({ + [ScopeNames.WITH_THUMBNAIL]: { include: [ { - model: () => ThumbnailModel, + model: ThumbnailModel, required: false } ] }, - [ ScopeNames.WITH_VIDEOS_LENGTH ]: { + [ScopeNames.WITH_VIDEOS_LENGTH]: { attributes: { include: [ - [ - fn('COUNT', 'toto'), - 'coucou' - ], [ literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), 'videosLength' ] ] } - }, - [ ScopeNames.WITH_ACCOUNT ]: { + } as FindOptions, + [ScopeNames.WITH_ACCOUNT]: { include: [ { - model: () => AccountModel, + model: AccountModel, required: true } ] }, - [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { + [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: { include: [ { - model: () => AccountModel.scope(AccountScopeNames.SUMMARY), + model: AccountModel.scope(AccountScopeNames.SUMMARY), required: true }, { - model: () => VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), + model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), required: false } ] }, - [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: { + [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: { include: [ { - model: () => AccountModel, + model: AccountModel, required: true }, { - model: () => VideoChannelModel, + model: VideoChannelModel, required: false } ] }, - [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { - // Only list local playlists OR playlists that are on an instance followed by actorId - const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) - const actorWhere = { - [ Op.or ]: [ - { - serverId: null - }, - { - serverId: { - [ Op.in ]: literal(inQueryInstanceFollow) - } - } - ] - } + [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { - const whereAnd: any[] = [] + let whereActor: WhereOptions = {} - if (options.privateAndUnlisted !== true) { + const whereAnd: WhereOptions[] = [] + + if (options.listMyPlaylists !== true) { whereAnd.push({ privacy: VideoPlaylistPrivacy.PUBLIC }) + + // Only list local playlists OR playlists that are on an instance followed by actorId + const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) + + whereActor = { + [Op.or]: [ + { + serverId: null + }, + { + serverId: { + [Op.in]: literal(inQueryInstanceFollow) + } + } + ] + } } if (options.accountId) { @@ -158,12 +170,20 @@ type AvailableForListOptions = { }) } + if (options.search) { + whereAnd.push({ + name: { + [Op.iLike]: '%' + options.search + '%' + } + }) + } + const where = { [Op.and]: whereAnd } const accountScope = { - method: [ AccountScopeNames.SUMMARY, actorWhere ] + method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] } return { @@ -178,9 +198,9 @@ type AvailableForListOptions = { required: false } ] - } + } as FindOptions } -}) +})) @Table({ tableName: 'videoPlaylist', @@ -211,7 +231,7 @@ export class VideoPlaylistModel extends Model { @AllowNull(true) @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true)) - @Column + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.DESCRIPTION.max)) description: string @AllowNull(false) @@ -280,21 +300,22 @@ export class VideoPlaylistModel extends Model { static listForApi (options: { followerActorId: number - start: number, - count: number, - sort: string, - type?: VideoPlaylistType, - accountId?: number, - videoChannelId?: number, - privateAndUnlisted?: boolean + start: number + count: number + sort: string + type?: VideoPlaylistType + accountId?: number + videoChannelId?: number + listMyPlaylists?: boolean + search?: string }) { const query = { offset: options.start, limit: options.count, - order: getSort(options.sort) + order: getPlaylistSort(options.sort) } - const scopes = [ + const scopes: (string | ScopeOptions)[] = [ { method: [ ScopeNames.AVAILABLE_FOR_LIST, @@ -303,10 +324,11 @@ export class VideoPlaylistModel extends Model { followerActorId: options.followerActorId, accountId: options.accountId, videoChannelId: options.videoChannelId, - privateAndUnlisted: options.privateAndUnlisted + listMyPlaylists: options.listMyPlaylists, + search: options.search } as AvailableForListOptions ] - } as any, // FIXME: typings + }, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ] @@ -319,15 +341,24 @@ export class VideoPlaylistModel extends Model { }) } - static listPublicUrlsOfForAP (accountId: number, start: number, count: number) { + static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { + const where = { + privacy: VideoPlaylistPrivacy.PUBLIC + } + + if (options.account) { + Object.assign(where, { ownerAccountId: options.account.id }) + } + + if (options.channel) { + Object.assign(where, { videoChannelId: options.channel.id }) + } + const query = { attributes: [ 'url' ], offset: start, limit: count, - where: { - ownerAccountId: accountId, - privacy: VideoPlaylistPrivacy.PUBLIC - } + where } return VideoPlaylistModel.findAndCountAll(query) @@ -336,7 +367,7 @@ export class VideoPlaylistModel extends Model { }) } - static listPlaylistIdsOf (accountId: number, videoIds: number[]) { + static listPlaylistIdsOf (accountId: number, videoIds: number[]): Bluebird { const query = { attributes: [ 'id' ], where: { @@ -344,11 +375,11 @@ export class VideoPlaylistModel extends Model { }, include: [ { - attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ], + attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ], model: VideoPlaylistElementModel.unscoped(), where: { videoId: { - [Op.any]: videoIds + [Op.in]: videoIds } }, required: true @@ -372,7 +403,7 @@ export class VideoPlaylistModel extends Model { .then(e => !!e) } - static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction) { + static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction): Bluebird { const where = buildWhereIdOrUUID(id) const query = { @@ -385,7 +416,7 @@ export class VideoPlaylistModel extends Model { .findOne(query) } - static loadWithAccountAndChannel (id: number | string, transaction: Transaction) { + static loadWithAccountAndChannel (id: number | string, transaction: Transaction): Bluebird { const where = buildWhereIdOrUUID(id) const query = { @@ -398,7 +429,7 @@ export class VideoPlaylistModel extends Model { .findOne(query) } - static loadByUrlAndPopulateAccount (url: string) { + static loadByUrlAndPopulateAccount (url: string): Bluebird { const query = { where: { url @@ -427,18 +458,20 @@ export class VideoPlaylistModel extends Model { return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) } - setThumbnail (thumbnail: ThumbnailModel) { - this.Thumbnail = thumbnail - } + async setAndSaveThumbnail (thumbnail: MThumbnail, t: Transaction) { + thumbnail.videoPlaylistId = this.id - getThumbnail () { - return this.Thumbnail + this.Thumbnail = await thumbnail.save({ transaction: t }) } hasThumbnail () { return !!this.Thumbnail } + hasGeneratedThumbnail () { + return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true + } + generateThumbnailName () { const extension = '.jpg' @@ -448,13 +481,21 @@ export class VideoPlaylistModel extends Model { getThumbnailUrl () { if (!this.hasThumbnail()) return null - return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename + return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename } getThumbnailStaticPath () { if (!this.hasThumbnail()) return null - return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename) + return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) + } + + getWatchUrl () { + return WEBSERVER.URL + '/videos/watch/playlist/' + this.uuid + } + + getEmbedStaticPath () { + return '/video-playlists/embed/' + this.uuid } setAsRefreshed () { @@ -473,7 +514,7 @@ export class VideoPlaylistModel extends Model { return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL) } - toFormattedJSON (): VideoPlaylist { + toFormattedJSON (this: MVideoPlaylistFormattable): VideoPlaylist { return { id: this.id, uuid: this.uuid, @@ -487,6 +528,7 @@ export class VideoPlaylistModel extends Model { }, thumbnailPath: this.getThumbnailStaticPath(), + embedPath: this.getEmbedStaticPath(), type: { id: this.type, @@ -499,11 +541,13 @@ export class VideoPlaylistModel extends Model { updatedAt: this.updatedAt, ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(), - videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null + videoChannel: this.VideoChannel + ? this.VideoChannel.toFormattedSummaryJSON() + : null } } - toActivityPubObject (page: number, t: Transaction): Promise { + toActivityPubObject (this: MVideoPlaylistAP, page: number, t: Transaction): Promise { const handler = (start: number, count: number) => { return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) }