X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=a62ae2f237047dc9088d1f36d9ff119ae4960173;hb=c4c0c31144b2cb8b6ade1cc39d676bc00c08cabf;hp=2d8b7b653a6c1f8437b9d31d0a0884d99f6d08b1;hpb=71d4af1efc810f853e1a0d986bf758c201692594;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 2d8b7b653..a62ae2f23 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,4 +1,4 @@ -import * as Bluebird from 'bluebird' +import Bluebird from 'bluebird' import { remove } from 'fs-extra' import { maxBy, minBy } from 'lodash' import { join } from 'path' @@ -24,30 +24,29 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { setAsUpdated } from '@server/helpers/database-utils' import { buildNSFWFilter } from '@server/helpers/express-utils' +import { uuidToShort } from '@server/helpers/uuid' import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' -import { LiveManager } from '@server/lib/live-manager' -import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' +import { LiveManager } from '@server/lib/live/live-manager' +import { removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' +import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths' +import { VideoPathManager } from '@server/lib/video-path-manager' import { getServerActor } from '@server/models/application/application' import { ModelCache } from '@server/models/model-cache' -import { AttributesOnly } from '@shared/core-utils' +import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' import { VideoFile } from '@shared/models/videos/video-file.model' import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' import { VideoObject } from '../../../shared/models/activitypub/objects' -import { Video, VideoDetails, VideoRateType } from '../../../shared/models/videos' +import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos' import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' import { VideoFilter } from '../../../shared/models/videos/video-query.type' import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' import { peertubeTruncate } from '../../helpers/core-utils' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { isBooleanValid } from '../../helpers/custom-validators/misc' +import { exists, isBooleanValid } from '../../helpers/custom-validators/misc' import { - isVideoCategoryValid, isVideoDescriptionValid, isVideoDurationValid, - isVideoLanguageValid, - isVideoLicenceValid, isVideoNameValid, isVideoPrivacyValid, isVideoStateValid, @@ -56,19 +55,7 @@ import { import { getVideoFileResolution } from '../../helpers/ffprobe-utils' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' -import { - ACTIVITY_PUB, - API_VERSION, - CONSTRAINTS_FIELDS, - LAZY_STATIC_PATHS, - STATIC_PATHS, - VIDEO_CATEGORIES, - VIDEO_LANGUAGES, - VIDEO_LICENCES, - VIDEO_PRIVACIES, - VIDEO_STATES, - WEBSERVER -} from '../../initializers/constants' +import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' import { sendDeleteVideo } from '../../lib/activitypub/send' import { MChannel, @@ -106,6 +93,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { ServerModel } from '../server/server' import { TrackerModel } from '../server/tracker' import { VideoTrackerModel } from '../server/video-tracker' +import { setAsUpdated } from '../shared' import { UserModel } from '../user/user' import { UserVideoHistoryModel } from '../user/user-video-history' import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' @@ -128,6 +116,7 @@ import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel import { VideoCommentModel } from './video-comment' import { VideoFileModel } from './video-file' import { VideoImportModel } from './video-import' +import { VideoJobInfoModel } from './video-job-info' import { VideoLiveModel } from './video-live' import { VideoPlaylistElementModel } from './video-playlist-element' import { VideoShareModel } from './video-share' @@ -136,20 +125,16 @@ import { VideoTagModel } from './video-tag' import { VideoViewModel } from './video-view' export enum ScopeNames { - AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', FOR_API = 'FOR_API', WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', WITH_TAGS = 'WITH_TAGS', - WITH_TRACKERS = 'WITH_TRACKERS', WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', WITH_BLACKLISTED = 'WITH_BLACKLISTED', - WITH_USER_HISTORY = 'WITH_USER_HISTORY', WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', - WITH_USER_ID = 'WITH_USER_ID', WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', - WITH_THUMBNAILS = 'WITH_THUMBNAILS', - WITH_LIVE = 'WITH_LIVE' + WITH_USER_HISTORY = 'WITH_USER_HISTORY', + WITH_THUMBNAILS = 'WITH_THUMBNAILS' } export type ForAPIOptions = { @@ -245,30 +230,6 @@ export type AvailableForListIDsOptions = { } ] }, - [ScopeNames.WITH_LIVE]: { - include: [ - { - model: VideoLiveModel.unscoped(), - required: false - } - ] - }, - [ScopeNames.WITH_USER_ID]: { - include: [ - { - attributes: [ 'accountId' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'userId' ], - model: AccountModel.unscoped(), - required: true - } - ] - } - ] - }, [ScopeNames.WITH_ACCOUNT_DETAILS]: { include: [ { @@ -326,14 +287,6 @@ export type AvailableForListIDsOptions = { [ScopeNames.WITH_TAGS]: { include: [ TagModel ] }, - [ScopeNames.WITH_TRACKERS]: { - include: [ - { - attributes: [ 'id', 'url' ], - model: TrackerModel - } - ] - }, [ScopeNames.WITH_BLACKLISTED]: { include: [ { @@ -506,19 +459,16 @@ export class VideoModel extends Model>> { @AllowNull(true) @Default(null) - @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true)) @Column category: number @AllowNull(true) @Default(null) - @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true)) @Column licence: number @AllowNull(true) @Default(null) - @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max)) language: string @@ -626,7 +576,7 @@ export class VideoModel extends Model>> { foreignKey: { allowNull: true }, - hooks: true + onDelete: 'cascade' }) VideoChannel: VideoChannelModel @@ -785,6 +735,15 @@ export class VideoModel extends Model>> { }) VideoCaptions: VideoCaptionModel[] + @HasOne(() => VideoJobInfoModel, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) + VideoJobInfo: VideoJobInfoModel + @BeforeDestroy static async sendDelete (instance: MVideoAccountLight, options) { if (!instance.isOwned()) return undefined @@ -804,25 +763,24 @@ export class VideoModel extends Model>> { } @BeforeDestroy - static async removeFiles (instance: VideoModel) { + static async removeFiles (instance: VideoModel, options) { const tasks: Promise[] = [] logger.info('Removing files of video %s.', instance.url) if (instance.isOwned()) { if (!Array.isArray(instance.VideoFiles)) { - instance.VideoFiles = await instance.$get('VideoFiles') + instance.VideoFiles = await instance.$get('VideoFiles', { transaction: options.transaction }) } // Remove physical files and torrents instance.VideoFiles.forEach(file => { - tasks.push(instance.removeFile(file)) - tasks.push(file.removeTorrent()) + tasks.push(instance.removeFileAndTorrent(file)) }) // Remove playlists file if (!Array.isArray(instance.VideoStreamingPlaylists)) { - instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists') + instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists', { transaction: options.transaction }) } for (const p of instance.VideoStreamingPlaylists) { @@ -845,7 +803,7 @@ export class VideoModel extends Model>> { logger.info('Stopping live of video %s after video deletion.', instance.uuid) - return LiveManager.Instance.stopSessionOf(instance.id) + LiveManager.Instance.stopSessionOf(instance.id) } @BeforeDestroy @@ -858,7 +816,7 @@ export class VideoModel extends Model>> { const tasks: Promise[] = [] if (!Array.isArray(instance.VideoAbuses)) { - instance.VideoAbuses = await instance.$get('VideoAbuses') + instance.VideoAbuses = await instance.$get('VideoAbuses', { transaction: options.transaction }) if (instance.VideoAbuses.length === 0) return undefined } @@ -873,12 +831,7 @@ export class VideoModel extends Model>> { tasks.push(abuse.save({ transaction: options.transaction })) } - Promise.all(tasks) - .catch(err => { - logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err }) - }) - - return undefined + await Promise.all(tasks) } static listLocal (): Promise { @@ -1025,10 +978,12 @@ export class VideoModel extends Model>> { start: number count: number sort: string + + channelId?: number isLive?: boolean search?: string }) { - const { accountId, start, count, sort, search, isLive } = options + const { accountId, channelId, start, count, sort, search, isLive } = options function buildBaseQuery (): FindOptions { const where: WhereOptions = {} @@ -1039,10 +994,14 @@ export class VideoModel extends Model>> { } } - if (isLive) { + if (exists(isLive)) { where.isLive = isLive } + const channelWhere = channelId + ? { id: channelId } + : {} + const baseQuery = { offset: start, limit: count, @@ -1052,6 +1011,7 @@ export class VideoModel extends Model>> { { model: VideoChannelModel, required: true, + where: channelWhere, include: [ { model: AccountModel, @@ -1129,7 +1089,8 @@ export class VideoModel extends Model>> { const trendingDays = options.sort.endsWith('trending') ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS : undefined - let trendingAlgorithm + + let trendingAlgorithm: string if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot' if (options.sort.endsWith('best')) trendingAlgorithm = 'best' @@ -1141,40 +1102,44 @@ export class VideoModel extends Model>> { : serverActor.id const queryOptions = { - start: options.start, - count: options.count, - sort: options.sort, + ...pick(options, [ + 'start', + 'count', + 'sort', + 'nsfw', + 'isLive', + 'categoryOneOf', + 'licenceOneOf', + 'languageOneOf', + 'tagsOneOf', + 'tagsAllOf', + 'filter', + 'withFiles', + 'accountId', + 'videoChannelId', + 'videoPlaylistId', + 'includeLocalVideos', + 'user', + 'historyOfUser', + 'search' + ]), + followerActorId, serverAccountId: serverActor.Account.id, - nsfw: options.nsfw, - isLive: options.isLive, - 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, - videoPlaylistId: options.videoPlaylistId, - includeLocalVideos: options.includeLocalVideos, - user: options.user, - historyOfUser: options.historyOfUser, trendingDays, - trendingAlgorithm, - search: options.search + trendingAlgorithm } return VideoModel.getAvailableForApi(queryOptions, options.countVideos) } static async searchAndPopulateAccountAndServer (options: { + start: number + count: number + sort: string includeLocalVideos: boolean search?: string - start?: number - count?: number - sort?: string + host?: string startDate?: string // ISO 8601 endDate?: string // ISO 8601 originallyPublishedStartDate?: string @@ -1190,41 +1155,38 @@ export class VideoModel extends Model>> { durationMax?: number // seconds user?: MUserAccountId filter?: VideoFilter + uuids?: string[] }) { const serverActor = await getServerActor() const queryOptions = { - followerActorId: serverActor.id, - serverAccountId: serverActor.Account.id, - - includeLocalVideos: options.includeLocalVideos, - nsfw: options.nsfw, - isLive: options.isLive, + ...pick(options, [ + 'includeLocalVideos', + 'nsfw', + 'isLive', + 'categoryOneOf', + 'licenceOneOf', + 'languageOneOf', + 'tagsOneOf', + 'tagsAllOf', + 'user', + 'filter', + 'host', + 'start', + 'count', + 'sort', + 'startDate', + 'endDate', + 'originallyPublishedStartDate', + 'originallyPublishedEndDate', + 'durationMin', + 'durationMax', + 'uuids', + 'search' + ]), - categoryOneOf: options.categoryOneOf, - licenceOneOf: options.licenceOneOf, - languageOneOf: options.languageOneOf, - - tagsOneOf: options.tagsOneOf, - tagsAllOf: options.tagsAllOf, - - user: options.user, - filter: options.filter, - - start: options.start, - count: options.count, - sort: options.sort, - - startDate: options.startDate, - endDate: options.endDate, - - originallyPublishedStartDate: options.originallyPublishedStartDate, - originallyPublishedEndDate: options.originallyPublishedEndDate, - - durationMin: options.durationMin, - durationMax: options.durationMax, - - search: options.search + followerActorId: serverActor.id, + serverAccountId: serverActor.Account.id } return VideoModel.getAvailableForApi(queryOptions) @@ -1439,7 +1401,7 @@ export class VideoModel extends Model>> { const rawQuery = `UPDATE "video" SET "${field}" = ` + '(' + - 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' + + 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' + ') ' + 'WHERE "video"."id" = :videoId' @@ -1467,15 +1429,15 @@ export class VideoModel extends Model>> { .then(results => results.length === 1) } - static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) { + static bulkUpdateSupportField (ofChannel: MChannel, t: Transaction) { const options = { where: { - channelId: videoChannel.id + channelId: ofChannel.id }, transaction: t } - return VideoModel.update({ support: videoChannel.support }, options) + return VideoModel.update({ support: ofChannel.support }, options) } static getAllIdsFromChannel (videoChannel: MChannelId): Promise { @@ -1556,26 +1518,6 @@ export class VideoModel extends Model>> { } } - static getCategoryLabel (id: number) { - return VIDEO_CATEGORIES[id] || 'Misc' - } - - static getLicenceLabel (id: number) { - return VIDEO_LICENCES[id] || 'Unknown' - } - - static getLanguageLabel (id: string) { - return VIDEO_LANGUAGES[id] || 'Unknown' - } - - static getPrivacyLabel (id: number) { - return VIDEO_PRIVACIES[id] || 'Unknown' - } - - static getStateLabel (id: number) { - return VIDEO_STATES[id] || 'Unknown' - } - isBlacklisted () { return !!this.VideoBlacklist } @@ -1658,11 +1600,11 @@ export class VideoModel extends Model>> { } getWatchStaticPath () { - return '/w/' + this.uuid + return buildVideoWatchPath({ shortUUID: uuidToShort(this.uuid) }) } getEmbedStaticPath () { - return '/videos/embed/' + this.uuid + return buildVideoEmbedPath(this) } getMiniatureStaticPath () { @@ -1718,9 +1660,10 @@ export class VideoModel extends Model>> { getMaxQualityResolution () { const file = this.getMaxQualityFile() const videoOrPlaylist = file.getVideoOrStreamingPlaylist() - const originalFilePath = getVideoFilePath(videoOrPlaylist, file) - return getVideoFileResolution(originalFilePath) + return VideoPathManager.Instance.makeAvailableVideoFile(videoOrPlaylist, file, originalFilePath => { + return getVideoFileResolution(originalFilePath) + }) } getDescriptionAPIPath () { @@ -1749,14 +1692,25 @@ export class VideoModel extends Model>> { .concat(toAdd) } - removeFile (videoFile: MVideoFile, isRedundancy = false) { - const filePath = getVideoFilePath(this, videoFile, isRedundancy) - return remove(filePath) - .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) + removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { + const filePath = isRedundancy + ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) + : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) + + const promises: Promise[] = [ remove(filePath) ] + if (!isRedundancy) promises.push(videoFile.removeTorrent()) + + if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { + promises.push(removeWebTorrentObjectStorage(videoFile)) + } + + return Promise.all(promises) } async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { - const directoryPath = getHLSDirectory(this, isRedundancy) + const directoryPath = isRedundancy + ? getHLSRedundancyDirectory(this) + : getHLSDirectory(this) await remove(directoryPath) @@ -1772,6 +1726,10 @@ export class VideoModel extends Model>> { await Promise.all( streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent()) ) + + if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) { + await removeHLSObjectStorage(streamingPlaylist, this) + } } } @@ -1815,16 +1773,16 @@ export class VideoModel extends Model>> { this.privacy === VideoPrivacy.INTERNAL } - async publishIfNeededAndSave (t: Transaction) { - if (this.state !== VideoState.PUBLISHED) { - this.state = VideoState.PUBLISHED - this.publishedAt = new Date() - await this.save({ transaction: t }) + async setNewState (newState: VideoState, isNewVideo: boolean, transaction: Transaction) { + if (this.state === newState) throw new Error('Cannot use same state ' + newState) - return true + this.state = newState + + if (this.state === VideoState.PUBLISHED && isNewVideo) { + this.publishedAt = new Date() } - return false + await this.save({ transaction }) } getBandwidthBits (videoFile: MVideoFile) {