PeerTube/server/core/models/video/video.ts

2128 lines
56 KiB
TypeScript
Raw Normal View History

import { buildVideoEmbedPath, buildVideoWatchPath, pick, wait } from '@peertube/peertube-core-utils'
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg'
import {
FileStorage,
ResultList,
ThumbnailType,
UserRight,
Video,
VideoDetails,
VideoFile,
VideoInclude,
VideoIncludeType,
VideoObject,
VideoPrivacy,
VideoRateType,
VideoState,
VideoStreamingPlaylistType,
type VideoPrivacyType,
type VideoStateType
} from '@peertube/peertube-models'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video.js'
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
import { LiveManager } from '@server/lib/live/live-manager.js'
import {
removeHLSFileObjectStorageByFilename,
removeHLSObjectStorage,
removeOriginalFileObjectStorage,
removeWebVideoObjectStorage
} from '@server/lib/object-storage/index.js'
import { tracer } from '@server/lib/opentelemetry/tracing.js'
import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
import { getServerActor } from '@server/models/application/application.js'
import { ModelCache } from '@server/models/shared/model-cache.js'
import { MVideoSource } from '@server/types/models/video/video-source.js'
2021-08-27 14:32:44 +02:00
import Bluebird from 'bluebird'
import { remove } from 'fs-extra/esm'
import maxBy from 'lodash-es/maxBy.js'
import minBy from 'lodash-es/minBy.js'
import { FindOptions, IncludeOptions, Includeable, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
2017-12-12 17:53:50 +01:00
import {
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
AfterCreate,
AfterDestroy,
AfterUpdate,
AllowNull,
BeforeDestroy,
BelongsTo,
BelongsToMany,
Column,
CreatedAt,
DataType,
Default,
ForeignKey,
HasMany,
HasOne,
Is,
IsInt,
IsUUID,
2024-02-22 10:12:04 +01:00
Min, Scopes,
Table,
2018-08-31 17:18:13 +02:00
UpdatedAt
2017-12-12 17:53:50 +01:00
} from 'sequelize-typescript'
import { peertubeTruncate } from '../../helpers/core-utils.js'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
import { exists, isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc.js'
2017-12-12 17:53:50 +01:00
import {
isVideoDescriptionValid,
isVideoDurationValid,
2019-02-26 10:55:40 +01:00
isVideoNameValid,
isVideoPrivacyValid,
isVideoStateValid,
2018-03-12 11:06:15 +01:00
isVideoSupportValid
} from '../../helpers/custom-validators/videos.js'
import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants.js'
import { sendDeleteVideo } from '../../lib/activitypub/send/index.js'
2019-08-15 11:53:26 +02:00
import {
MChannel,
2019-08-20 13:52:49 +02:00
MChannelAccountDefault,
2019-08-15 11:53:26 +02:00
MChannelId,
2023-06-01 14:51:16 +02:00
MStoryboard,
MStreamingPlaylist,
MStreamingPlaylistFilesVideo,
2019-08-15 11:53:26 +02:00
MUserAccountId,
MUserId,
2019-08-21 14:31:57 +02:00
MVideoAP,
2023-06-01 14:51:16 +02:00
MVideoAPLight,
MVideoAccountLightBlacklistAllFiles,
2023-06-01 14:51:16 +02:00
MVideoCaptionLanguageUrl,
2019-08-15 11:53:26 +02:00
MVideoDetails,
MVideoFileVideo,
MVideoForUser,
2019-08-21 14:31:57 +02:00
MVideoFormattable,
MVideoFormattableDetails,
2019-08-15 11:53:26 +02:00
MVideoFullLight,
2021-06-11 14:09:33 +02:00
MVideoId,
2020-03-05 15:04:57 +01:00
MVideoImmutable,
2019-08-15 11:53:26 +02:00
MVideoThumbnail,
2019-08-22 10:46:54 +02:00
MVideoThumbnailBlacklist,
2019-08-21 14:31:57 +02:00
MVideoWithAllFiles,
MVideoWithFile,
type MVideo,
type MVideoAccountLight
} from '../../types/models/index.js'
import { MThumbnail } from '../../types/models/video/thumbnail.js'
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file.js'
import { VideoAbuseModel } from '../abuse/video-abuse.js'
import { AccountVideoRateModel } from '../account/account-video-rate.js'
import { AccountModel } from '../account/account.js'
import { ActorImageModel } from '../actor/actor-image.js'
import { ActorModel } from '../actor/actor.js'
import { VideoRedundancyModel } from '../redundancy/video-redundancy.js'
import { ServerModel } from '../server/server.js'
import { TrackerModel } from '../server/tracker.js'
import { VideoTrackerModel } from '../server/video-tracker.js'
2024-02-22 10:12:04 +01:00
import {
SequelizeModel,
buildTrigramSearchIndex,
buildWhereIdOrUUID,
getVideoSort,
isOutdated,
setAsUpdated,
throwIfNotValid
} from '../shared/index.js'
import { UserVideoHistoryModel } from '../user/user-video-history.js'
import { UserModel } from '../user/user.js'
import { VideoViewModel } from '../view/video-view.js'
import { videoModelToActivityPubObject } from './formatter/video-activity-pub-format.js'
2021-06-10 08:53:32 +02:00
import {
VideoFormattingJSONOptions,
videoFilesModelToFormattedJSON,
2021-06-10 08:53:32 +02:00
videoModelToFormattedDetailsJSON,
videoModelToFormattedJSON
} from './formatter/video-api-format.js'
import { ScheduleVideoUpdateModel } from './schedule-video-update.js'
import {
BuildVideosListQueryOptions,
DisplayOnlyForFollowerOptions,
VideoModelGetQueryBuilder,
VideosIdListQueryBuilder,
VideosModelListQueryBuilder
} from './sql/video/index.js'
import { StoryboardModel } from './storyboard.js'
import { TagModel } from './tag.js'
import { ThumbnailModel } from './thumbnail.js'
import { VideoBlacklistModel } from './video-blacklist.js'
import { VideoCaptionModel } from './video-caption.js'
import { SummaryOptions, VideoChannelModel, ScopeNames as VideoChannelScopeNames } from './video-channel.js'
import { VideoCommentModel } from './video-comment.js'
import { VideoFileModel } from './video-file.js'
import { VideoImportModel } from './video-import.js'
import { VideoJobInfoModel } from './video-job-info.js'
import { VideoLiveModel } from './video-live.js'
import { VideoPasswordModel } from './video-password.js'
import { VideoPlaylistElementModel } from './video-playlist-element.js'
import { VideoShareModel } from './video-share.js'
import { VideoSourceModel } from './video-source.js'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist.js'
import { VideoTagModel } from './video-tag.js'
2018-10-05 11:15:06 +02:00
export enum ScopeNames {
FOR_API = 'FOR_API',
2018-01-04 11:19:16 +01:00
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
2017-12-14 10:07:57 +01:00
WITH_TAGS = 'WITH_TAGS',
WITH_WEB_VIDEO_FILES = 'WITH_WEB_VIDEO_FILES',
2018-08-14 09:08:47 +02:00
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
2018-10-05 11:15:06 +02:00
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
2019-01-29 08:37:25 +01:00
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
2021-06-11 14:45:58 +02:00
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
WITH_THUMBNAILS = 'WITH_THUMBNAILS'
2017-12-14 10:07:57 +01:00
}
2019-07-31 15:57:32 +02:00
export type ForAPIOptions = {
ids?: number[]
2019-02-26 10:55:40 +01:00
videoPlaylistId?: number
2019-07-31 15:57:32 +02:00
withAccountBlockerIds?: number[]
}
2019-04-23 09:50:57 +02:00
@Scopes(() => ({
[ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
attributes: [ 'id', 'url', 'uuid', 'remote' ]
},
2020-01-31 16:56:52 +01:00
[ScopeNames.FOR_API]: (options: ForAPIOptions) => {
2020-12-08 14:30:29 +01:00
const include: Includeable[] = [
{
model: VideoChannelModel.scope({
method: [
VideoChannelScopeNames.SUMMARY, {
withAccount: true,
withAccountBlockerIds: options.withAccountBlockerIds
} as SummaryOptions
]
}),
required: true
},
{
attributes: [ 'type', 'filename' ],
model: ThumbnailModel,
required: false
}
]
const query: FindOptions = {}
2019-07-31 15:57:32 +02:00
if (options.ids) {
query.where = {
id: {
2020-01-31 16:56:52 +01:00
[Op.in]: options.ids
2019-07-31 15:57:32 +02:00
}
}
}
2019-02-26 10:55:40 +01:00
if (options.videoPlaylistId) {
2020-12-08 14:30:29 +01:00
include.push({
2019-02-26 10:55:40 +01:00
model: VideoPlaylistElementModel.unscoped(),
2019-03-12 11:40:42 +01:00
required: true,
where: {
videoPlaylistId: options.videoPlaylistId
}
2019-02-26 10:55:40 +01:00
})
}
2020-12-08 14:30:29 +01:00
query.include = include
return query
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_THUMBNAILS]: {
include: [
{
2019-04-23 09:50:57 +02:00
model: ThumbnailModel,
required: false
}
]
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_ACCOUNT_DETAILS]: {
2017-12-14 10:07:57 +01:00
include: [
{
2019-04-23 09:50:57 +02:00
model: VideoChannelModel.unscoped(),
2017-12-14 10:07:57 +01:00
required: true,
include: [
2018-01-11 14:30:27 +01:00
{
attributes: {
exclude: [ 'privateKey', 'publicKey' ]
},
2019-04-23 09:50:57 +02:00
model: ActorModel.unscoped(),
2018-01-18 17:44:04 +01:00
required: true,
include: [
{
attributes: [ 'host' ],
2019-04-23 09:50:57 +02:00
model: ServerModel.unscoped(),
2018-01-18 17:44:04 +01:00
required: false
},
{
model: ActorImageModel,
as: 'Avatars',
required: false
2018-01-18 17:44:04 +01:00
}
]
2018-01-11 14:30:27 +01:00
},
2017-12-14 10:07:57 +01:00
{
2019-04-23 09:50:57 +02:00
model: AccountModel.unscoped(),
2017-12-14 10:07:57 +01:00
required: true,
include: [
{
2019-04-23 09:50:57 +02:00
model: ActorModel.unscoped(),
2018-01-11 14:30:27 +01:00
attributes: {
exclude: [ 'privateKey', 'publicKey' ]
},
2017-12-14 17:38:41 +01:00
required: true,
include: [
{
2018-01-18 17:44:04 +01:00
attributes: [ 'host' ],
2019-04-23 09:50:57 +02:00
model: ServerModel.unscoped(),
2017-12-14 17:38:41 +01:00
required: false
2018-02-16 11:19:54 +01:00
},
{
model: ActorImageModel,
as: 'Avatars',
2018-02-16 11:19:54 +01:00
required: false
2017-12-14 17:38:41 +01:00
}
]
2017-12-14 10:07:57 +01:00
}
]
}
]
}
2019-04-23 09:50:57 +02:00
]
2017-12-14 10:07:57 +01:00
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_TAGS]: {
2019-04-23 09:50:57 +02:00
include: [ TagModel ]
2017-12-14 10:07:57 +01:00
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_BLACKLISTED]: {
2018-08-14 09:08:47 +02:00
include: [
{
2019-08-15 11:53:26 +02:00
attributes: [ 'id', 'reason', 'unfederated' ],
2019-04-23 09:50:57 +02:00
model: VideoBlacklistModel,
2018-08-14 09:08:47 +02:00
required: false
}
]
},
[ScopeNames.WITH_WEB_VIDEO_FILES]: (withRedundancies = false) => {
2019-01-29 08:37:25 +01:00
let subInclude: any[] = []
if (withRedundancies === true) {
subInclude = [
{
attributes: [ 'fileUrl' ],
model: VideoRedundancyModel.unscoped(),
required: false
}
]
}
return {
include: [
{
model: VideoFileModel,
2021-02-25 11:17:53 +01:00
separate: true,
2019-01-29 08:37:25 +01:00
required: false,
include: subInclude
}
]
}
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
const subInclude: IncludeOptions[] = [
{
model: VideoFileModel,
required: false
}
]
2019-01-29 08:37:25 +01:00
if (withRedundancies === true) {
subInclude.push({
attributes: [ 'fileUrl' ],
model: VideoRedundancyModel.unscoped(),
required: false
})
2019-01-29 08:37:25 +01:00
}
return {
include: [
{
model: VideoStreamingPlaylistModel.unscoped(),
required: false,
2021-02-25 11:17:53 +01:00
separate: true,
2019-01-29 08:37:25 +01:00
include: subInclude
}
]
}
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_SCHEDULED_UPDATE]: {
include: [
{
2019-04-23 09:50:57 +02:00
model: ScheduleVideoUpdateModel.unscoped(),
required: false
}
]
2018-10-05 11:15:06 +02:00
},
2020-01-31 16:56:52 +01:00
[ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
2018-10-05 11:15:06 +02:00
return {
include: [
{
attributes: [ 'currentTime' ],
model: UserVideoHistoryModel.unscoped(),
required: false,
where: {
userId
}
}
]
}
2017-12-14 10:07:57 +01:00
}
2019-04-23 09:50:57 +02:00
}))
2017-12-12 17:53:50 +01:00
@Table({
tableName: 'video',
2020-01-28 14:45:17 +01:00
indexes: [
buildTrigramSearchIndex('video_name_trigram', 'name'),
{ fields: [ 'createdAt' ] },
{
fields: [
{ name: 'publishedAt', order: 'DESC' },
{ name: 'id', order: 'ASC' }
]
},
{ fields: [ 'duration' ] },
2021-03-03 11:24:16 +01:00
{
fields: [
{ name: 'views', order: 'DESC' },
{ name: 'id', order: 'ASC' }
]
},
2020-01-28 14:45:17 +01:00
{ fields: [ 'channelId' ] },
{
fields: [ 'originallyPublishedAt' ],
where: {
originallyPublishedAt: {
[Op.ne]: null
}
}
},
{
fields: [ 'category' ], // We don't care videos with an unknown category
where: {
category: {
[Op.ne]: null
}
}
},
{
fields: [ 'licence' ], // We don't care videos with an unknown licence
where: {
licence: {
[Op.ne]: null
}
}
},
{
fields: [ 'language' ], // We don't care videos with an unknown language
where: {
language: {
[Op.ne]: null
}
}
},
{
fields: [ 'nsfw' ], // Most of the videos are not NSFW
where: {
nsfw: true
}
},
2023-10-25 15:07:36 +02:00
{
fields: [ 'isLive' ], // Most of the videos are VOD
where: {
isLive: true
}
},
2020-01-28 14:45:17 +01:00
{
fields: [ 'remote' ], // Only index local videos
where: {
remote: false
}
},
{
fields: [ 'uuid' ],
unique: true
},
{
fields: [ 'url' ],
unique: true
}
]
2017-12-12 17:53:50 +01:00
})
2024-02-22 10:12:04 +01:00
export class VideoModel extends SequelizeModel<VideoModel> {
2017-12-12 17:53:50 +01:00
@AllowNull(false)
@Default(DataType.UUIDV4)
@IsUUID(4)
@Column(DataType.UUID)
uuid: string
@AllowNull(false)
@Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
@Column
name: string
@AllowNull(true)
@Default(null)
@Column
category: number
@AllowNull(true)
@Default(null)
@Column
licence: number
@AllowNull(true)
@Default(null)
2018-04-23 14:39:52 +02:00
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max))
language: string
2017-12-12 17:53:50 +01:00
@AllowNull(false)
@Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
@Column(DataType.INTEGER)
privacy: VideoPrivacyType
2017-12-12 17:53:50 +01:00
@AllowNull(false)
2018-01-03 10:12:36 +01:00
@Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
2017-12-12 17:53:50 +01:00
@Column
nsfw: boolean
@AllowNull(true)
@Default(null)
2019-04-18 11:28:17 +02:00
@Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description', true))
2017-12-12 17:53:50 +01:00
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
description: string
@AllowNull(true)
@Default(null)
2019-04-18 11:28:17 +02:00
@Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support', true))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max))
support: string
2017-12-12 17:53:50 +01:00
@AllowNull(false)
@Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
@Column
duration: number
@AllowNull(false)
@Default(0)
@IsInt
@Min(0)
@Column
views: number
@AllowNull(false)
@Default(0)
@IsInt
@Min(0)
@Column
likes: number
@AllowNull(false)
@Default(0)
@IsInt
@Min(0)
@Column
dislikes: number
@AllowNull(false)
@Column
remote: boolean
@AllowNull(false)
@Default(false)
@Column
isLive: boolean
@AllowNull(false)
2017-12-12 17:53:50 +01:00
@Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
url: string
2018-01-03 10:12:36 +01:00
@AllowNull(false)
@Column
commentsEnabled: boolean
@AllowNull(false)
@Column
downloadEnabled: boolean
@AllowNull(false)
@Column
waitTranscoding: boolean
@AllowNull(false)
@Default(null)
@Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
@Column
state: VideoStateType
2024-02-27 11:18:56 +01:00
@AllowNull(true)
@Column(DataType.FLOAT)
aspectRatio: number
2023-07-19 16:02:49 +02:00
// We already have the information in videoSource table for local videos, but we prefer to normalize it for performance
// And also to store the info from remote instances
@AllowNull(true)
@Column
inputFileUpdatedAt: Date
2017-12-12 17:53:50 +01:00
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@AllowNull(false)
2019-04-18 11:28:17 +02:00
@Default(DataType.NOW)
@Column
publishedAt: Date
2019-02-11 14:41:55 +01:00
@AllowNull(true)
@Default(null)
2019-01-12 14:41:45 +01:00
@Column
originallyPublishedAt: Date
2017-12-12 17:53:50 +01:00
@ForeignKey(() => VideoChannelModel)
@Column
channelId: number
@BelongsTo(() => VideoChannelModel, {
2016-12-11 21:50:51 +01:00
foreignKey: {
2017-12-14 17:38:41 +01:00
allowNull: true
2016-12-11 21:50:51 +01:00
},
onDelete: 'cascade'
2016-12-11 21:50:51 +01:00
})
VideoChannel: Awaited<VideoChannelModel>
2016-12-24 16:59:17 +01:00
2017-12-12 17:53:50 +01:00
@BelongsToMany(() => TagModel, {
2016-12-24 16:59:17 +01:00
foreignKey: 'videoId',
2017-12-12 17:53:50 +01:00
through: () => VideoTagModel,
onDelete: 'CASCADE'
2016-12-24 16:59:17 +01:00
})
Tags: Awaited<TagModel>[]
2017-01-04 20:59:23 +01:00
2021-02-18 10:15:11 +01:00
@BelongsToMany(() => TrackerModel, {
foreignKey: 'videoId',
through: () => VideoTrackerModel,
onDelete: 'CASCADE'
})
Trackers: Awaited<TrackerModel>[]
2021-02-18 10:15:11 +01:00
@HasMany(() => ThumbnailModel, {
foreignKey: {
name: 'videoId',
allowNull: true
},
hooks: true,
onDelete: 'cascade'
})
Thumbnails: Awaited<ThumbnailModel>[]
2019-02-26 10:55:40 +01:00
@HasMany(() => VideoPlaylistElementModel, {
foreignKey: {
name: 'videoId',
2019-07-31 15:57:32 +02:00
allowNull: true
2019-02-26 10:55:40 +01:00
},
2019-07-31 15:57:32 +02:00
onDelete: 'set null'
2019-02-26 10:55:40 +01:00
})
VideoPlaylistElements: Awaited<VideoPlaylistElementModel>[]
2019-02-26 10:55:40 +01:00
@HasOne(() => VideoSourceModel, {
foreignKey: {
name: 'videoId',
2023-07-19 16:02:49 +02:00
allowNull: false
},
onDelete: 'CASCADE'
})
VideoSource: Awaited<VideoSourceModel>
2017-12-12 17:53:50 +01:00
@HasMany(() => VideoAbuseModel, {
2017-01-04 20:59:23 +01:00
foreignKey: {
name: 'videoId',
allowNull: true
2017-01-04 20:59:23 +01:00
},
onDelete: 'set null'
2017-01-04 20:59:23 +01:00
})
VideoAbuses: Awaited<VideoAbuseModel>[]
2017-12-12 17:53:50 +01:00
@HasMany(() => VideoFileModel, {
foreignKey: {
name: 'videoId',
allowNull: true
},
2018-09-11 16:27:07 +02:00
hooks: true,
onDelete: 'cascade'
})
VideoFiles: Awaited<VideoFileModel>[]
2017-11-21 18:23:10 +01:00
2019-01-29 08:37:25 +01:00
@HasMany(() => VideoStreamingPlaylistModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
hooks: true,
onDelete: 'cascade'
})
VideoStreamingPlaylists: Awaited<VideoStreamingPlaylistModel>[]
2019-01-29 08:37:25 +01:00
2017-12-12 17:53:50 +01:00
@HasMany(() => VideoShareModel, {
2017-11-21 18:23:10 +01:00
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
VideoShares: Awaited<VideoShareModel>[]
2017-11-23 16:55:13 +01:00
2017-12-12 17:53:50 +01:00
@HasMany(() => AccountVideoRateModel, {
2017-11-23 16:55:13 +01:00
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
AccountVideoRates: Awaited<AccountVideoRateModel>[]
2016-11-11 15:20:03 +01:00
2017-12-28 11:16:08 +01:00
@HasMany(() => VideoCommentModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade',
hooks: true
2017-12-28 11:16:08 +01:00
})
VideoComments: Awaited<VideoCommentModel>[]
2017-12-28 11:16:08 +01:00
2018-08-31 17:18:13 +02:00
@HasMany(() => VideoViewModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
2018-10-05 11:15:06 +02:00
onDelete: 'cascade'
2018-08-31 17:18:13 +02:00
})
VideoViews: Awaited<VideoViewModel>[]
2018-08-31 17:18:13 +02:00
2018-10-05 11:15:06 +02:00
@HasMany(() => UserVideoHistoryModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
UserVideoHistories: Awaited<UserVideoHistoryModel>[]
2018-10-05 11:15:06 +02:00
@HasOne(() => ScheduleVideoUpdateModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
ScheduleVideoUpdate: Awaited<ScheduleVideoUpdateModel>
2018-08-13 16:57:13 +02:00
@HasOne(() => VideoBlacklistModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
VideoBlacklist: Awaited<VideoBlacklistModel>
2018-08-13 16:57:13 +02:00
2020-10-27 16:06:24 +01:00
@HasOne(() => VideoLiveModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
hooks: true,
2020-10-27 16:06:24 +01:00
onDelete: 'cascade'
})
VideoLive: Awaited<VideoLiveModel>
2020-10-27 16:06:24 +01:00
@HasOne(() => VideoImportModel, {
foreignKey: {
name: 'videoId',
allowNull: true
},
onDelete: 'set null'
})
VideoImport: Awaited<VideoImportModel>
2018-07-12 19:02:00 +02:00
@HasMany(() => VideoCaptionModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade',
hooks: true,
2020-01-31 16:56:52 +01:00
['separate' as any]: true
2018-07-12 19:02:00 +02:00
})
VideoCaptions: Awaited<VideoCaptionModel>[]
2018-07-12 19:02:00 +02:00
@HasMany(() => VideoPasswordModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
VideoPasswords: Awaited<VideoPasswordModel>[]
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
@HasOne(() => VideoJobInfoModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
VideoJobInfo: Awaited<VideoJobInfoModel>
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
2023-06-01 14:51:16 +02:00
@HasOne(() => StoryboardModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade',
hooks: true
2023-06-01 14:51:16 +02:00
})
Storyboard: Awaited<StoryboardModel>
2023-06-01 14:51:16 +02:00
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
@AfterCreate
static notifyCreate (video: MVideo) {
InternalEventEmitter.Instance.emit('video-created', { video })
}
@AfterUpdate
static notifyUpdate (video: MVideo) {
InternalEventEmitter.Instance.emit('video-updated', { video })
}
@AfterDestroy
static notifyDestroy (video: MVideo) {
InternalEventEmitter.Instance.emit('video-deleted', { video })
}
2023-09-01 13:16:27 +02:00
@BeforeDestroy
static stopLiveIfNeeded (instance: VideoModel) {
if (!instance.isLive) return
logger.info('Stopping live of video %s after video deletion.', instance.uuid)
LiveManager.Instance.stopSessionOf({ videoUUID: instance.uuid, error: null })
2023-09-01 13:16:27 +02:00
}
@BeforeDestroy
static invalidateCache (instance: VideoModel) {
ModelCache.Instance.invalidateCache('video', instance.id)
}
@BeforeDestroy
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
static async sendDelete (instance: MVideoAccountLight, options: { transaction: Transaction }) {
2021-03-09 16:10:52 +01:00
if (!instance.isOwned()) return undefined
2021-03-09 16:10:52 +01:00
// Lazy load channels
if (!instance.VideoChannel) {
instance.VideoChannel = await instance.$get('VideoChannel', {
include: [
ActorModel,
AccountModel
],
transaction: options.transaction
}) as MChannelAccountDefault
}
2021-03-09 16:10:52 +01:00
return sendDeleteVideo(instance, options.transaction)
}
2018-04-25 10:21:38 +02:00
@BeforeDestroy
2021-06-11 16:22:54 +02:00
static async removeFiles (instance: VideoModel, options) {
const tasks: Promise<any>[] = []
2016-11-11 15:20:03 +01:00
2018-07-30 17:02:40 +02:00
logger.info('Removing files of video %s.', instance.url)
2018-04-25 10:21:38 +02:00
2017-12-12 17:53:50 +01:00
if (instance.isOwned()) {
if (!Array.isArray(instance.VideoFiles)) {
2021-06-11 16:22:54 +02:00
instance.VideoFiles = await instance.$get('VideoFiles', { transaction: options.transaction })
}
2017-12-12 17:53:50 +01:00
// Remove physical files and torrents
instance.VideoFiles.forEach(file => {
tasks.push(instance.removeWebVideoFile(file))
2017-12-12 17:53:50 +01:00
})
2019-01-29 08:37:25 +01:00
// Remove playlists file
2020-01-24 16:48:05 +01:00
if (!Array.isArray(instance.VideoStreamingPlaylists)) {
2021-06-11 16:22:54 +02:00
instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists', { transaction: options.transaction })
2020-01-24 16:48:05 +01:00
}
for (const p of instance.VideoStreamingPlaylists) {
tasks.push(instance.removeStreamingPlaylistFiles(p))
}
// Remove source files
const promiseRemoveSources = VideoSourceModel.listAll(instance.id, options.transaction)
.then(sources => Promise.all(sources.map(s => instance.removeOriginalFile(s))))
tasks.push(promiseRemoveSources)
2017-12-12 17:53:50 +01:00
}
2018-04-25 10:21:38 +02:00
// Do not wait video deletion because we could be in a transaction
Promise.all(tasks)
.then(() => logger.info('Removed files of video %s.', instance.url))
.catch(err => logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err }))
2018-04-25 10:21:38 +02:00
return undefined
2017-12-12 17:53:50 +01:00
}
2016-11-11 15:20:03 +01:00
@BeforeDestroy
static async saveEssentialDataToAbuses (instance: VideoModel, options) {
const tasks: Promise<any>[] = []
if (!Array.isArray(instance.VideoAbuses)) {
2021-06-11 16:22:54 +02:00
instance.VideoAbuses = await instance.$get('VideoAbuses', { transaction: options.transaction })
if (instance.VideoAbuses.length === 0) return undefined
}
2020-07-07 10:57:04 +02:00
logger.info('Saving video abuses details of video %s.', instance.url)
2021-03-09 16:10:52 +01:00
if (!instance.Trackers) instance.Trackers = await instance.$get('Trackers', { transaction: options.transaction })
const details = instance.toFormattedDetailsJSON()
for (const abuse of instance.VideoAbuses) {
abuse.deletedVideo = details
tasks.push(abuse.save({ transaction: options.transaction }))
}
2021-06-11 16:22:54 +02:00
await Promise.all(tasks)
}
static listLocalIds (): Promise<number[]> {
const query = {
attributes: [ 'id' ],
raw: true,
where: {
remote: false
}
}
return VideoModel.findAll(query)
.then(rows => rows.map(r => r.id))
}
2017-12-14 17:38:41 +01:00
static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
2017-12-12 17:53:50 +01:00
function getRawQuery (select: string) {
const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
2017-12-14 17:38:41 +01:00
'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
'WHERE "Account"."actorId" = ' + actorId
2017-12-12 17:53:50 +01:00
const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
2017-12-14 17:38:41 +01:00
'WHERE "VideoShare"."actorId" = ' + actorId
2017-12-12 17:53:50 +01:00
return `(${queryVideo}) UNION (${queryVideoShare})`
}
2017-12-12 17:53:50 +01:00
const rawQuery = getRawQuery('"Video"."id"')
const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
const query = {
distinct: true,
offset: start,
limit: count,
2021-10-22 16:39:37 +02:00
order: getVideoSort('-createdAt', [ 'Tags', 'name', 'ASC' ]),
2017-12-12 17:53:50 +01:00
where: {
id: {
2020-01-31 16:56:52 +01:00
[Op.in]: Sequelize.literal('(' + rawQuery + ')')
},
[Op.or]: getPrivaciesForFederation()
2017-12-12 17:53:50 +01:00
},
include: [
2018-07-12 19:02:00 +02:00
{
2021-04-08 15:04:14 +02:00
attributes: [ 'filename', 'language', 'fileUrl' ],
2018-07-12 19:02:00 +02:00
model: VideoCaptionModel.unscoped(),
required: false
},
2023-06-01 14:51:16 +02:00
{
model: StoryboardModel.unscoped(),
required: false
},
2017-12-12 17:53:50 +01:00
{
attributes: [ 'id', 'url' ],
model: VideoShareModel.unscoped(),
2017-12-12 17:53:50 +01:00
required: false,
2018-05-28 12:13:00 +02:00
// We only want videos shared by this actor
where: {
2020-01-31 16:56:52 +01:00
[Op.and]: [
2018-05-28 12:13:00 +02:00
{
id: {
2020-01-31 16:56:52 +01:00
[Op.not]: null
2018-05-28 12:13:00 +02:00
}
},
{
actorId
}
]
},
2017-12-14 17:38:41 +01:00
include: [
{
attributes: [ 'id', 'url' ],
model: ActorModel.unscoped()
2017-12-14 17:38:41 +01:00
}
]
2017-12-12 17:53:50 +01:00
},
{
model: VideoChannelModel.unscoped(),
2017-12-12 17:53:50 +01:00
required: true,
include: [
{
attributes: [ 'name' ],
model: AccountModel.unscoped(),
required: true,
include: [
{
2018-05-28 12:13:00 +02:00
attributes: [ 'id', 'url', 'followersUrl' ],
model: ActorModel.unscoped(),
required: true
}
]
},
{
2018-05-28 12:13:00 +02:00
attributes: [ 'id', 'url', 'followersUrl' ],
model: ActorModel.unscoped(),
2017-12-12 17:53:50 +01:00
required: true
}
]
},
2020-11-02 15:43:44 +01:00
{
model: VideoStreamingPlaylistModel.unscoped(),
required: false,
include: [
{
model: VideoFileModel,
required: false
}
]
},
2020-11-06 11:22:12 +01:00
VideoLiveModel.unscoped(),
2017-12-12 17:53:50 +01:00
VideoFileModel,
TagModel
2017-12-12 17:53:50 +01:00
]
}
2017-12-12 17:53:50 +01:00
return Bluebird.all([
2019-04-23 09:50:57 +02:00
VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
2017-12-12 17:53:50 +01:00
]).then(([ rows, totals ]) => {
// totals: totalVideos + totalVideoShares
let totalVideos = 0
let totalVideoShares = 0
2020-01-31 16:56:52 +01:00
if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
2017-12-12 17:53:50 +01:00
const total = totalVideos + totalVideoShares
return {
data: rows,
2022-07-13 11:58:01 +02:00
total
2017-12-12 17:53:50 +01:00
}
})
}
2021-05-28 13:05:59 +02:00
static async listPublishedLiveUUIDs () {
2020-11-13 14:36:30 +01:00
const options = {
2021-05-28 13:05:59 +02:00
attributes: [ 'uuid' ],
2020-11-13 14:36:30 +01:00
where: {
isLive: true,
2021-05-26 12:03:44 +02:00
remote: false,
2020-11-13 14:36:30 +01:00
state: VideoState.PUBLISHED
}
}
2020-12-08 14:30:29 +01:00
const result = await VideoModel.findAll(options)
2021-05-28 13:05:59 +02:00
return result.map(v => v.uuid)
2020-11-13 14:36:30 +01:00
}
2021-01-20 15:28:34 +01:00
static listUserVideosForApi (options: {
accountId: number
start: number
count: number
sort: string
channelId?: number
isLive?: boolean
search?: string
2021-01-20 15:28:34 +01:00
}) {
const { accountId, channelId, start, count, sort, search, isLive } = options
2021-01-20 15:28:34 +01:00
2022-05-24 09:16:42 +02:00
function buildBaseQuery (forCount: boolean): FindOptions {
const where: WhereOptions = {}
if (search) {
where.name = {
[Op.iLike]: '%' + search + '%'
}
}
2021-10-20 10:04:06 +02:00
if (exists(isLive)) {
where.isLive = isLive
}
const channelWhere = channelId
? { id: channelId }
: {}
const baseQuery = {
2019-04-23 09:50:57 +02:00
offset: start,
limit: count,
where,
2019-04-23 09:50:57 +02:00
order: getVideoSort(sort),
include: [
{
2022-07-18 14:53:50 +02:00
model: forCount
? VideoChannelModel.unscoped()
: VideoChannelModel,
2019-04-23 09:50:57 +02:00
required: true,
where: channelWhere,
2019-04-23 09:50:57 +02:00
include: [
{
2022-05-24 09:16:42 +02:00
model: forCount
? AccountModel.unscoped()
: AccountModel,
2019-04-23 09:50:57 +02:00
where: {
id: accountId
},
required: true
}
]
}
]
}
return baseQuery
2017-12-12 17:53:50 +01:00
}
2017-10-16 10:05:49 +02:00
2022-05-24 09:16:42 +02:00
const countQuery = buildBaseQuery(true)
const findQuery = buildBaseQuery(false)
2019-04-23 09:50:57 +02:00
const findScopes: (string | ScopeOptions)[] = [
2019-04-26 10:20:58 +02:00
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_BLACKLISTED,
ScopeNames.WITH_THUMBNAILS
]
2019-04-23 09:50:57 +02:00
return Promise.all([
VideoModel.count(countQuery),
2019-08-20 13:52:49 +02:00
VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
2019-04-23 09:50:57 +02:00
]).then(([ count, rows ]) => {
return {
2019-08-20 13:52:49 +02:00
data: rows,
2019-04-23 09:50:57 +02:00
total: count
}
})
2017-12-12 17:53:50 +01:00
}
2018-04-24 17:05:32 +02:00
static async listForApi (options: {
2020-01-31 16:56:52 +01:00
start: number
count: number
sort: string
2020-01-31 16:56:52 +01:00
nsfw: boolean
isLive?: boolean
isLocal?: boolean
include?: VideoIncludeType
hasFiles?: boolean // default false
hasWebtorrentFiles?: boolean // TODO: remove in v7
hasWebVideoFiles?: boolean
2021-11-03 11:32:41 +01:00
hasHLSFiles?: boolean
2020-01-31 16:56:52 +01:00
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
privacyOneOf?: VideoPrivacyType[]
2020-01-31 16:56:52 +01:00
accountId?: number
videoChannelId?: number
displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
2020-01-31 16:56:52 +01:00
videoPlaylistId?: number
2020-01-31 16:56:52 +01:00
trendingDays?: number
2020-01-31 16:56:52 +01:00
user?: MUserAccountId
historyOfUser?: MUserId
2020-01-08 14:15:16 +01:00
countVideos?: boolean
search?: string
excludeAlreadyWatched?: boolean
2020-01-08 14:15:16 +01:00
}) {
2021-11-03 11:32:41 +01:00
VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
2020-03-05 15:04:57 +01:00
const trendingDays = options.sort.endsWith('trending')
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
: undefined
2021-07-29 11:54:38 +02:00
let trendingAlgorithm: string
if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
if (options.sort.endsWith('best')) trendingAlgorithm = 'best'
const serverActor = await getServerActor()
const queryOptions = {
2021-07-29 14:17:03 +02:00
...pick(options, [
'start',
'count',
'sort',
'nsfw',
'isLive',
'categoryOneOf',
'licenceOneOf',
'languageOneOf',
'tagsOneOf',
'tagsAllOf',
'privacyOneOf',
'isLocal',
'include',
'displayOnlyForFollower',
'hasFiles',
2021-07-29 14:17:03 +02:00
'accountId',
'videoChannelId',
'videoPlaylistId',
'user',
'historyOfUser',
2021-11-03 11:32:41 +01:00
'hasHLSFiles',
'hasWebtorrentFiles',
'hasWebVideoFiles',
'search',
'excludeAlreadyWatched'
2021-07-29 14:17:03 +02:00
]),
serverAccountIdForBlock: serverActor.Account.id,
trendingDays,
2021-07-29 14:17:03 +02:00
trendingAlgorithm
2018-04-24 17:05:32 +02:00
}
2020-03-05 15:04:57 +01:00
return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
}
2018-07-20 18:31:49 +02:00
static async searchAndPopulateAccountAndServer (options: {
2021-07-29 14:17:03 +02:00
start: number
count: number
sort: string
2021-11-03 11:32:41 +01:00
2018-07-20 18:31:49 +02:00
nsfw?: boolean
isLive?: boolean
isLocal?: boolean
include?: VideoIncludeType
2021-11-03 11:32:41 +01:00
2018-07-20 18:31:49 +02:00
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
privacyOneOf?: VideoPrivacyType[]
2021-11-03 11:32:41 +01:00
displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
user?: MUserAccountId
hasWebtorrentFiles?: boolean // TODO: remove in v7
hasWebVideoFiles?: boolean
2021-11-03 11:32:41 +01:00
hasHLSFiles?: boolean
search?: string
host?: string
startDate?: string // ISO 8601
endDate?: string // ISO 8601
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
2018-07-20 18:31:49 +02:00
durationMin?: number // seconds
durationMax?: number // seconds
uuids?: string[]
excludeAlreadyWatched?: boolean
2023-10-25 14:53:53 +02:00
countVideos?: boolean
2018-07-20 18:31:49 +02:00
}) {
2021-11-03 11:32:41 +01:00
VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
2021-11-03 11:32:41 +01:00
const serverActor = await getServerActor()
const queryOptions = {
2021-07-29 14:17:03 +02:00
...pick(options, [
'include',
2021-07-29 14:17:03 +02:00
'nsfw',
'isLive',
'categoryOneOf',
'licenceOneOf',
'languageOneOf',
'tagsOneOf',
'tagsAllOf',
'privacyOneOf',
2021-07-29 14:17:03 +02:00
'user',
'isLocal',
2021-07-29 14:17:03 +02:00
'host',
'start',
'count',
'sort',
'startDate',
'endDate',
'originallyPublishedStartDate',
'originallyPublishedEndDate',
'durationMin',
'durationMax',
2021-11-03 11:32:41 +01:00
'hasHLSFiles',
'hasWebtorrentFiles',
'hasWebVideoFiles',
2021-07-29 14:17:03 +02:00
'uuids',
'search',
'displayOnlyForFollower',
'excludeAlreadyWatched'
2021-07-29 14:17:03 +02:00
]),
serverAccountIdForBlock: serverActor.Account.id
2018-04-24 17:05:32 +02:00
}
2023-10-25 14:53:53 +02:00
return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
}
2022-07-27 16:19:25 +02:00
static countLives (options: {
remote: boolean
mode: 'published' | 'not-ended'
}) {
const query = {
2020-10-28 15:24:40 +01:00
where: {
2022-07-27 16:19:25 +02:00
remote: options.remote,
isLive: true,
2022-07-27 16:19:25 +02:00
state: options.mode === 'not-ended'
? { [Op.ne]: VideoState.LIVE_ENDED }
: { [Op.eq]: VideoState.PUBLISHED }
2020-10-28 15:24:40 +01:00
}
}
2022-07-27 16:19:25 +02:00
return VideoModel.count(query)
2020-10-28 15:24:40 +01:00
}
static countVideosUploadedByUserSince (userId: number, since: Date) {
const options = {
include: [
{
model: VideoChannelModel.unscoped(),
required: true,
include: [
{
model: AccountModel.unscoped(),
required: true,
include: [
{
model: UserModel.unscoped(),
required: true,
where: {
id: userId
}
}
]
}
]
}
],
where: {
createdAt: {
[Op.gte]: since
}
}
}
return VideoModel.unscoped().count(options)
}
2020-10-28 15:24:40 +01:00
static countLivesOfAccount (accountId: number) {
const options = {
where: {
remote: false,
isLive: true,
state: {
[Op.ne]: VideoState.LIVE_ENDED
}
2020-10-28 15:24:40 +01:00
},
include: [
{
required: true,
model: VideoChannelModel.unscoped(),
where: {
accountId
}
}
]
}
return VideoModel.count(options)
}
2021-06-11 14:09:33 +02:00
static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2017-10-16 10:05:49 +02:00
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' })
2017-12-12 17:53:50 +01:00
}
2017-10-16 10:05:49 +02:00
2021-06-11 14:09:33 +02:00
static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2019-08-22 10:46:54 +02:00
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' })
2019-08-22 10:46:54 +02:00
}
2020-12-08 14:30:29 +01:00
static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> {
const fun = () => {
const query = {
where: buildWhereIdOrUUID(id),
transaction: t
}
return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
}
return ModelCache.Instance.doCache({
cacheType: 'load-video-immutable-id',
key: '' + id,
deleteKey: 'video',
fun
})
}
2020-12-08 14:30:29 +01:00
static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> {
const fun = () => {
const query: FindOptions = {
where: {
url
},
transaction
}
return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
}
return ModelCache.Instance.doCache({
cacheType: 'load-video-immutable-url',
key: url,
deleteKey: 'video',
fun
})
}
2021-06-11 14:09:33 +02:00
static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ id, transaction, type: 'id' })
}
2021-06-11 14:09:33 +02:00
static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging })
}
2017-10-31 11:52:52 +01:00
2021-06-11 14:09:33 +02:00
static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2019-01-29 08:37:25 +01:00
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' })
}
2024-02-12 10:47:52 +01:00
static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLight> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ url, transaction, type: 'account' })
}
static loadByUrlAndPopulateAccountAndFiles (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' })
}
2022-06-28 14:57:51 +02:00
static loadFull (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2019-01-29 08:37:25 +01:00
2022-06-28 14:57:51 +02:00
return queryBuilder.queryVideo({ id, transaction: t, type: 'full', userId })
2019-01-29 08:37:25 +01:00
}
2019-07-19 17:30:41 +02:00
static loadForGetAPI (parameters: {
2020-01-31 16:56:52 +01:00
id: number | string
transaction?: Transaction
2019-07-19 17:30:41 +02:00
userId?: number
2020-12-08 14:30:29 +01:00
}): Promise<MVideoDetails> {
const { id, transaction, userId } = parameters
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
2019-01-29 08:37:25 +01:00
2021-06-11 14:09:33 +02:00
return queryBuilder.queryVideo({ id, transaction, type: 'api', userId })
2017-12-28 11:16:08 +01:00
}
2018-02-28 18:04:46 +01:00
static async getStats () {
2022-07-05 15:43:21 +02:00
const serverActor = await getServerActor()
2018-02-28 18:04:46 +01:00
let totalLocalVideoViews = await VideoModel.sum('views', {
where: {
remote: false
}
})
2020-03-13 13:43:26 +01:00
2018-02-28 18:04:46 +01:00
// Sequelize could return null...
if (!totalLocalVideoViews) totalLocalVideoViews = 0
2022-07-05 15:43:21 +02:00
const baseOptions = {
2020-03-13 13:43:26 +01:00
start: 0,
count: 0,
sort: '-publishedAt',
2022-07-05 15:43:21 +02:00
nsfw: null,
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
}
2022-07-05 15:43:21 +02:00
}
const { total: totalLocalVideos } = await VideoModel.listForApi({
...baseOptions,
isLocal: true
2020-03-13 13:43:26 +01:00
})
2022-07-05 15:43:21 +02:00
const { total: totalVideos } = await VideoModel.listForApi(baseOptions)
2018-02-28 18:04:46 +01:00
return {
totalLocalVideos,
totalLocalVideoViews,
totalVideos
}
}
2024-02-12 10:47:52 +01:00
static loadByNameAndChannel (channel: MChannelId, name: string): Promise<MVideo> {
return VideoModel.unscoped().findOne({
where: {
name,
channelId: channel.id
}
})
}
2018-08-29 16:26:25 +02:00
static incrementViews (id: number, views: number) {
return VideoModel.increment('views', {
by: views,
where: {
id
}
})
}
static updateRatesOf (videoId: number, type: VideoRateType, count: number, t: Transaction) {
const field = type === 'like'
? 'likes'
: 'dislikes'
const rawQuery = `UPDATE "video" SET "${field}" = :count WHERE "video"."id" = :videoId`
return AccountVideoRateModel.sequelize.query(rawQuery, {
transaction: t,
replacements: { videoId, rateType: type, count },
type: QueryTypes.UPDATE
})
}
static syncLocalRates (videoId: number, type: VideoRateType, t: Transaction) {
const field = type === 'like'
? 'likes'
: 'dislikes'
const rawQuery = `UPDATE "video" SET "${field}" = ` +
'(' +
2021-06-11 14:45:58 +02:00
'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' +
') ' +
'WHERE "video"."id" = :videoId'
return AccountVideoRateModel.sequelize.query(rawQuery, {
transaction: t,
replacements: { videoId, rateType: type },
type: QueryTypes.UPDATE
})
}
static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
// Instances only share videos
const query = 'SELECT 1 FROM "videoShare" ' +
2020-01-31 16:56:52 +01:00
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
2020-06-09 15:59:35 +02:00
'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' +
'UNION ' +
'SELECT 1 FROM "video" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "account"."actorId" ' +
'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "video"."id" = $videoId ' +
2020-01-31 16:56:52 +01:00
'LIMIT 1'
const options = {
2019-10-21 14:50:55 +02:00
type: QueryTypes.SELECT as QueryTypes.SELECT,
bind: { followerActorId, videoId },
raw: true
}
return VideoModel.sequelize.query(query, options)
.then(results => results.length === 1)
}
2021-06-11 14:45:58 +02:00
static bulkUpdateSupportField (ofChannel: MChannel, t: Transaction) {
const options = {
where: {
2021-06-11 14:45:58 +02:00
channelId: ofChannel.id
},
transaction: t
}
2021-06-11 14:45:58 +02:00
return VideoModel.update({ support: ofChannel.support }, options)
}
2024-02-28 16:22:51 +01:00
static async getAllIdsFromChannel (videoChannel: MChannelId, limit?: number): Promise<number[]> {
const videos = await VideoModel.findAll({
attributes: [ 'id' ],
where: {
channelId: videoChannel.id
2024-02-28 16:22:51 +01:00
},
limit
})
2024-02-28 16:22:51 +01:00
return videos.map(v => v.id)
}
2018-08-30 14:58:00 +02:00
// threshold corresponds to how many video the field should have to be returned
2018-09-14 11:52:23 +02:00
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
const serverActor = await getServerActor()
2018-09-14 11:52:23 +02:00
2021-06-10 08:53:32 +02:00
const queryOptions: BuildVideosListQueryOptions = {
2020-03-05 15:04:57 +01:00
attributes: [ `"${field}"` ],
group: `GROUP BY "${field}"`,
having: `HAVING COUNT("${field}") >= ${threshold}`,
start: 0,
sort: 'random',
count,
serverAccountIdForBlock: serverActor.Account.id,
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
}
2018-09-14 11:52:23 +02:00
}
2021-06-10 08:53:32 +02:00
const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize)
2018-08-30 14:58:00 +02:00
2021-06-10 08:53:32 +02:00
return queryBuilder.queryVideoIds(queryOptions)
.then(rows => rows.map(r => r[field]))
2018-08-30 14:58:00 +02:00
}
2018-09-14 09:57:21 +02:00
static buildTrendingQuery (trendingDays: number) {
return {
attributes: [],
subQuery: false,
model: VideoViewModel,
required: false,
where: {
startDate: {
2022-01-14 14:15:23 +01:00
// FIXME: ts error
[Op.gte as any]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
2018-09-14 09:57:21 +02:00
}
}
}
}
2018-10-05 11:15:06 +02:00
private static async getAvailableForApi (
2021-06-10 08:53:32 +02:00
options: BuildVideosListQueryOptions,
2018-10-05 11:15:06 +02:00
countVideos = true
2020-06-05 10:42:36 +02:00
): Promise<ResultList<VideoModel>> {
2022-07-28 10:56:05 +02:00
const span = tracer.startSpan('peertube.VideoModel.getAvailableForApi')
2020-03-09 14:44:44 +01:00
function getCount () {
if (countVideos !== true) return Promise.resolve(undefined)
2018-09-03 18:05:12 +02:00
2020-03-09 14:44:44 +01:00
const countOptions = Object.assign({}, options, { isCount: true })
2021-06-10 08:53:32 +02:00
const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize)
2021-06-10 08:53:32 +02:00
return queryBuilder.countVideoIds(countOptions)
2020-03-09 14:44:44 +01:00
}
function getModels () {
2020-03-13 13:43:26 +01:00
if (options.count === 0) return Promise.resolve([])
2021-06-10 08:53:32 +02:00
const queryBuilder = new VideosModelListQueryBuilder(VideoModel.sequelize)
2020-03-09 14:44:44 +01:00
2021-06-10 08:53:32 +02:00
return queryBuilder.queryVideos(options)
2020-03-09 14:44:44 +01:00
}
const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
2022-07-28 10:56:05 +02:00
span.end()
return {
data: rows,
total: count
}
}
private static throwIfPrivateIncludeWithoutUser (include: VideoIncludeType, user: MUserAccountId) {
2021-11-03 11:32:41 +01:00
if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
2023-07-28 16:28:16 +02:00
throw new Error('Try to include protected videos but user cannot see all videos')
}
}
private static throwIfPrivacyOneOfWithoutUser (privacyOneOf: VideoPrivacyType[], user: MUserAccountId) {
if (privacyOneOf && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
throw new Error('Try to choose video privacies but user cannot see all videos')
2021-11-03 11:32:41 +01:00
}
}
private static isPrivateInclude (include: VideoIncludeType) {
return include & VideoInclude.BLACKLISTED ||
include & VideoInclude.BLOCKED_OWNER ||
include & VideoInclude.NOT_PUBLISHED_STATE
}
2019-07-23 12:04:15 +02:00
isBlacklisted () {
return !!this.VideoBlacklist
}
2019-07-31 15:57:32 +02:00
isBlocked () {
2020-06-17 10:55:40 +02:00
return this.VideoChannel.Account.Actor.Server?.isBlocked() || this.VideoChannel.Account.isBlocked()
2019-07-31 15:57:32 +02:00
}
2020-01-31 16:56:52 +01:00
getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
2022-08-09 09:09:31 +02:00
const files = this.getAllFiles()
const file = fun(files, file => file.resolution)
if (!file) return undefined
2022-08-09 09:09:31 +02:00
if (file.videoId) {
return Object.assign(file, { Video: this })
}
2022-08-09 09:09:31 +02:00
if (file.videoStreamingPlaylistId) {
const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
}
2022-08-09 09:09:31 +02:00
throw new Error('File is not associated to a video of a playlist')
2017-11-09 17:51:58 +01:00
}
2020-01-31 16:56:52 +01:00
getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
return this.getQualityFileBy(maxBy)
}
2020-01-31 16:56:52 +01:00
getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
return this.getQualityFileBy(minBy)
}
getWebVideoFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
2019-08-01 14:19:18 +02:00
if (Array.isArray(this.VideoFiles) === false) return undefined
const file = this.VideoFiles.find(f => f.resolution === resolution)
if (!file) return undefined
return Object.assign(file, { Video: this })
2019-08-01 14:19:18 +02:00
}
hasWebVideoFiles () {
return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0
}
2021-06-08 17:29:45 +02:00
async addAndSaveThumbnail (thumbnail: MThumbnail, transaction?: Transaction) {
2019-04-23 09:50:57 +02:00
thumbnail.videoId = this.id
const savedThumbnail = await thumbnail.save({ transaction })
if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
2021-11-10 15:13:56 +01:00
this.Thumbnails = this.Thumbnails.filter(t => t.id !== savedThumbnail.id)
2019-04-23 09:50:57 +02:00
this.Thumbnails.push(savedThumbnail)
}
2024-02-15 09:15:55 +01:00
// ---------------------------------------------------------------------------
hasMiniature () {
return !!this.getMiniature()
}
2019-04-23 09:50:57 +02:00
getMiniature () {
if (Array.isArray(this.Thumbnails) === false) return undefined
2019-04-23 09:50:57 +02:00
return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
}
hasPreview () {
return !!this.getPreview()
}
getPreview () {
if (Array.isArray(this.Thumbnails) === false) return undefined
return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
2017-12-12 17:53:50 +01:00
}
2016-12-29 19:07:05 +01:00
2024-02-15 09:15:55 +01:00
// ---------------------------------------------------------------------------
2017-12-12 17:53:50 +01:00
isOwned () {
return this.remote === false
2017-10-30 10:16:27 +01:00
}
2018-12-26 10:36:24 +01:00
getWatchStaticPath () {
return buildVideoWatchPath({ shortUUID: uuidToShort(this.uuid) })
2018-12-26 10:36:24 +01:00
}
2018-07-12 19:02:00 +02:00
getEmbedStaticPath () {
2021-07-26 15:04:37 +02:00
return buildVideoEmbedPath(this)
2017-12-12 17:53:50 +01:00
}
2017-11-09 17:51:58 +01:00
2019-04-23 09:50:57 +02:00
getMiniatureStaticPath () {
const thumbnail = this.getMiniature()
if (!thumbnail) return null
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
return thumbnail.getLocalStaticPath()
2017-11-09 17:51:58 +01:00
}
2018-07-12 19:02:00 +02:00
getPreviewStaticPath () {
const preview = this.getPreview()
if (!preview) return null
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
return preview.getLocalStaticPath()
2017-12-12 17:53:50 +01:00
}
2019-08-21 14:31:57 +02:00
toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
return videoModelToFormattedJSON(this, options)
}
2019-08-21 14:31:57 +02:00
toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
return videoModelToFormattedDetailsJSON(this)
}
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
getFormattedWebVideoFilesJSON (includeMagnet = true): VideoFile[] {
return videoFilesModelToFormattedJSON(this, this.VideoFiles, { includeMagnet })
}
getFormattedHLSVideoFilesJSON (includeMagnet = true): VideoFile[] {
let acc: VideoFile[] = []
for (const p of this.VideoStreamingPlaylists) {
acc = acc.concat(videoFilesModelToFormattedJSON(this, p.VideoFiles, { includeMagnet }))
}
return acc
}
getFormattedAllVideoFilesJSON (includeMagnet = true): VideoFile[] {
2021-01-27 16:42:13 +01:00
let files: VideoFile[] = []
2020-08-24 16:11:37 +02:00
if (Array.isArray(this.VideoFiles)) {
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
files = files.concat(this.getFormattedWebVideoFilesJSON(includeMagnet))
2020-08-24 16:11:37 +02:00
}
Add Podcast RSS feeds (#5487) * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Initial test implementation of Podcast RSS This is a pretty simple implementation to add support for The Podcast Namespace in RSS -- instead of affecting the existing RSS implementation, this adds a new UI option. I attempted to retain compatibility with the rest of the RSS feed implementation as much as possible and have created a temporary fork of the "pfeed" library to support this effort. * Update to pfeed-podcast 1.2.2 * Add correct feed image to RSS channel * Prefer HLS videos for podcast RSS Remove video/stream titles, add optional height attribute to podcast RSS * Prefix podcast RSS images with root server URL * Add optional video query support to include captions * Add transcripts & person images to podcast RSS feed * Prefer webseed/webtorrent files over HLS fragmented mp4s * Experimentally adding podcast fields to basic config page * Add validation for new basic config fields * Don't include "content" in podcast feed, use full description for "description" * Add medium/socialInteract to podcast RSS feeds. Use HTML for description * Change base production image to bullseye, install prosody in image * Add liveItem and trackers to Podcast RSS feeds Remove height from alternateEnclosure, replaced with title. * Clear Podcast RSS feed cache when live streams start/end * Upgrade to Node 16 * Refactor clearCacheRoute to use ApiCache * Remove unnecessary type hint * Update dockerfile to node 16, install python-is-python2 * Use new file paths for captions/playlists * Fix legacy videos in RSS after migration to object storage * Improve method of identifying non-fragmented mp4s in podcast RSS feeds * Don't include fragmented MP4s in podcast RSS feeds * Add experimental support for podcast:categories on the podcast RSS item * Fix undefined category when no videos exist Allows for empty feeds to exist (important for feeds that might only go live) * Add support for podcast:locked -- user has to opt in to show their email * Use comma for podcast:categories delimiter * Make cache clearing async * Fix merge, temporarily test with pfeed-podcast * Syntax changes * Add EXT_MIMETYPE constants for captions * Update & fix tests, fix enclosure mimetypes, remove admin email * Add test for podacst:socialInteract * Add filters hooks for podcast customTags * Remove showdown, updated to pfeed-podcast 6.1.2 * Add 'action:api.live-video.state.updated' hook * Avoid assigning undefined category to podcast feeds * Remove nvmrc * Remove comment * Remove unused podcast config * Remove more unused podcast config * Fix MChannelAccountDefault type hint missed in merge * Remove extra line * Re-add newline in config * Fix lint errors for isEmailPublic * Fix thumbnails in podcast feeds * Requested changes based on review * Provide podcast rss 2.0 only on video channels * Misc cleanup for a less messy PR * Lint fixes * Remove pfeed-podcast * Add peertube version to new hooks * Don't use query include, remove TODO * Remove film medium hack * Clear podcast rss cache before video/channel update hooks * Clear podcast rss cache before video uploaded/deleted hooks * Refactor podcast feed cache clearing * Set correct person name from video channel * Styling * Fix tests --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
2023-05-22 16:00:05 +02:00
if (Array.isArray(this.VideoStreamingPlaylists)) {
files = files.concat(this.getFormattedHLSVideoFilesJSON(includeMagnet))
2020-08-24 16:11:37 +02:00
}
2021-01-27 16:42:13 +01:00
return files
2017-12-12 17:53:50 +01:00
}
2017-11-09 17:51:58 +01:00
toActivityPubObject (this: MVideoAP): Promise<VideoObject> {
return Hooks.wrapObject(
videoModelToActivityPubObject(this),
2023-03-10 15:08:56 +01:00
'filter:activity-pub.video.json-ld.build.result',
{ video: this }
)
2017-12-12 17:53:50 +01:00
}
2023-06-01 14:51:16 +02:00
async lightAPToFullAP (this: MVideoAPLight, transaction: Transaction): Promise<MVideoAP> {
const videoAP = this as MVideoAP
const getCaptions = () => {
if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions
return this.$get('VideoCaptions', {
attributes: [ 'filename', 'language', 'fileUrl' ],
transaction
}) as Promise<MVideoCaptionLanguageUrl[]>
}
const getStoryboard = () => {
if (videoAP.Storyboard) return videoAP.Storyboard
return this.$get('Storyboard', { transaction }) as Promise<MStoryboard>
}
const [ captions, storyboard ] = await Promise.all([ getCaptions(), getStoryboard() ])
return Object.assign(this, {
VideoCaptions: captions,
Storyboard: storyboard
})
}
2017-12-12 17:53:50 +01:00
getTruncatedDescription () {
if (!this.description) return null
const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
return peertubeTruncate(this.description, { length: maxLength })
}
getAllFiles () {
let files: MVideoFile[] = []
if (Array.isArray(this.VideoFiles)) {
files = files.concat(this.VideoFiles)
}
if (Array.isArray(this.VideoStreamingPlaylists)) {
for (const p of this.VideoStreamingPlaylists) {
if (Array.isArray(p.VideoFiles)) {
files = files.concat(p.VideoFiles)
}
}
}
return files
}
2022-02-11 10:51:33 +01:00
probeMaxQualityFile () {
const file = this.getMaxQualityFile()
const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
2017-11-10 14:34:45 +01:00
return VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(videoOrPlaylist), async originalFilePath => {
const probe = await ffprobePromise(originalFilePath)
const { audioStream } = await getAudioStream(originalFilePath, probe)
const hasAudio = await hasAudioStream(originalFilePath, probe)
const fps = await getVideoStreamFPS(originalFilePath, probe)
return {
audioStream,
hasAudio,
fps,
2022-02-11 10:51:33 +01:00
...await getVideoStreamDimensionsInfo(originalFilePath, probe)
}
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
})
2017-12-12 17:53:50 +01:00
}
2017-11-10 14:34:45 +01:00
getDescriptionAPIPath () {
2017-12-12 17:53:50 +01:00
return `/api/${API_VERSION}/videos/${this.uuid}/description`
2016-12-11 21:50:51 +01:00
}
getHLSPlaylist (): MStreamingPlaylistFilesVideo {
2019-08-09 15:04:36 +02:00
if (!this.VideoStreamingPlaylists) return undefined
const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
if (!playlist) return undefined
return playlist.withVideo(this)
2019-08-09 15:04:36 +02:00
}
setHLSPlaylist (playlist: MStreamingPlaylist) {
const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
2018-12-04 17:08:55 +01:00
if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
this.VideoStreamingPlaylists = toAdd
return
}
this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
2020-01-31 16:56:52 +01:00
.filter(s => s.type !== VideoStreamingPlaylistType.HLS)
.concat(toAdd)
}
removeWebVideoFile (videoFile: MVideoFile, isRedundancy = false) {
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
const filePath = isRedundancy
? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile)
: VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile)
2021-07-23 11:20:00 +02:00
const promises: Promise<any>[] = [ remove(filePath) ]
if (!isRedundancy) promises.push(videoFile.removeTorrent())
2024-02-12 10:47:52 +01:00
if (videoFile.storage === FileStorage.OBJECT_STORAGE) {
promises.push(removeWebVideoObjectStorage(videoFile))
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
}
2021-07-23 11:20:00 +02:00
return Promise.all(promises)
2016-12-11 21:50:51 +01:00
}
2020-01-24 16:48:05 +01:00
async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
const directoryPath = isRedundancy
? getHLSRedundancyDirectory(this)
: getHLSDirectory(this)
2019-01-29 08:37:25 +01:00
try {
await remove(directoryPath)
} catch (err) {
// If it's a live, ffmpeg may have added another file while fs-extra is removing the directory
// So wait a little bit and retry
if (err.code === 'ENOTEMPTY') {
await wait(1000)
await remove(directoryPath)
return
}
throw err
}
2020-01-24 16:48:05 +01:00
if (isRedundancy !== true) {
2020-01-31 16:56:52 +01:00
const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
2020-01-24 16:48:05 +01:00
streamingPlaylistWithFiles.Video = this
if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
}
// Remove physical files and torrents
await Promise.all(
streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
2020-01-24 16:48:05 +01:00
)
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
2024-02-12 10:47:52 +01:00
if (streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
2021-11-18 14:35:08 +01:00
await removeHLSObjectStorage(streamingPlaylist.withVideo(this))
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
}
2020-01-24 16:48:05 +01:00
}
2019-01-29 08:37:25 +01:00
}
async removeStreamingPlaylistVideoFile (streamingPlaylist: MStreamingPlaylist, videoFile: MVideoFile) {
const filePath = VideoPathManager.Instance.getFSHLSOutputPath(this, videoFile.filename)
await videoFile.removeTorrent()
await remove(filePath)
const resolutionFilename = getHlsResolutionPlaylistFilename(videoFile.filename)
await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename))
2024-02-12 10:47:52 +01:00
if (videoFile.storage === FileStorage.OBJECT_STORAGE) {
await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), videoFile.filename)
await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), resolutionFilename)
}
}
async removeStreamingPlaylistFile (streamingPlaylist: MStreamingPlaylist, filename: string) {
const filePath = VideoPathManager.Instance.getFSHLSOutputPath(this, filename)
await remove(filePath)
2024-02-12 10:47:52 +01:00
if (streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), filename)
}
}
async removeOriginalFile (videoSource: MVideoSource) {
if (!videoSource.keptOriginalFilename) return
const filePath = VideoPathManager.Instance.getFSOriginalVideoFilePath(videoSource.keptOriginalFilename)
await remove(filePath)
if (videoSource.storage === FileStorage.OBJECT_STORAGE) {
await removeOriginalFileObjectStorage(videoSource)
}
}
2018-08-22 16:15:35 +02:00
isOutdated () {
if (this.isOwned()) return false
2019-03-19 14:13:53 +01:00
return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
2018-08-22 16:15:35 +02:00
}
2019-12-12 15:47:47 +01:00
hasPrivacyForFederation () {
return isPrivacyForFederation(this.privacy)
2019-12-12 15:47:47 +01:00
}
2020-11-04 14:16:57 +01:00
hasStateForFederation () {
return isStateForFederation(this.state)
}
isNewVideoForFederation (newPrivacy: VideoPrivacyType) {
return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true
2019-12-12 15:47:47 +01:00
}
2021-11-10 15:52:22 +01:00
setAsRefreshed (transaction?: Transaction) {
2023-01-10 11:09:30 +01:00
return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction })
}
// ---------------------------------------------------------------------------
requiresUserAuth (options: {
urlParamId: string
checkBlacklist: boolean
}) {
const { urlParamId, checkBlacklist } = options
if (this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL) {
return true
}
if (this.privacy === VideoPrivacy.UNLISTED) {
if (urlParamId && !isUUIDValid(urlParamId)) return true
2019-12-12 15:47:47 +01:00
return false
2019-12-12 15:47:47 +01:00
}
if (checkBlacklist && this.VideoBlacklist) return true
if (this.privacy === VideoPrivacy.PUBLIC || this.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
return false
}
throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`)
2019-12-12 15:47:47 +01:00
}
hasPrivateStaticPath () {
return isVideoInPrivateDirectory(this.privacy)
}
// ---------------------------------------------------------------------------
async setNewState (newState: VideoStateType, isNewVideo: boolean, transaction: Transaction) {
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
if (this.state === newState) throw new Error('Cannot use same state ' + newState)
this.state = newState
2016-12-24 16:59:17 +01:00
if (this.state === VideoState.PUBLISHED && isNewVideo) {
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
this.publishedAt = new Date()
}
Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
2021-08-17 08:26:20 +02:00
await this.save({ transaction })
}
getBandwidthBits (this: MVideo, videoFile: MVideoFile) {
if (!this.duration) return videoFile.size
2021-02-18 10:15:11 +01:00
return Math.ceil((videoFile.size * 8) / this.duration)
2018-09-11 16:27:07 +02:00
}
2021-02-18 10:15:11 +01:00
getTrackerUrls () {
if (this.isOwned()) {
return [
WEBSERVER.URL + '/tracker/announce',
WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket'
]
}
2019-01-29 08:37:25 +01:00
2021-02-18 10:15:11 +01:00
return this.Trackers.map(t => t.url)
2019-01-29 08:37:25 +01:00
}
}