1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-05 19:42:24 +02:00

Create and inject caption playlist in HLS master

This commit is contained in:
Chocobozzz 2025-04-08 15:26:02 +02:00
parent a7be820abc
commit 6e44e7e29a
No known key found for this signature in database
GPG key ID: 583A612D890159BE
49 changed files with 1368 additions and 401 deletions

View file

@ -20,7 +20,8 @@ import {
CreatedAt,
DataType,
ForeignKey,
Is, Scopes,
Is,
Scopes,
Table,
UpdatedAt
} from 'sequelize-typescript'
@ -57,7 +58,6 @@ export enum ScopeNames {
]
}
}))
@Table({
tableName: 'videoRedundancy',
indexes: [
@ -77,7 +77,6 @@ export enum ScopeNames {
]
})
export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
@CreatedAt
createdAt: Date
@ -134,8 +133,8 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
const videoUUID = videoStreamingPlaylist.Video.uuid
logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true)
.catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
videoStreamingPlaylist.Video.removeAllStreamingPlaylistFiles({ playlist: videoStreamingPlaylist, isRedundancy: true })
.catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
return undefined
}
@ -295,7 +294,7 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
}
return VideoRedundancyModel.findOne(query)
.then(r => !!r)
.then(r => !!r)
}
static async getVideoSample (p: Promise<VideoModel[]>) {
@ -503,7 +502,7 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
'(' +
'SELECT "videoId" FROM "videoStreamingPlaylist" ' +
'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
')'
')'
)
}
}
@ -516,12 +515,12 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
const sql = `WITH "tmp" AS ` +
`(` +
`SELECT "videoStreamingFile"."size" AS "videoStreamingFileSize", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
`FROM "videoRedundancy" AS "videoRedundancy" ` +
`LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
`LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
`ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
`WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
`SELECT "videoStreamingFile"."size" AS "videoStreamingFileSize", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
`FROM "videoRedundancy" AS "videoRedundancy" ` +
`LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
`LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
`ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
`WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
`) ` +
`SELECT ` +
`COALESCE(SUM("videoStreamingFileSize"), '0') AS "totalUsed", ` +
@ -604,7 +603,7 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
`SELECT "videoStreamingPlaylist"."videoId" AS "videoId" FROM "videoRedundancy" ` +
`INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoRedundancy"."videoStreamingPlaylistId" ` +
`WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
')'
')'
)
return {

View file

@ -12,7 +12,7 @@ import {
import { uuidToShort } from '@peertube/peertube-node-utils'
import { generateMagnetUri } from '@server/helpers/webtorrent.js'
import { tracer } from '@server/lib/opentelemetry/tracing.js'
import { getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
import { getHLSResolutionPlaylistFilename } from '@server/lib/paths.js'
import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls.js'
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
import { isArray } from '../../../helpers/custom-validators/misc.js'
@ -277,7 +277,7 @@ export function videoFilesModelToFormattedJSON (
hasVideo: videoFile.hasVideo(),
playlistUrl: includePlaylistUrl === true
? getHlsResolutionPlaylistFilename(fileUrl)
? getHLSResolutionPlaylistFilename(fileUrl)
: undefined,
storage: video.remote

View file

@ -1,14 +1,19 @@
import { removeVTTExt } from '@peertube/peertube-core-utils'
import { FileStorage, type FileStorageType, VideoCaption, VideoCaptionObject } from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { getObjectStoragePublicFileUrl } from '@server/lib/object-storage/urls.js'
import { removeCaptionObjectStorage } from '@server/lib/object-storage/videos.js'
import { removeCaptionObjectStorage, removeHLSFileObjectStorageByFilename } from '@server/lib/object-storage/videos.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import {
MVideo,
MVideoCaption,
MVideoCaptionFilename,
MVideoCaptionFormattable,
MVideoCaptionLanguageUrl,
MVideoCaptionUrl,
MVideoCaptionVideo,
MVideoOwned
MVideoOwned,
MVideoPrivacy
} from '@server/types/models/index.js'
import { remove } from 'fs-extra/esm'
import { join } from 'path'
@ -22,7 +27,8 @@ import {
DataType,
Default,
ForeignKey,
Is, Scopes,
Is,
Scopes,
Table,
UpdatedAt
} from 'sequelize-typescript'
@ -31,13 +37,14 @@ import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants.js'
import { SequelizeModel, buildWhereIdOrUUID, doesExist, throwIfNotValid } from '../shared/index.js'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist.js'
import { VideoModel } from './video.js'
export enum ScopeNames {
CAPTION_WITH_VIDEO = 'CAPTION_WITH_VIDEO'
}
const videoAttributes = [ 'id', 'name', 'remote', 'uuid', 'url', 'state' ]
const videoAttributes = [ 'id', 'name', 'remote', 'uuid', 'url', 'state', 'privacy' ]
@Scopes(() => ({
[ScopeNames.CAPTION_WITH_VIDEO]: {
@ -50,7 +57,6 @@ const videoAttributes = [ 'id', 'name', 'remote', 'uuid', 'url', 'state' ]
]
}
}))
@Table({
tableName: 'videoCaption',
indexes: [
@ -83,6 +89,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
@Column
filename: string
@AllowNull(true)
@Column
m3u8Filename: string
@AllowNull(false)
@Default(FileStorage.FILE_SYSTEM)
@Column
@ -92,6 +102,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
@Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
fileUrl: string
@AllowNull(true)
@Column
m3u8Url: string
@AllowNull(false)
@Column
automaticallyGenerated: boolean
@ -117,11 +131,8 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
if (instance.isOwned()) {
logger.info('Removing caption %s.', instance.filename)
try {
await instance.removeCaptionFile()
} catch (err) {
logger.error('Cannot remove caption file %s.', instance.filename)
}
instance.removeAllCaptionFiles()
.catch(err => logger.error('Cannot remove caption file ' + instance.filename, { err }))
}
return undefined
@ -230,7 +241,7 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
}
const captions = await VideoCaptionModel.scope(ScopeNames.CAPTION_WITH_VIDEO).findAll<MVideoCaptionVideo>(query)
const result: { [ id: number ]: MVideoCaptionVideo[] } = {}
const result: { [id: number]: MVideoCaptionVideo[] } = {}
for (const id of videoIds) {
result[id] = []
@ -253,6 +264,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
return `${buildUUID()}-${language}.vtt`
}
static generateM3U8Filename (vttFilename: string) {
return removeVTTExt(vttFilename) + '.m3u8'
}
// ---------------------------------------------------------------------------
toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
@ -265,9 +280,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
captionPath: this.Video.isOwned() && this.fileUrl
? null // On object storage
: this.getCaptionStaticPath(),
: this.getFileStaticPath(),
fileUrl: this.getFileUrl(this.Video),
m3u8Url: this.getM3U8Url(this.Video),
updatedAt: this.updatedAt.toISOString()
}
@ -278,7 +294,22 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
identifier: this.language,
name: VideoCaptionModel.getLanguageLabel(this.language),
automaticallyGenerated: this.automaticallyGenerated,
url: this.getOriginFileUrl(video)
// TODO: Remove break flag in v8
url: process.env.ENABLE_AP_BREAKING_CHANGES === 'true'
? [
{
type: 'Link',
mediaType: 'text/vtt',
href: this.getOriginFileUrl(video)
},
{
type: 'Link',
mediaType: 'application/x-mpegURL',
href: this.getOriginFileUrl(video)
}
]
: this.getOriginFileUrl(video)
}
}
@ -288,33 +319,77 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
return this.Video.remote === false
}
getCaptionStaticPath (this: MVideoCaptionLanguageUrl) {
// ---------------------------------------------------------------------------
getFileStaticPath (this: MVideoCaptionFilename) {
return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
}
getFSPath () {
return join(CONFIG.STORAGE.CAPTIONS_DIR, this.filename)
}
getM3U8StaticPath (this: MVideoCaptionFilename, video: MVideoPrivacy) {
if (!this.m3u8Filename) return null
removeCaptionFile (this: MVideoCaption) {
if (this.storage === FileStorage.OBJECT_STORAGE) {
return removeCaptionObjectStorage(this)
}
return remove(this.getFSPath())
return VideoStreamingPlaylistModel.getPlaylistFileStaticPath(video, this.m3u8Filename)
}
// ---------------------------------------------------------------------------
getFileUrl (this: MVideoCaptionLanguageUrl, video: MVideoOwned) {
getFSFilePath () {
return join(CONFIG.STORAGE.CAPTIONS_DIR, this.filename)
}
getFSM3U8Path (video: MVideoPrivacy) {
if (!this.m3u8Filename) return null
return VideoPathManager.Instance.getFSHLSOutputPath(video, this.m3u8Filename)
}
async removeAllCaptionFiles (this: MVideoCaptionVideo) {
await this.removeCaptionFile()
await this.removeCaptionPlaylist()
}
async removeCaptionFile (this: MVideoCaptionVideo) {
if (this.storage === FileStorage.OBJECT_STORAGE) {
if (this.fileUrl) {
await removeCaptionObjectStorage(this)
}
} else {
await remove(this.getFSFilePath())
}
this.filename = null
this.fileUrl = null
}
async removeCaptionPlaylist (this: MVideoCaptionVideo) {
if (!this.m3u8Filename) return
const hls = await VideoStreamingPlaylistModel.loadHLSByVideoWithVideo(this.videoId)
if (!hls) return
if (this.storage === FileStorage.OBJECT_STORAGE) {
if (this.m3u8Url) {
await removeHLSFileObjectStorageByFilename(hls, this.m3u8Filename)
}
} else {
await remove(this.getFSM3U8Path(this.Video))
}
this.m3u8Filename = null
this.m3u8Url = null
}
// ---------------------------------------------------------------------------
getFileUrl (this: MVideoCaptionUrl, video: MVideoOwned) {
if (video.isOwned() && this.storage === FileStorage.OBJECT_STORAGE) {
return getObjectStoragePublicFileUrl(this.fileUrl, CONFIG.OBJECT_STORAGE.CAPTIONS)
}
return WEBSERVER.URL + this.getCaptionStaticPath()
return WEBSERVER.URL + this.getFileStaticPath()
}
getOriginFileUrl (this: MVideoCaptionLanguageUrl, video: MVideoOwned) {
getOriginFileUrl (this: MVideoCaptionUrl, video: MVideoOwned) {
if (video.isOwned()) return this.getFileUrl(video)
return this.fileUrl
@ -322,6 +397,22 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
// ---------------------------------------------------------------------------
getM3U8Url (this: MVideoCaptionUrl, video: MVideoOwned & MVideoPrivacy) {
if (!this.m3u8Filename) return null
if (video.isOwned()) {
if (this.storage === FileStorage.OBJECT_STORAGE) {
return getObjectStoragePublicFileUrl(this.m3u8Url, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
}
return WEBSERVER.URL + this.getM3U8StaticPath(video)
}
return this.m3u8Url
}
// ---------------------------------------------------------------------------
isEqual (this: MVideoCaption, other: MVideoCaption) {
if (this.fileUrl) return this.fileUrl === other.fileUrl

View file

@ -12,22 +12,18 @@ import { getHLSPrivateFileUrl, getObjectStoragePublicFileUrl } from '@server/lib
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths.js'
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { MStreamingPlaylist, MStreamingPlaylistFiles, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models/index.js'
import {
MStreamingPlaylist,
MStreamingPlaylistFiles,
MStreamingPlaylistFilesVideo,
MStreamingPlaylistVideo,
MVideo,
MVideoPrivacy
} from '@server/types/models/index.js'
import memoizee from 'memoizee'
import { join } from 'path'
import { Op, Transaction } from 'sequelize'
import {
AllowNull,
BelongsTo,
Column,
CreatedAt,
DataType,
Default,
ForeignKey,
HasMany,
Is, Table,
UpdatedAt
} from 'sequelize-typescript'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, Table, UpdatedAt } from 'sequelize-typescript'
import { isArrayOf } from '../../helpers/custom-validators/misc.js'
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos.js'
import {
@ -205,7 +201,7 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
return VideoStreamingPlaylistModel.findByPk(id, options)
}
static loadHLSPlaylistByVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylist> {
static loadHLSByVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylist> {
const options = {
where: {
type: VideoStreamingPlaylistType.HLS,
@ -217,10 +213,31 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
return VideoStreamingPlaylistModel.findOne(options)
}
static loadHLSByVideoWithVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylistVideo> {
const options = {
where: {
type: VideoStreamingPlaylistType.HLS,
videoId
},
include: [
{
model: VideoModel.unscoped(),
required: true
}
],
transaction
}
return VideoStreamingPlaylistModel.findOne(options)
}
static async loadOrGenerate (video: MVideo, transaction?: Transaction) {
let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id, transaction)
let playlist = await VideoStreamingPlaylistModel.loadHLSByVideo(video.id, transaction)
let generated = false
if (!playlist) {
generated = true
playlist = new VideoStreamingPlaylistModel({
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
type: VideoStreamingPlaylistType.HLS,
@ -234,7 +251,7 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
await playlist.save({ transaction })
}
return Object.assign(playlist, { Video: video })
return { generated, playlist: Object.assign(playlist, { Video: video }) }
}
static doesOwnedVideoUUIDExist (videoUUID: string, storage: FileStorageType) {
@ -339,19 +356,21 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
return Object.assign(this, { Video: video })
}
private getMasterPlaylistStaticPath (video: MVideo) {
// ---------------------------------------------------------------------------
static getPlaylistFileStaticPath (video: MVideoPrivacy, filename: string) {
if (isVideoInPrivateDirectory(video.privacy)) {
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.playlistFilename)
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, filename)
}
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.playlistFilename)
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, filename)
}
private getSha256SegmentsStaticPath (video: MVideo) {
if (isVideoInPrivateDirectory(video.privacy)) {
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.segmentsSha256Filename)
}
private getMasterPlaylistStaticPath (video: MVideoPrivacy) {
return VideoStreamingPlaylistModel.getPlaylistFileStaticPath(video, this.playlistFilename)
}
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.segmentsSha256Filename)
private getSha256SegmentsStaticPath (video: MVideoPrivacy) {
return VideoStreamingPlaylistModel.getPlaylistFileStaticPath(video, this.segmentsSha256Filename)
}
}

View file

@ -31,7 +31,7 @@ import {
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 { 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'
@ -640,7 +640,6 @@ export class VideoModel extends SequelizeModel<VideoModel> {
name: 'videoId',
allowNull: true
},
hooks: true,
onDelete: 'cascade'
})
VideoFiles: Awaited<VideoFileModel>[]
@ -650,7 +649,6 @@ export class VideoModel extends SequelizeModel<VideoModel> {
name: 'videoId',
allowNull: false
},
hooks: true,
onDelete: 'cascade'
})
VideoStreamingPlaylists: Awaited<VideoStreamingPlaylistModel>[]
@ -834,7 +832,7 @@ export class VideoModel extends SequelizeModel<VideoModel> {
static async removeFiles (instance: VideoModel, options) {
const tasks: Promise<any>[] = []
logger.info('Removing files of video %s.', instance.url)
logger.info('Removing files of video %s.', instance.url, { toto: new Error().stack })
if (instance.isOwned()) {
if (!Array.isArray(instance.VideoFiles)) {
@ -852,7 +850,8 @@ export class VideoModel extends SequelizeModel<VideoModel> {
}
for (const p of instance.VideoStreamingPlaylists) {
tasks.push(instance.removeStreamingPlaylistFiles(p))
// Captions will be automatically deleted
tasks.push(instance.removeAllStreamingPlaylistFiles({ playlist: p, deleteCaptionPlaylists: false }))
}
// Remove source files
@ -1904,7 +1903,7 @@ export class VideoModel extends SequelizeModel<VideoModel> {
if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions
return this.$get('VideoCaptions', {
attributes: [ 'filename', 'language', 'fileUrl', 'storage', 'automaticallyGenerated' ],
attributes: [ 'filename', 'language', 'fileUrl', 'storage', 'automaticallyGenerated', 'm3u8Filename', 'm3u8Url' ],
transaction
}) as Promise<MVideoCaptionLanguageUrl[]>
}
@ -1993,47 +1992,76 @@ export class VideoModel extends SequelizeModel<VideoModel> {
return Promise.all(promises)
}
async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
async removeAllStreamingPlaylistFiles (options: {
playlist: MStreamingPlaylist
deleteCaptionPlaylists?: boolean // default true
isRedundancy?: boolean // default false
}) {
const { playlist, deleteCaptionPlaylists = true, isRedundancy = false } = options
const directoryPath = isRedundancy
? getHLSRedundancyDirectory(this)
: getHLSDirectory(this)
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)
const removeDirectory = async () => {
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
return
}
throw err
}
throw err
}
if (isRedundancy !== true) {
const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
streamingPlaylistWithFiles.Video = this
if (isRedundancy) {
await removeDirectory()
} else {
if (deleteCaptionPlaylists) {
const captions = await VideoCaptionModel.listVideoCaptions(playlist.videoId)
if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
// Remove playlist files associated to captions
for (const caption of captions) {
try {
await caption.removeCaptionPlaylist()
await caption.save()
} catch (err) {
logger.error(
`Cannot remove caption ${caption.filename} (${caption.language}) playlist files associated to video ${this.name}`,
{ video: this, ...lTags(this.uuid) }
)
}
}
}
await removeDirectory()
const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
playlistWithFiles.Video = this
if (!Array.isArray(playlistWithFiles.VideoFiles)) {
playlistWithFiles.VideoFiles = await playlistWithFiles.$get('VideoFiles')
}
// Remove physical files and torrents
await Promise.all(
streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
playlistWithFiles.VideoFiles.map(file => file.removeTorrent())
)
if (streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
await removeHLSObjectStorage(streamingPlaylist.withVideo(this))
if (playlist.storage === FileStorage.OBJECT_STORAGE) {
await removeHLSObjectStorage(playlist.withVideo(this))
}
}
logger.debug(
`Removing files associated to streaming playlist of video ${this.url}`,
{ streamingPlaylist, isRedundancy, ...lTags(this.uuid) }
{ playlist, isRedundancy, ...lTags(this.uuid) }
)
}
@ -2042,7 +2070,7 @@ export class VideoModel extends SequelizeModel<VideoModel> {
await videoFile.removeTorrent()
await remove(filePath)
const resolutionFilename = getHlsResolutionPlaylistFilename(videoFile.filename)
const resolutionFilename = getHLSResolutionPlaylistFilename(videoFile.filename)
await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename))
if (videoFile.storage === FileStorage.OBJECT_STORAGE) {