X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-playlist.ts;h=00cca054981db54e435fb12f90dace0df39fbf38;hb=2b621ac0ebe83693bba6354b3482a03ba58143e7;hp=98cea1b64f21fabfeadd580b44062f6b33b04690;hpb=a1eda903a497857017495f37a1fd3593ba7ab23c;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 98cea1b64..00cca0549 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -1,5 +1,5 @@ import { join } from 'path' -import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' +import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' import { AllowNull, BelongsTo, @@ -17,15 +17,16 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { v4 as uuidv4 } from 'uuid' +import { activityPubCollectionPagination } from '@server/lib/activitypub/collection' import { MAccountId, MChannelId } from '@server/types/models' -import { AttributesOnly } from '@shared/core-utils' +import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' +import { buildUUID, uuidToShort } from '@shared/extra-utils' +import { AttributesOnly } from '@shared/typescript-utils' 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 { 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, @@ -52,7 +53,16 @@ import { } from '../../types/models/video/video-playlist' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' import { ActorModel } from '../actor/actor' -import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' +import { setAsUpdated } from '../shared' +import { + buildServerIdsFollowedBy, + buildTrigramSearchIndex, + buildWhereIdOrUUID, + createSimilarityAttribute, + getPlaylistSort, + isOutdated, + throwIfNotValid +} from '../utils' import { ThumbnailModel } from './thumbnail' import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' import { VideoPlaylistElementModel } from './video-playlist-element' @@ -73,6 +83,14 @@ type AvailableForListOptions = { videoChannelId?: number listMyPlaylists?: boolean search?: string + host?: string + uuids?: string[] + withVideos?: boolean + forCount?: boolean +} + +function getVideoLengthSelect () { + return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"' } @Scopes(() => ({ @@ -88,7 +106,7 @@ type AvailableForListOptions = { attributes: { include: [ [ - literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), + literal(`(${getVideoLengthSelect()})`), 'videosLength' ] ] @@ -127,9 +145,19 @@ type AvailableForListOptions = { ] }, [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { + const whereAnd: WhereOptions[] = [] + + const whereServer = options.host && options.host !== WEBSERVER.HOST + ? { host: options.host } + : undefined + let whereActor: WhereOptions = {} - const whereAnd: WhereOptions[] = [] + if (options.host === WEBSERVER.HOST) { + whereActor = { + [Op.and]: [ { serverId: null } ] + } + } if (options.listMyPlaylists !== true) { whereAnd.push({ @@ -154,9 +182,7 @@ type AvailableForListOptions = { }) } - whereActor = { - [Op.or]: whereActorOr - } + Object.assign(whereActor, { [Op.or]: whereActorOr }) } if (options.accountId) { @@ -177,32 +203,65 @@ type AvailableForListOptions = { }) } - if (options.search) { + if (options.uuids) { whereAnd.push({ - name: { - [Op.iLike]: '%' + options.search + '%' + uuid: { + [Op.in]: options.uuids } }) } + if (options.withVideos === true) { + whereAnd.push( + literal(`(${getVideoLengthSelect()}) != 0`) + ) + } + + let attributesInclude: any[] = [ literal('0 as similarity') ] + + if (options.search) { + const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) + const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') + attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ] + + whereAnd.push({ + [Op.or]: [ + Sequelize.literal( + 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' + ), + Sequelize.literal( + 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + ) + ] + }) + } + const where = { [Op.and]: whereAnd } + const include: Includeable[] = [ + { + model: AccountModel.scope({ + method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ] + }), + required: true + } + ] + + if (options.forCount !== true) { + include.push({ + model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), + required: false + }) + } + return { + attributes: { + include: attributesInclude + }, where, - include: [ - { - model: AccountModel.scope({ - method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] - }), - required: true - }, - { - model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), - required: false - } - ] + include } as FindOptions } })) @@ -210,6 +269,8 @@ type AvailableForListOptions = { @Table({ tableName: 'videoPlaylist', indexes: [ + buildTrigramSearchIndex('video_playlist_name_trigram', 'name'), + { fields: [ 'ownerAccountId' ] }, @@ -303,16 +364,10 @@ export class VideoPlaylistModel extends Model { - return { total: count, data: rows } - }) + const scopesCount: (string | ScopeOptions)[] = [ + { + method: [ + ScopeNames.AVAILABLE_FOR_LIST, + + { + ...commonAvailableForListOptions, + + withVideos: options.withVideos || false, + forCount: true + } as AvailableForListOptions + ] + }, + ScopeNames.WITH_VIDEOS_LENGTH + ] + + return Promise.all([ + VideoPlaylistModel.scope(scopesCount).count(), + VideoPlaylistModel.scope(scopesFind).findAll(query) + ]).then(([ count, rows ]) => ({ total: count, data: rows })) + } + + static searchForApi (options: Pick & { + start: number + count: number + sort: string + }) { + return VideoPlaylistModel.listForApi({ + ...options, + + type: VideoPlaylistType.REGULAR, + listMyPlaylists: false, + withVideos: true + }) } static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { @@ -359,17 +450,24 @@ export class VideoPlaylistModel extends Model { + return { + attributes: forCount === true + ? [] + : [ 'url' ], + offset: start, + limit: count, + where + } } - return VideoPlaylistModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { total: count, data: rows.map(p => p.url) } - }) + return Promise.all([ + VideoPlaylistModel.count(getQuery(true)), + VideoPlaylistModel.findAll(getQuery(false)) + ]).then(([ total, rows ]) => ({ + total, + data: rows.map(p => p.url) + })) } static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise { @@ -444,6 +542,18 @@ export class VideoPlaylistModel extends Model { + const query = { + where: { + url + } + } + + return VideoPlaylistModel + .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) + .findOne(query) + } + static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' } @@ -480,7 +590,7 @@ export class VideoPlaylistModel extends Model