From df0b219d36bf6852cdf2a7ad09ed4a41c6bccefa Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 5 Mar 2019 10:58:44 +0100 Subject: Add playlist rest tests --- server/models/video/video-channel.ts | 6 +- server/models/video/video-playlist-element.ts | 10 ++- server/models/video/video-playlist.ts | 92 ++++++++++++++++++++++----- server/models/video/video-share.ts | 2 +- server/models/video/video.ts | 38 +++++++---- 5 files changed, 109 insertions(+), 39 deletions(-) (limited to 'server/models/video') 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 = { ] }) @Scopes({ - [ScopeNames.SUMMARY]: (required: boolean, withAccount: boolean) => { + [ScopeNames.SUMMARY]: (withAccount = false) => { const base: IFindOptions = { - attributes: [ 'name', 'description', 'id' ], + attributes: [ 'name', 'description', 'id', 'actorId' ], include: [ { attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], @@ -225,7 +225,7 @@ export class VideoChannelModel extends Model { foreignKey: { allowNull: true }, - onDelete: 'cascade', + onDelete: 'CASCADE', hooks: true }) 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' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { CONSTRAINTS_FIELDS } from '../../initializers' import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' +import * as validator from 'validator' @Table({ tableName: 'videoPlaylistElement', @@ -34,10 +35,6 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object fields: [ 'videoPlaylistId', 'videoId' ], unique: true }, - { - fields: [ 'videoPlaylistId', 'position' ], - unique: true - }, { fields: [ 'url' ], unique: true @@ -143,7 +140,7 @@ export class VideoPlaylistElementModel extends Model return VideoPlaylistElementModel.findOne(query) } - static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number) { + static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Sequelize.Transaction) { const query = { attributes: [ 'url' ], offset: start, @@ -151,7 +148,8 @@ export class VideoPlaylistElementModel extends Model order: getSort('position'), where: { videoPlaylistId - } + }, + transaction: t } 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 { isVideoPlaylistPrivacyValid } from '../../helpers/custom-validators/video-playlists' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers' +import { + CONFIG, + CONSTRAINTS_FIELDS, + STATIC_PATHS, + THUMBNAILS_SIZE, + VIDEO_PLAYLIST_PRIVACIES, + VIDEO_PLAYLIST_TYPES +} from '../../initializers' import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' @@ -34,22 +41,25 @@ import { PlaylistObject } from '../../../shared/models/activitypub/objects/playl import { activityPubCollectionPagination } from '../../helpers/activitypub' import { remove } from 'fs-extra' import { logger } from '../../helpers/logger' +import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' enum ScopeNames { AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', - WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' + WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', + WITH_ACCOUNT = 'WITH_ACCOUNT' } type AvailableForListOptions = { followerActorId: number - accountId?: number, + type?: VideoPlaylistType + accountId?: number videoChannelId?: number privateAndUnlisted?: boolean } @Scopes({ - [ScopeNames.WITH_VIDEOS_LENGTH]: { + [ ScopeNames.WITH_VIDEOS_LENGTH ]: { attributes: { include: [ [ @@ -59,7 +69,15 @@ type AvailableForListOptions = { ] } }, - [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: { + [ ScopeNames.WITH_ACCOUNT ]: { + include: [ + { + model: () => AccountModel, + required: true + } + ] + }, + [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { include: [ { model: () => AccountModel.scope(AccountScopeNames.SUMMARY), @@ -71,7 +89,7 @@ type AvailableForListOptions = { } ] }, - [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { + [ 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 = { @@ -107,6 +125,12 @@ type AvailableForListOptions = { }) } + if (options.type) { + whereAnd.push({ + type: options.type + }) + } + const where = { [Sequelize.Op.and]: whereAnd } @@ -179,6 +203,11 @@ export class VideoPlaylistModel extends Model { @Column(DataType.UUID) uuid: string + @AllowNull(false) + @Default(VideoPlaylistType.REGULAR) + @Column + type: VideoPlaylistType + @ForeignKey(() => AccountModel) @Column ownerAccountId: number @@ -208,13 +237,10 @@ export class VideoPlaylistModel extends Model { name: 'videoPlaylistId', allowNull: false }, - onDelete: 'cascade' + onDelete: 'CASCADE' }) VideoPlaylistElements: VideoPlaylistElementModel[] - // Calculated field - videosLength?: number - @BeforeDestroy static async removeFiles (instance: VideoPlaylistModel) { logger.info('Removing files of video playlist %s.', instance.url) @@ -227,6 +253,7 @@ export class VideoPlaylistModel extends Model { start: number, count: number, sort: string, + type?: VideoPlaylistType, accountId?: number, videoChannelId?: number, privateAndUnlisted?: boolean @@ -242,6 +269,7 @@ export class VideoPlaylistModel extends Model { method: [ ScopeNames.AVAILABLE_FOR_LIST, { + type: options.type, followerActorId: options.followerActorId, accountId: options.accountId, videoChannelId: options.videoChannelId, @@ -289,7 +317,7 @@ export class VideoPlaylistModel extends Model { .then(e => !!e) } - static load (id: number | string, transaction: Sequelize.Transaction) { + static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) { const where = buildWhereIdOrUUID(id) const query = { @@ -298,14 +326,39 @@ export class VideoPlaylistModel extends Model { } return VideoPlaylistModel - .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ]) + .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ]) .findOne(query) } + static loadByUrlAndPopulateAccount (url: string) { + const query = { + where: { + url + } + } + + return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query) + } + static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' } + static getTypeLabel (type: VideoPlaylistType) { + return VIDEO_PLAYLIST_TYPES[type] || 'Unknown' + } + + static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) { + const query = { + where: { + videoChannelId + }, + transaction + } + + return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) + } + getThumbnailName () { const extension = '.jpg' @@ -345,7 +398,12 @@ export class VideoPlaylistModel extends Model { thumbnailPath: this.getThumbnailStaticPath(), - videosLength: this.videosLength, + type: { + id: this.type, + label: VideoPlaylistModel.getTypeLabel(this.type) + }, + + videosLength: this.get('videosLength'), createdAt: this.createdAt, updatedAt: this.updatedAt, @@ -355,18 +413,20 @@ export class VideoPlaylistModel extends Model { } } - toActivityPubObject (): Promise { + toActivityPubObject (page: number, t: Sequelize.Transaction): Promise { const handler = (start: number, count: number) => { - return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count) + return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) } - return activityPubCollectionPagination(this.url, handler, null) + return activityPubCollectionPagination(this.url, handler, page) .then(o => { return Object.assign(o, { type: 'Playlist' as 'Playlist', name: this.name, content: this.description, uuid: this.uuid, + published: this.createdAt.toISOString(), + updated: this.updatedAt.toISOString(), attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], icon: { 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 { .then(res => res.map(r => r.Actor)) } - static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction): Bluebird { + static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Sequelize.Transaction): Bluebird { const query = { attributes: [], 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 = { }, include: [ { - model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY) + model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }) } ] } @@ -1535,18 +1535,7 @@ export class VideoModel extends Model { if (ids.length === 0) return { data: [], total: count } - // FIXME: typings - const apiScope: any[] = [ - { - method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] - } - ] - - if (options.user) { - apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) - } - - const secondQuery = { + const secondQuery: IFindOptions = { offset: 0, limit: query.limit, attributes: query.attributes, @@ -1556,6 +1545,29 @@ export class VideoModel extends Model { ) ] } + + // FIXME: typing + const apiScope: any[] = [] + + if (options.user) { + apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) + + // Even if the relation is n:m, we know that a user only have 0..1 video history + // So we won't have multiple rows for the same video + // A subquery adds some bugs in our query so disable it + secondQuery.subQuery = false + } + + apiScope.push({ + method: [ + ScopeNames.FOR_API, { + ids, withFiles: + options.withFiles, + videoPlaylistId: options.videoPlaylistId + } as ForAPIOptions + ] + }) + const rows = await VideoModel.scope(apiScope).findAll(secondQuery) return { -- cgit v1.2.3