+ static async listForApi (options: {
+ start: number,
+ count: number,
+ sort: string,
+ nsfw: boolean,
+ includeLocalVideos: boolean,
+ withFiles: boolean,
+ categoryOneOf?: number[],
+ licenceOneOf?: number[],
+ languageOneOf?: string[],
+ tagsOneOf?: string[],
+ tagsAllOf?: string[],
+ filter?: VideoFilter,
+ accountId?: number,
+ videoChannelId?: number,
+ actorId?: number
+ }) {
+ const query = {
+ offset: options.start,
+ limit: options.count,
+ order: getSort(options.sort)
+ }
+
+ // actorId === null has a meaning, so just check undefined
+ const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id
+
+ const scopes = {
+ method: [
+ ScopeNames.AVAILABLE_FOR_LIST, {
+ actorId,
+ nsfw: options.nsfw,
+ categoryOneOf: options.categoryOneOf,
+ licenceOneOf: options.licenceOneOf,
+ languageOneOf: options.languageOneOf,
+ tagsOneOf: options.tagsOneOf,
+ tagsAllOf: options.tagsAllOf,
+ filter: options.filter,
+ withFiles: options.withFiles,
+ accountId: options.accountId,
+ videoChannelId: options.videoChannelId,
+ includeLocalVideos: options.includeLocalVideos
+ } as AvailableForListOptions
+ ]
+ }
+
+ return VideoModel.scope(scopes)
+ .findAndCountAll(query)
+ .then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
+ }
+
+ static async searchAndPopulateAccountAndServer (options: {
+ includeLocalVideos: boolean
+ search?: string
+ start?: number
+ count?: number
+ sort?: string
+ startDate?: string // ISO 8601
+ endDate?: string // ISO 8601
+ nsfw?: boolean
+ categoryOneOf?: number[]
+ licenceOneOf?: number[]
+ languageOneOf?: string[]
+ tagsOneOf?: string[]
+ tagsAllOf?: string[]
+ durationMin?: number // seconds
+ durationMax?: number // seconds
+ }) {
+ const whereAnd = [ ]
+
+ if (options.startDate || options.endDate) {
+ const publishedAtRange = { }
+
+ if (options.startDate) publishedAtRange[Sequelize.Op.gte] = options.startDate
+ if (options.endDate) publishedAtRange[Sequelize.Op.lte] = options.endDate
+
+ whereAnd.push({ publishedAt: publishedAtRange })
+ }
+
+ if (options.durationMin || options.durationMax) {
+ const durationRange = { }
+
+ if (options.durationMin) durationRange[Sequelize.Op.gte] = options.durationMin
+ if (options.durationMax) durationRange[Sequelize.Op.lte] = options.durationMax
+
+ whereAnd.push({ duration: durationRange })
+ }
+
+ const attributesInclude = []
+ const escapedSearch = VideoModel.sequelize.escape(options.search)
+ const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
+ if (options.search) {
+ whereAnd.push(
+ {
+ id: {
+ [ Sequelize.Op.in ]: Sequelize.literal(
+ '(' +
+ 'SELECT "video"."id" FROM "video" ' +
+ 'WHERE ' +
+ 'lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
+ 'lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
+ 'UNION ALL ' +
+ 'SELECT "video"."id" FROM "video" LEFT JOIN "videoTag" ON "videoTag"."videoId" = "video"."id" ' +
+ 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
+ 'WHERE "tag"."name" = ' + escapedSearch +
+ ')'
+ )
+ }
+ }
+ )
+
+ attributesInclude.push(createSimilarityAttribute('VideoModel.name', options.search))
+ }
+
+ // Cannot search on similarity if we don't have a search
+ if (!options.search) {
+ attributesInclude.push(
+ Sequelize.literal('0 as similarity')
+ )
+ }
+
+ const query: IFindOptions<VideoModel> = {
+ attributes: {
+ include: attributesInclude
+ },
+ offset: options.start,
+ limit: options.count,
+ order: getSort(options.sort),
+ where: {
+ [ Sequelize.Op.and ]: whereAnd
+ }
+ }
+
+ const serverActor = await getServerActor()
+ const scopes = {
+ method: [
+ ScopeNames.AVAILABLE_FOR_LIST, {
+ actorId: serverActor.id,
+ includeLocalVideos: options.includeLocalVideos,
+ nsfw: options.nsfw,
+ categoryOneOf: options.categoryOneOf,
+ licenceOneOf: options.licenceOneOf,
+ languageOneOf: options.languageOneOf,
+ tagsOneOf: options.tagsOneOf,
+ tagsAllOf: options.tagsAllOf
+ } as AvailableForListOptions
+ ]
+ }
+
+ return VideoModel.scope(scopes)
+ .findAndCountAll(query)
+ .then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
+ }
+
+ static load (id: number, t?: Sequelize.Transaction) {
+ const options = t ? { transaction: t } : undefined
+
+ return VideoModel.findById(id, options)
+ }
+
+ static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
+ const query: IFindOptions<VideoModel> = {
+ where: {
+ url
+ }
+ }
+
+ if (t !== undefined) query.transaction = t
+
+ return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
+ }
+
+ static loadAndPopulateAccountAndServerAndTags (id: number) {
+ const options = {
+ order: [ [ 'Tags', 'name', 'ASC' ] ]
+ }
+
+ return VideoModel
+ .scope([
+ ScopeNames.WITH_TAGS,
+ ScopeNames.WITH_BLACKLISTED,
+ ScopeNames.WITH_FILES,
+ ScopeNames.WITH_ACCOUNT_DETAILS,
+ ScopeNames.WITH_SCHEDULED_UPDATE
+ ])
+ .findById(id, options)
+ }
+
+ static loadByUUID (uuid: string) {
+ const options = {
+ where: {
+ uuid
+ }
+ }
+
+ return VideoModel
+ .scope([ ScopeNames.WITH_FILES ])
+ .findOne(options)
+ }
+
+ static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
+ const options = {
+ order: [ [ 'Tags', 'name', 'ASC' ] ],
+ where: {
+ uuid
+ },
+ transaction: t
+ }
+
+ return VideoModel
+ .scope([
+ ScopeNames.WITH_TAGS,
+ ScopeNames.WITH_BLACKLISTED,
+ ScopeNames.WITH_FILES,
+ ScopeNames.WITH_ACCOUNT_DETAILS,
+ ScopeNames.WITH_SCHEDULED_UPDATE
+ ])
+ .findOne(options)
+ }
+
+ static async getStats () {
+ const totalLocalVideos = await VideoModel.count({
+ where: {
+ remote: false
+ }