X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=6c183933b165452829000d5a65c6dd6dfa5678af;hb=6d8c8ea73a774c3568e6d28a4cbebcf7979d5c2a;hp=c7cd2890ca593f123408be4e0795a093c80acef1;hpb=627621c1e8d37c33f7b3dd59f4c8907b12c630bc;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index c7cd2890c..6c183933b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -27,7 +27,7 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { VideoPrivacy, VideoState } from '../../../shared' +import { UserRight, VideoPrivacy, VideoState } from '../../../shared' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' import { VideoFilter } from '../../../shared/models/videos/video-query.type' @@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate' import { ActorModel } from '../activitypub/actor' import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' -import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' +import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' import { TagModel } from './tag' import { VideoAbuseModel } from './video-abuse' import { VideoChannelModel } from './video-channel' @@ -92,6 +92,8 @@ import { videoModelToFormattedJSON } from './video-format-utils' import * as validator from 'validator' +import { UserVideoHistoryModel } from '../account/user-video-history' +import { UserModel } from '../account/user' // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation const indexes: Sequelize.DefineIndexesOptions[] = [ @@ -127,7 +129,8 @@ export enum ScopeNames { WITH_TAGS = 'WITH_TAGS', WITH_FILES = 'WITH_FILES', WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', - WITH_BLACKLISTED = 'WITH_BLACKLISTED' + WITH_BLACKLISTED = 'WITH_BLACKLISTED', + WITH_USER_HISTORY = 'WITH_USER_HISTORY' } type ForAPIOptions = { @@ -136,6 +139,7 @@ type ForAPIOptions = { } type AvailableForListIDsOptions = { + serverAccountId: number actorId: number includeLocalVideos: boolean filter?: VideoFilter @@ -149,6 +153,7 @@ type AvailableForListIDsOptions = { accountId?: number videoChannelId?: number trendingDays?: number + user?: UserModel } @Scopes({ @@ -234,6 +239,22 @@ type AvailableForListIDsOptions = { } ] }, + channelId: { + [ Sequelize.Op.notIn ]: Sequelize.literal( + '(' + + 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + + buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + + ')' + + ')' + ) + } + }, + include: [] + } + + // Only list public/published videos + if (!options.filter || options.filter !== 'all-local') { + const privacyWhere = { // Always list public videos privacy: VideoPrivacy.PUBLIC, // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding @@ -248,8 +269,9 @@ type AvailableForListIDsOptions = { } } ] - }, - include: [] + } + + Object.assign(query.where, privacyWhere) } if (options.filter || options.accountId || options.videoChannelId) { @@ -464,6 +486,8 @@ type AvailableForListIDsOptions = { include: [ { model: () => VideoFileModel.unscoped(), + // FIXME: typings + [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join required: false, include: [ { @@ -482,6 +506,20 @@ type AvailableForListIDsOptions = { required: false } ] + }, + [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { + return { + include: [ + { + attributes: [ 'currentTime' ], + model: UserVideoHistoryModel.unscoped(), + required: false, + where: { + userId + } + } + ] + } } }) @Table({ @@ -672,11 +710,19 @@ export class VideoModel extends Model { name: 'videoId', allowNull: false }, - onDelete: 'cascade', - hooks: true + onDelete: 'cascade' }) VideoViews: VideoViewModel[] + @HasMany(() => UserVideoHistoryModel, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) + UserVideoHistories: UserVideoHistoryModel[] + @HasOne(() => ScheduleVideoUpdateModel, { foreignKey: { name: 'videoId', @@ -762,6 +808,16 @@ export class VideoModel extends Model { return VideoModel.scope(ScopeNames.WITH_FILES).findAll() } + static listLocal () { + const query = { + where: { + remote: false + } + } + + return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) + } + static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { function getRawQuery (select: string) { const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + @@ -930,8 +986,13 @@ export class VideoModel extends Model { accountId?: number, videoChannelId?: number, actorId?: number - trendingDays?: number + trendingDays?: number, + user?: UserModel }, countVideos = true) { + if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { + throw new Error('Try to filter all-local but no user has not the see all videos right') + } + const query: IFindOptions = { offset: options.start, limit: options.count, @@ -945,11 +1006,14 @@ export class VideoModel extends Model { query.group = 'VideoModel.id' } + const serverActor = await getServerActor() + // actorId === null has a meaning, so just check undefined - const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id + const actorId = options.actorId !== undefined ? options.actorId : serverActor.id const queryOptions = { actorId, + serverAccountId: serverActor.Account.id, nsfw: options.nsfw, categoryOneOf: options.categoryOneOf, licenceOneOf: options.licenceOneOf, @@ -961,6 +1025,7 @@ export class VideoModel extends Model { accountId: options.accountId, videoChannelId: options.videoChannelId, includeLocalVideos: options.includeLocalVideos, + user: options.user, trendingDays } @@ -983,6 +1048,8 @@ export class VideoModel extends Model { tagsAllOf?: string[] durationMin?: number // seconds durationMax?: number // seconds + user?: UserModel, + filter?: VideoFilter }) { const whereAnd = [] @@ -1052,13 +1119,16 @@ export class VideoModel extends Model { const serverActor = await getServerActor() const queryOptions = { actorId: serverActor.id, + serverAccountId: serverActor.Account.id, includeLocalVideos: options.includeLocalVideos, nsfw: options.nsfw, categoryOneOf: options.categoryOneOf, licenceOneOf: options.licenceOneOf, languageOneOf: options.languageOneOf, tagsOneOf: options.tagsOneOf, - tagsAllOf: options.tagsAllOf + tagsAllOf: options.tagsAllOf, + user: options.user, + filter: options.filter } return VideoModel.getAvailableForApi(query, queryOptions) @@ -1103,19 +1173,29 @@ export class VideoModel extends Model { .findOne(options) } - static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { + static loadByUrl (url: string, transaction?: Sequelize.Transaction) { const query: IFindOptions = { where: { url - } + }, + transaction } - if (t !== undefined) query.transaction = t + return VideoModel.findOne(query) + } + + static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + }, + transaction + } return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) } - static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { + static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { const where = VideoModel.buildWhereIdOrUUID(id) const options = { @@ -1124,14 +1204,20 @@ export class VideoModel extends Model { transaction: t } + const scopes = [ + ScopeNames.WITH_TAGS, + ScopeNames.WITH_BLACKLISTED, + ScopeNames.WITH_FILES, + ScopeNames.WITH_ACCOUNT_DETAILS, + ScopeNames.WITH_SCHEDULED_UPDATE + ] + + if (userId) { + scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings + } + return VideoModel - .scope([ - ScopeNames.WITH_TAGS, - ScopeNames.WITH_BLACKLISTED, - ScopeNames.WITH_FILES, - ScopeNames.WITH_ACCOUNT_DETAILS, - ScopeNames.WITH_SCHEDULED_UPDATE - ]) + .scope(scopes) .findOne(options) } @@ -1169,9 +1255,11 @@ export class VideoModel extends Model { // threshold corresponds to how many video the field should have to be returned static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { - const actorId = (await getServerActor()).id + const serverActor = await getServerActor() + const actorId = serverActor.id - const scopeOptions = { + const scopeOptions: AvailableForListIDsOptions = { + serverAccountId: serverActor.Account.id, actorId, includeLocalVideos: true } @@ -1206,7 +1294,7 @@ export class VideoModel extends Model { } private static buildActorWhereWithFilter (filter?: VideoFilter) { - if (filter && filter === 'local') { + if (filter && (filter === 'local' || filter === 'all-local')) { return { serverId: null } @@ -1215,7 +1303,11 @@ export class VideoModel extends Model { return {} } - private static async getAvailableForApi (query: IFindOptions, options: AvailableForListIDsOptions, countVideos = true) { + private static async getAvailableForApi ( + query: IFindOptions, + options: AvailableForListIDsOptions, + countVideos = true + ) { const idsScope = { method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, options @@ -1239,8 +1331,15 @@ export class VideoModel extends Model { if (ids.length === 0) return { data: [], total: count } - const apiScope = { - method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] + // 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 = { @@ -1406,7 +1505,7 @@ export class VideoModel extends Model { return getVideoFileResolution(originalFilePath) } - getDescriptionPath () { + getDescriptionAPIPath () { return `/api/${API_VERSION}/videos/${this.uuid}/description` }