X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=3b30e9e283bf0fdb8e569506f5cbd6b733abbeab;hb=6dd9de95dfa39bd5c1faed00d1dbd52cd112bae0;hp=7022607728b105013c04abbe8e40758004d70a7a;hpb=092092969633bbcf6d4891a083ea497a7d5c3154;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 702260772..3b30e9e28 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -51,8 +51,9 @@ import { getServerActor } from '../../helpers/utils' import { ACTIVITY_PUB, API_VERSION, - CONFIG, - CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY, + CONSTRAINTS_FIELDS, + HLS_REDUNDANCY_DIRECTORY, + HLS_STREAMING_PLAYLIST_DIRECTORY, PREVIEWS_SIZE, REMOTE_SCHEME, STATIC_DOWNLOAD_PATHS, @@ -62,7 +63,8 @@ import { VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, - VIDEO_STATES + VIDEO_STATES, + WEBSERVER } from '../../initializers' import { sendDeleteVideo } from '../../lib/activitypub/send' import { AccountModel } from '../account/account' @@ -70,10 +72,18 @@ import { AccountVideoRateModel } from '../account/account-video-rate' import { ActorModel } from '../activitypub/actor' import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' -import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' +import { + buildBlockedAccountSQL, + buildTrigramSearchIndex, + buildWhereIdOrUUID, + createSimilarityAttribute, + getVideoSort, + isOutdated, + throwIfNotValid +} from '../utils' import { TagModel } from './tag' import { VideoAbuseModel } from './video-abuse' -import { VideoChannelModel } from './video-channel' +import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' import { VideoCommentModel } from './video-comment' import { VideoFileModel } from './video-file' import { VideoShareModel } from './video-share' @@ -91,11 +101,12 @@ import { videoModelToFormattedDetailsJSON, videoModelToFormattedJSON } from './video-format-utils' -import * as validator from 'validator' import { UserVideoHistoryModel } from '../account/user-video-history' import { UserModel } from '../account/user' import { VideoImportModel } from './video-import' import { VideoStreamingPlaylistModel } from './video-streaming-playlist' +import { VideoPlaylistElementModel } from './video-playlist-element' +import { CONFIG } from '../../initializers/config' // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation const indexes: Sequelize.DefineIndexesOptions[] = [ @@ -106,6 +117,14 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ { fields: [ 'duration' ] }, { fields: [ 'views' ] }, { fields: [ 'channelId' ] }, + { + fields: [ 'originallyPublishedAt' ], + where: { + originallyPublishedAt: { + [Sequelize.Op.ne]: null + } + } + }, { fields: [ 'category' ], // We don't care videos with an unknown category where: { @@ -167,6 +186,9 @@ export enum ScopeNames { type ForAPIOptions = { ids: number[] + + videoPlaylistId?: number + withFiles?: boolean } @@ -174,6 +196,7 @@ type AvailableForListIDsOptions = { serverAccountId: number followerActorId: number includeLocalVideos: boolean + filter?: VideoFilter categoryOneOf?: number[] nsfw?: boolean @@ -181,9 +204,14 @@ type AvailableForListIDsOptions = { languageOneOf?: string[] tagsOneOf?: string[] tagsAllOf?: string[] + withFiles?: boolean + accountId?: number videoChannelId?: number + + videoPlaylistId?: number + trendingDays?: number user?: UserModel, historyOfUser?: UserModel @@ -191,62 +219,18 @@ type AvailableForListIDsOptions = { @Scopes({ [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { - const accountInclude = { - attributes: [ 'id', 'name' ], - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id', 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], - model: ActorModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - }, - { - model: AvatarModel.unscoped(), - required: false - } - ] - } - ] - } - - const videoChannelInclude = { - attributes: [ 'name', 'description', 'id' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], - model: ActorModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - }, - { - model: AvatarModel.unscoped(), - required: false - } - ] - }, - accountInclude - ] - } - const query: IFindOptions = { where: { id: { [ Sequelize.Op.any ]: options.ids } }, - include: [ videoChannelInclude ] + include: [ + { + model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), + required: true + } + ] } if (options.withFiles === true) { @@ -256,6 +240,16 @@ type AvailableForListIDsOptions = { }) } + if (options.videoPlaylistId) { + query.include.push({ + model: VideoPlaylistElementModel.unscoped(), + required: true, + where: { + videoPlaylistId: options.videoPlaylistId + } + }) + } + return query }, [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { @@ -307,6 +301,19 @@ type AvailableForListIDsOptions = { Object.assign(query.where, privacyWhere) } + if (options.videoPlaylistId) { + query.include.push({ + attributes: [], + model: VideoPlaylistElementModel.unscoped(), + required: true, + where: { + videoPlaylistId: options.videoPlaylistId + } + }) + + query.subQuery = false + } + if (options.filter || options.accountId || options.videoChannelId) { const videoChannelInclude: IIncludeOptions = { attributes: [], @@ -715,6 +722,10 @@ export class VideoModel extends Model { @Column commentsEnabled: boolean + @AllowNull(false) + @Column + downloadEnabled: boolean + @AllowNull(false) @Column waitTranscoding: boolean @@ -736,6 +747,11 @@ export class VideoModel extends Model { @Column publishedAt: Date + @AllowNull(true) + @Default(null) + @Column + originallyPublishedAt: Date + @ForeignKey(() => VideoChannelModel) @Column channelId: number @@ -755,6 +771,15 @@ export class VideoModel extends Model { }) Tags: TagModel[] + @HasMany(() => VideoPlaylistElementModel, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) + VideoPlaylistElements: VideoPlaylistElementModel[] + @HasMany(() => VideoAbuseModel, { foreignKey: { name: 'videoId', @@ -1101,6 +1126,7 @@ export class VideoModel extends Model { accountId?: number, videoChannelId?: number, followerActorId?: number + videoPlaylistId?: number, trendingDays?: number, user?: UserModel, historyOfUser?: UserModel @@ -1140,6 +1166,7 @@ export class VideoModel extends Model { withFiles: options.withFiles, accountId: options.accountId, videoChannelId: options.videoChannelId, + videoPlaylistId: options.videoPlaylistId, includeLocalVideos: options.includeLocalVideos, user: options.user, historyOfUser: options.historyOfUser, @@ -1157,6 +1184,8 @@ export class VideoModel extends Model { sort?: string startDate?: string // ISO 8601 endDate?: string // ISO 8601 + originallyPublishedStartDate?: string + originallyPublishedEndDate?: string nsfw?: boolean categoryOneOf?: number[] licenceOneOf?: number[] @@ -1179,6 +1208,15 @@ export class VideoModel extends Model { whereAnd.push({ publishedAt: publishedAtRange }) } + if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) { + const originallyPublishedAtRange = {} + + if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Sequelize.Op.gte ] = options.originallyPublishedStartDate + if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Sequelize.Op.lte ] = options.originallyPublishedEndDate + + whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange }) + } + if (options.durationMin || options.durationMax) { const durationRange = {} @@ -1252,7 +1290,7 @@ export class VideoModel extends Model { } static load (id: number | string, t?: Sequelize.Transaction) { - const where = VideoModel.buildWhereIdOrUUID(id) + const where = buildWhereIdOrUUID(id) const options = { where, transaction: t @@ -1262,7 +1300,7 @@ export class VideoModel extends Model { } static loadWithRights (id: number | string, t?: Sequelize.Transaction) { - const where = VideoModel.buildWhereIdOrUUID(id) + const where = buildWhereIdOrUUID(id) const options = { where, transaction: t @@ -1272,7 +1310,7 @@ export class VideoModel extends Model { } static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { - const where = VideoModel.buildWhereIdOrUUID(id) + const where = buildWhereIdOrUUID(id) const options = { attributes: [ 'id' ], @@ -1285,7 +1323,7 @@ export class VideoModel extends Model { static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) - .findById(id, { transaction: t, logging }) + .findByPk(id, { transaction: t, logging }) } static loadByUUIDWithFile (uuid: string) { @@ -1325,7 +1363,7 @@ export class VideoModel extends Model { } static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { - const where = VideoModel.buildWhereIdOrUUID(id) + const where = buildWhereIdOrUUID(id) const options = { order: [ [ 'Tags', 'name', 'ASC' ] ], @@ -1352,7 +1390,7 @@ export class VideoModel extends Model { } static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) { - const where = VideoModel.buildWhereIdOrUUID(id) + const where = buildWhereIdOrUUID(id) const options = { order: [ [ 'Tags', 'name', 'ASC' ] ], @@ -1505,18 +1543,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, @@ -1526,6 +1553,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 { @@ -1554,10 +1604,6 @@ export class VideoModel extends Model { return VIDEO_STATES[ id ] || 'Unknown' } - static buildWhereIdOrUUID (id: number | string) { - return validator.isInt('' + id) ? { id } : { uuid: id } - } - getOriginalFile () { if (Array.isArray(this.VideoFiles) === false) return undefined @@ -1570,7 +1616,6 @@ export class VideoModel extends Model { } getThumbnailName () { - // We always have a copy of the thumbnail const extension = '.jpg' return this.uuid + extension } @@ -1621,10 +1666,10 @@ export class VideoModel extends Model { name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`, createdBy: 'PeerTube', announceList: [ - [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ], - [ CONFIG.WEBSERVER.URL + '/tracker/announce' ] + [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ], + [ WEBSERVER.URL + '/tracker/announce' ] ], - urlList: [ CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] + urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] } const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) @@ -1714,7 +1759,7 @@ export class VideoModel extends Model { } removeStreamingPlaylist (isRedundancy = false) { - const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY + const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_STREAMING_PLAYLIST_DIRECTORY const filePath = join(baseDir, this.uuid) return remove(filePath) @@ -1724,12 +1769,7 @@ export class VideoModel extends Model { isOutdated () { if (this.isOwned()) return false - const now = Date.now() - const createdAtTime = this.createdAt.getTime() - const updatedAtTime = this.updatedAt.getTime() - - return (now - createdAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL && - (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL + return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL) } setAsRefreshed () { @@ -1743,8 +1783,8 @@ export class VideoModel extends Model { let baseUrlWs if (this.isOwned()) { - baseUrlHttp = CONFIG.WEBSERVER.URL - baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + baseUrlHttp = WEBSERVER.URL + baseUrlWs = WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT } else { baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host