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'
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'
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[] = [
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 = {
}
type AvailableForListIDsOptions = {
+ serverAccountId: number
actorId: number
includeLocalVideos: boolean
filter?: VideoFilter
accountId?: number
videoChannelId?: number
trendingDays?: number
+ user?: UserModel
}
@Scopes({
}
]
},
+ 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
}
}
]
- },
- include: []
+ }
+
+ Object.assign(query.where, privacyWhere)
}
if (options.filter || options.accountId || options.videoChannelId) {
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: [
{
required: false
}
]
+ },
+ [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
+ return {
+ include: [
+ {
+ attributes: [ 'currentTime' ],
+ model: UserVideoHistoryModel.unscoped(),
+ required: false,
+ where: {
+ userId
+ }
+ }
+ ]
+ }
}
})
@Table({
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',
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" ' +
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<VideoModel> = {
offset: options.start,
limit: options.count,
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,
accountId: options.accountId,
videoChannelId: options.videoChannelId,
includeLocalVideos: options.includeLocalVideos,
+ user: options.user,
trendingDays
}
tagsAllOf?: string[]
durationMin?: number // seconds
durationMax?: number // seconds
+ user?: UserModel,
+ filter?: VideoFilter
}) {
const whereAnd = []
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)
.findOne(options)
}
- static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
+ static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
const query: IFindOptions<VideoModel> = {
where: {
url
- }
+ },
+ transaction
}
- if (t !== undefined) query.transaction = t
+ return VideoModel.findOne(query)
+ }
+
+ static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
+ const query: IFindOptions<VideoModel> = {
+ 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 = {
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)
}
// 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
}
}
private static buildActorWhereWithFilter (filter?: VideoFilter) {
- if (filter && filter === 'local') {
+ if (filter && (filter === 'local' || filter === 'all-local')) {
return {
serverId: null
}
return {}
}
- private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) {
+ private static async getAvailableForApi (
+ query: IFindOptions<VideoModel>,
+ options: AvailableForListIDsOptions,
+ countVideos = true
+ ) {
const idsScope = {
method: [
ScopeNames.AVAILABLE_FOR_LIST_IDS, options
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 = {
return getVideoFileResolution(originalFilePath)
}
- getDescriptionPath () {
+ getDescriptionAPIPath () {
return `/api/${API_VERSION}/videos/${this.uuid}/description`
}