Add ability to delete a specific video file
This commit is contained in:
parent
12d84abeca
commit
1bb4c9ab2e
23 changed files with 678 additions and 209 deletions
|
@ -107,6 +107,11 @@
|
|||
<ul>
|
||||
<li *ngFor="let file of video.files">
|
||||
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
||||
|
||||
<my-global-icon
|
||||
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
||||
(click)="removeVideoFile(video, file, 'webtorrent')"
|
||||
></my-global-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -117,6 +122,11 @@
|
|||
<ul>
|
||||
<li *ngFor="let file of video.streamingPlaylists[0].files">
|
||||
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
||||
|
||||
<my-global-icon
|
||||
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
||||
(click)="removeVideoFile(video, file, 'hls')"
|
||||
></my-global-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,13 @@ my-embed {
|
|||
|
||||
.video-info > div {
|
||||
display: flex;
|
||||
|
||||
my-global-icon {
|
||||
width: 16px;
|
||||
margin-left: 3px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
|||
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
||||
import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
|
||||
import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
|
||||
import { UserRight, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
|
||||
import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
|
||||
import { VideoAdminService } from './video-admin.service'
|
||||
|
||||
@Component({
|
||||
|
@ -196,6 +196,22 @@ export class VideoListComponent extends RestTable implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') {
|
||||
const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?`
|
||||
const res = await this.confirmService.confirm(message, $localize`Delete file`)
|
||||
if (res === false) return
|
||||
|
||||
this.videoService.removeFile(video.uuid, file.id, type)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`File removed.`)
|
||||
this.reloadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private async removeVideos (videos: Video[]) {
|
||||
const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)(
|
||||
{ count: videos.length },
|
||||
|
|
|
@ -305,6 +305,11 @@ export class VideoService {
|
|||
)
|
||||
}
|
||||
|
||||
removeFile (videoId: number | string, fileId: number, type: 'hls' | 'webtorrent') {
|
||||
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/' + type + '/' + fileId)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') {
|
||||
const body: VideoTranscodingCreate = { transcodingType: type }
|
||||
|
||||
|
|
|
@ -150,6 +150,7 @@
|
|||
"node-media-server": "^2.1.4",
|
||||
"nodemailer": "^6.0.0",
|
||||
"opentelemetry-instrumentation-sequelize": "^0.29.0",
|
||||
"p-queue": "^6",
|
||||
"parse-torrent": "^9.1.0",
|
||||
"password-generator": "^2.0.2",
|
||||
"pg": "^8.2.1",
|
||||
|
|
|
@ -2,6 +2,7 @@ import express from 'express'
|
|||
import toInt from 'validator/lib/toInt'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
|
||||
import { removeAllWebTorrentFiles, removeHLSFile, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { HttpStatusCode, UserRight } from '@shared/models'
|
||||
import {
|
||||
|
@ -9,10 +10,13 @@ import {
|
|||
authenticate,
|
||||
ensureUserHasRight,
|
||||
videoFileMetadataGetValidator,
|
||||
videoFilesDeleteHLSFileValidator,
|
||||
videoFilesDeleteHLSValidator,
|
||||
videoFilesDeleteWebTorrentFileValidator,
|
||||
videoFilesDeleteWebTorrentValidator,
|
||||
videosGetValidator
|
||||
} from '../../../middlewares'
|
||||
import { updatePlaylistAfterFileChange } from '@server/lib/hls'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video')
|
||||
const filesRouter = express.Router()
|
||||
|
@ -27,14 +31,26 @@ filesRouter.delete('/:id/hls',
|
|||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
|
||||
asyncMiddleware(videoFilesDeleteHLSValidator),
|
||||
asyncMiddleware(removeHLSPlaylist)
|
||||
asyncMiddleware(removeHLSPlaylistController)
|
||||
)
|
||||
filesRouter.delete('/:id/hls/:videoFileId',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
|
||||
asyncMiddleware(videoFilesDeleteHLSFileValidator),
|
||||
asyncMiddleware(removeHLSFileController)
|
||||
)
|
||||
|
||||
filesRouter.delete('/:id/webtorrent',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
|
||||
asyncMiddleware(videoFilesDeleteWebTorrentValidator),
|
||||
asyncMiddleware(removeWebTorrentFiles)
|
||||
asyncMiddleware(removeAllWebTorrentFilesController)
|
||||
)
|
||||
filesRouter.delete('/:id/webtorrent/:videoFileId',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
|
||||
asyncMiddleware(videoFilesDeleteWebTorrentFileValidator),
|
||||
asyncMiddleware(removeWebTorrentFileController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -51,33 +67,53 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
|
|||
return res.json(videoFile.metadata)
|
||||
}
|
||||
|
||||
async function removeHLSPlaylist (req: express.Request, res: express.Response) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function removeHLSPlaylistController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
|
||||
logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid))
|
||||
|
||||
const hls = video.getHLSPlaylist()
|
||||
await video.removeStreamingPlaylistFiles(hls)
|
||||
await hls.destroy()
|
||||
|
||||
video.VideoStreamingPlaylists = video.VideoStreamingPlaylists.filter(p => p.id !== hls.id)
|
||||
await removeHLSPlaylist(video)
|
||||
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function removeWebTorrentFiles (req: express.Request, res: express.Response) {
|
||||
async function removeHLSFileController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
const videoFileId = +req.params.videoFileId
|
||||
|
||||
logger.info('Deleting HLS file %d of %s.', videoFileId, video.url, lTags(video.uuid))
|
||||
|
||||
const playlist = await removeHLSFile(video, videoFileId)
|
||||
if (playlist) await updatePlaylistAfterFileChange(video, playlist)
|
||||
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function removeAllWebTorrentFilesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
|
||||
logger.info('Deleting WebTorrent files of %s.', video.url, lTags(video.uuid))
|
||||
|
||||
for (const file of video.VideoFiles) {
|
||||
await video.removeWebTorrentFileAndTorrent(file)
|
||||
await file.destroy()
|
||||
}
|
||||
|
||||
video.VideoFiles = []
|
||||
await removeAllWebTorrentFiles(video)
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function removeWebTorrentFileController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
|
||||
const videoFileId = +req.params.videoFileId
|
||||
logger.info('Deleting WebTorrent file %d of %s.', videoFileId, video.url, lTags(video.uuid))
|
||||
|
||||
await removeWebTorrentFile(video, videoFileId)
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { FfprobeData } from 'fluent-ffmpeg'
|
||||
import { getMaxBitrate } from '@shared/core-utils'
|
||||
import {
|
||||
buildFileMetadata,
|
||||
ffprobePromise,
|
||||
getAudioStream,
|
||||
getVideoStreamDuration,
|
||||
getMaxAudioBitrate,
|
||||
buildFileMetadata,
|
||||
getVideoStreamBitrate,
|
||||
getVideoStreamFPS,
|
||||
getVideoStream,
|
||||
getVideoStreamBitrate,
|
||||
getVideoStreamDimensionsInfo,
|
||||
getVideoStreamDuration,
|
||||
getVideoStreamFPS,
|
||||
hasAudioStream
|
||||
} from '@shared/extra-utils/ffprobe'
|
||||
import { VideoResolution, VideoTranscodingFPS } from '@shared/models'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Transaction } from 'sequelize/types'
|
||||
import { CreationAttributes, Transaction } from 'sequelize/types'
|
||||
import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
|
||||
import { logger, LoggerTagsFn } from '@server/helpers/logger'
|
||||
import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail'
|
||||
|
@ -7,7 +7,15 @@ import { VideoCaptionModel } from '@server/models/video/video-caption'
|
|||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
|
||||
import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
|
||||
import {
|
||||
MStreamingPlaylistFiles,
|
||||
MStreamingPlaylistFilesVideo,
|
||||
MThumbnail,
|
||||
MVideoCaption,
|
||||
MVideoFile,
|
||||
MVideoFullLight,
|
||||
MVideoThumbnail
|
||||
} from '@server/types/models'
|
||||
import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
|
||||
import { getOrCreateAPActor } from '../../actors'
|
||||
import { checkUrlsSameHost } from '../../url'
|
||||
|
@ -125,38 +133,39 @@ export abstract class APVideoAbstractBuilder {
|
|||
// Remove video playlists that do not exist anymore
|
||||
await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
|
||||
|
||||
const oldPlaylists = video.VideoStreamingPlaylists
|
||||
video.VideoStreamingPlaylists = []
|
||||
|
||||
for (const playlistAttributes of streamingPlaylistAttributes) {
|
||||
const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
|
||||
streamingPlaylistModel.Video = video
|
||||
|
||||
await this.setStreamingPlaylistFiles(video, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
|
||||
await this.setStreamingPlaylistFiles(oldPlaylists, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
|
||||
|
||||
video.VideoStreamingPlaylists.push(streamingPlaylistModel)
|
||||
}
|
||||
}
|
||||
|
||||
private async insertOrReplaceStreamingPlaylist (attributes: VideoStreamingPlaylistModel['_creationAttributes'], t: Transaction) {
|
||||
private async insertOrReplaceStreamingPlaylist (attributes: CreationAttributes<VideoStreamingPlaylistModel>, t: Transaction) {
|
||||
const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
|
||||
|
||||
return streamingPlaylist as MStreamingPlaylistFilesVideo
|
||||
}
|
||||
|
||||
private getStreamingPlaylistFiles (video: MVideoFullLight, type: VideoStreamingPlaylistType) {
|
||||
const playlist = video.VideoStreamingPlaylists.find(s => s.type === type)
|
||||
private getStreamingPlaylistFiles (oldPlaylists: MStreamingPlaylistFiles[], type: VideoStreamingPlaylistType) {
|
||||
const playlist = oldPlaylists.find(s => s.type === type)
|
||||
if (!playlist) return []
|
||||
|
||||
return playlist.VideoFiles
|
||||
}
|
||||
|
||||
private async setStreamingPlaylistFiles (
|
||||
video: MVideoFullLight,
|
||||
oldPlaylists: MStreamingPlaylistFiles[],
|
||||
playlistModel: MStreamingPlaylistFilesVideo,
|
||||
tagObjects: ActivityTagObject[],
|
||||
t: Transaction
|
||||
) {
|
||||
const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(video, playlistModel.type)
|
||||
const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type)
|
||||
|
||||
const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra'
|
||||
import { flatten, uniq } from 'lodash'
|
||||
import PQueue from 'p-queue'
|
||||
import { basename, dirname, join } from 'path'
|
||||
import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models'
|
||||
import { sha256 } from '@shared/extra-utils'
|
||||
import { VideoStorage } from '@shared/models'
|
||||
import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg'
|
||||
|
@ -14,7 +15,7 @@ import { sequelizeTypescript } from '../initializers/database'
|
|||
import { VideoFileModel } from '../models/video/video-file'
|
||||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||
import { storeHLSFile } from './object-storage'
|
||||
import { getHlsResolutionPlaylistFilename } from './paths'
|
||||
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths'
|
||||
import { VideoPathManager } from './video-path-manager'
|
||||
|
||||
async function updateStreamingPlaylistsInfohashesIfNeeded () {
|
||||
|
@ -33,80 +34,123 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
|
|||
}
|
||||
}
|
||||
|
||||
async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlaylistFilesVideo) {
|
||||
const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
|
||||
async function updatePlaylistAfterFileChange (video: MVideo, playlist: MStreamingPlaylist) {
|
||||
let playlistWithFiles = await updateMasterHLSPlaylist(video, playlist)
|
||||
playlistWithFiles = await updateSha256VODSegments(video, playlist)
|
||||
|
||||
for (const file of playlist.VideoFiles) {
|
||||
const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
|
||||
// Refresh playlist, operations can take some time
|
||||
playlistWithFiles = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlist.id)
|
||||
playlistWithFiles.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
|
||||
await playlistWithFiles.save()
|
||||
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => {
|
||||
const size = await getVideoStreamDimensionsInfo(videoFilePath)
|
||||
video.setHLSPlaylist(playlistWithFiles)
|
||||
}
|
||||
|
||||
const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file)
|
||||
const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}`
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}`
|
||||
if (file.fps) line += ',FRAME-RATE=' + file.fps
|
||||
// Avoid concurrency issues when updating streaming playlist files
|
||||
const playlistFilesQueue = new PQueue({ concurrency: 1 })
|
||||
|
||||
const codecs = await Promise.all([
|
||||
getVideoStreamCodec(videoFilePath),
|
||||
getAudioStreamCodec(videoFilePath)
|
||||
])
|
||||
function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> {
|
||||
return playlistFilesQueue.add(async () => {
|
||||
const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id)
|
||||
|
||||
line += `,CODECS="${codecs.filter(c => !!c).join(',')}"`
|
||||
const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
|
||||
|
||||
masterPlaylists.push(line)
|
||||
masterPlaylists.push(playlistFilename)
|
||||
})
|
||||
}
|
||||
for (const file of playlist.VideoFiles) {
|
||||
const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
|
||||
|
||||
await VideoPathManager.Instance.makeAvailablePlaylistFile(playlist, playlist.playlistFilename, async masterPlaylistPath => {
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => {
|
||||
const size = await getVideoStreamDimensionsInfo(videoFilePath)
|
||||
|
||||
const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file)
|
||||
const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}`
|
||||
|
||||
let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}`
|
||||
if (file.fps) line += ',FRAME-RATE=' + file.fps
|
||||
|
||||
const codecs = await Promise.all([
|
||||
getVideoStreamCodec(videoFilePath),
|
||||
getAudioStreamCodec(videoFilePath)
|
||||
])
|
||||
|
||||
line += `,CODECS="${codecs.filter(c => !!c).join(',')}"`
|
||||
|
||||
masterPlaylists.push(line)
|
||||
masterPlaylists.push(playlistFilename)
|
||||
})
|
||||
}
|
||||
|
||||
if (playlist.playlistFilename) {
|
||||
await video.removeStreamingPlaylistFile(playlist, playlist.playlistFilename)
|
||||
}
|
||||
playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
|
||||
|
||||
const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename)
|
||||
await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
|
||||
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
await storeHLSFile(playlist, playlist.playlistFilename, masterPlaylistPath)
|
||||
playlist.playlistUrl = await storeHLSFile(playlist, playlist.playlistFilename)
|
||||
await remove(masterPlaylistPath)
|
||||
}
|
||||
|
||||
return playlist.save()
|
||||
})
|
||||
}
|
||||
|
||||
async function updateSha256VODSegments (video: MVideoUUID, playlist: MStreamingPlaylistFilesVideo) {
|
||||
const json: { [filename: string]: { [range: string]: string } } = {}
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// For all the resolutions available for this video
|
||||
for (const file of playlist.VideoFiles) {
|
||||
const rangeHashes: { [range: string]: string } = {}
|
||||
const fileWithPlaylist = file.withVideoOrPlaylist(playlist)
|
||||
async function updateSha256VODSegments (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> {
|
||||
return playlistFilesQueue.add(async () => {
|
||||
const json: { [filename: string]: { [range: string]: string } } = {}
|
||||
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => {
|
||||
const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id)
|
||||
|
||||
return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => {
|
||||
const playlistContent = await readFile(resolutionPlaylistPath)
|
||||
const ranges = getRangesFromPlaylist(playlistContent.toString())
|
||||
// For all the resolutions available for this video
|
||||
for (const file of playlist.VideoFiles) {
|
||||
const rangeHashes: { [range: string]: string } = {}
|
||||
const fileWithPlaylist = file.withVideoOrPlaylist(playlist)
|
||||
|
||||
const fd = await open(videoPath, 'r')
|
||||
for (const range of ranges) {
|
||||
const buf = Buffer.alloc(range.length)
|
||||
await read(fd, buf, 0, range.length, range.offset)
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => {
|
||||
|
||||
rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
|
||||
}
|
||||
await close(fd)
|
||||
return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => {
|
||||
const playlistContent = await readFile(resolutionPlaylistPath)
|
||||
const ranges = getRangesFromPlaylist(playlistContent.toString())
|
||||
|
||||
const videoFilename = file.filename
|
||||
json[videoFilename] = rangeHashes
|
||||
const fd = await open(videoPath, 'r')
|
||||
for (const range of ranges) {
|
||||
const buf = Buffer.alloc(range.length)
|
||||
await read(fd, buf, 0, range.length, range.offset)
|
||||
|
||||
rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
|
||||
}
|
||||
await close(fd)
|
||||
|
||||
const videoFilename = file.filename
|
||||
json[videoFilename] = rangeHashes
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename)
|
||||
await outputJSON(outputPath, json)
|
||||
if (playlist.segmentsSha256Filename) {
|
||||
await video.removeStreamingPlaylistFile(playlist, playlist.segmentsSha256Filename)
|
||||
}
|
||||
playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
|
||||
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
await storeHLSFile(playlist, playlist.segmentsSha256Filename)
|
||||
await remove(outputPath)
|
||||
}
|
||||
const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename)
|
||||
await outputJSON(outputPath, json)
|
||||
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
playlist.segmentsSha256Url = await storeHLSFile(playlist, playlist.segmentsSha256Filename)
|
||||
await remove(outputPath)
|
||||
}
|
||||
|
||||
return playlist.save()
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function buildSha256Segment (segmentPath: string) {
|
||||
const buf = await readFile(segmentPath)
|
||||
return sha256(buf)
|
||||
|
@ -190,7 +234,8 @@ export {
|
|||
updateSha256VODSegments,
|
||||
buildSha256Segment,
|
||||
downloadPlaylistSegments,
|
||||
updateStreamingPlaylistsInfohashesIfNeeded
|
||||
updateStreamingPlaylistsInfohashesIfNeeded,
|
||||
updatePlaylistAfterFileChange
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -55,7 +55,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
|
|||
|
||||
if (currentVideoFile) {
|
||||
// Remove old file and old torrent
|
||||
await video.removeWebTorrentFileAndTorrent(currentVideoFile)
|
||||
await video.removeWebTorrentFile(currentVideoFile)
|
||||
// Remove the old video file from the array
|
||||
video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { generateWebTorrentVideoFilename } from '@server/lib/paths'
|
|||
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
|
||||
import { isAbleToUploadVideo } from '@server/lib/user'
|
||||
import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
|
||||
import { removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio'
|
||||
import { UserModel } from '@server/models/user/user'
|
||||
|
@ -27,12 +28,12 @@ import {
|
|||
} from '@shared/extra-utils'
|
||||
import {
|
||||
VideoStudioEditionPayload,
|
||||
VideoStudioTaskPayload,
|
||||
VideoStudioTask,
|
||||
VideoStudioTaskCutPayload,
|
||||
VideoStudioTaskIntroPayload,
|
||||
VideoStudioTaskOutroPayload,
|
||||
VideoStudioTaskWatermarkPayload,
|
||||
VideoStudioTask
|
||||
VideoStudioTaskPayload,
|
||||
VideoStudioTaskWatermarkPayload
|
||||
} from '@shared/models'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger'
|
||||
|
||||
|
@ -89,7 +90,6 @@ async function processVideoStudioEdition (job: Job) {
|
|||
await move(editionResultPath, outputPath)
|
||||
|
||||
await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
|
||||
|
||||
await removeAllFiles(video, newFile)
|
||||
|
||||
await newFile.save()
|
||||
|
@ -197,18 +197,12 @@ async function buildNewFile (video: MVideoId, path: string) {
|
|||
}
|
||||
|
||||
async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) {
|
||||
const hls = video.getHLSPlaylist()
|
||||
|
||||
if (hls) {
|
||||
await video.removeStreamingPlaylistFiles(hls)
|
||||
await hls.destroy()
|
||||
}
|
||||
await removeHLSPlaylist(video)
|
||||
|
||||
for (const file of video.VideoFiles) {
|
||||
if (file.id === webTorrentFileException.id) continue
|
||||
|
||||
await video.removeWebTorrentFileAndTorrent(file)
|
||||
await file.destroy()
|
||||
await removeWebTorrentFile(video, file.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
|
|||
if (payload.isMaxQuality && payload.autoDeleteWebTorrentIfNeeded && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
|
||||
// Remove webtorrent files if not enabled
|
||||
for (const file of video.VideoFiles) {
|
||||
await video.removeWebTorrentFileAndTorrent(file)
|
||||
await video.removeWebTorrentFile(file)
|
||||
await file.destroy()
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,8 @@ import { toEven } from '@server/helpers/core-utils'
|
|||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||
import { sequelizeTypescript } from '@server/initializers/database'
|
||||
import { MStreamingPlaylistFilesVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
|
||||
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
|
||||
import {
|
||||
buildFileMetadata,
|
||||
canDoQuickTranscode,
|
||||
|
@ -18,17 +17,10 @@ import {
|
|||
TranscodeVODOptionsType
|
||||
} from '../../helpers/ffmpeg'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
|
||||
import { VideoFileModel } from '../../models/video/video-file'
|
||||
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
|
||||
import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
|
||||
import {
|
||||
generateHLSMasterPlaylistFilename,
|
||||
generateHlsSha256SegmentsFilename,
|
||||
generateHLSVideoFilename,
|
||||
generateWebTorrentVideoFilename,
|
||||
getHlsResolutionPlaylistFilename
|
||||
} from '../paths'
|
||||
import { updatePlaylistAfterFileChange } from '../hls'
|
||||
import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
|
||||
import { VideoPathManager } from '../video-path-manager'
|
||||
import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
|
||||
|
||||
|
@ -260,7 +252,7 @@ async function onWebTorrentVideoFileTranscoding (
|
|||
await createTorrentAndSetInfoHash(video, videoFile)
|
||||
|
||||
const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
||||
if (oldFile) await video.removeWebTorrentFileAndTorrent(oldFile)
|
||||
if (oldFile) await video.removeWebTorrentFile(oldFile)
|
||||
|
||||
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
||||
video.VideoFiles = await video.$get('VideoFiles')
|
||||
|
@ -314,35 +306,15 @@ async function generateHlsPlaylistCommon (options: {
|
|||
await transcodeVOD(transcodeOptions)
|
||||
|
||||
// Create or update the playlist
|
||||
const { playlist, oldPlaylistFilename, oldSegmentsSha256Filename } = await retryTransactionWrapper(() => {
|
||||
const playlist = await retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async transaction => {
|
||||
const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
|
||||
|
||||
const oldPlaylistFilename = playlist.playlistFilename
|
||||
const oldSegmentsSha256Filename = playlist.segmentsSha256Filename
|
||||
|
||||
playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
|
||||
playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
|
||||
|
||||
playlist.p2pMediaLoaderInfohashes = []
|
||||
playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
|
||||
|
||||
playlist.type = VideoStreamingPlaylistType.HLS
|
||||
|
||||
await playlist.save({ transaction })
|
||||
|
||||
return { playlist, oldPlaylistFilename, oldSegmentsSha256Filename }
|
||||
return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
|
||||
})
|
||||
})
|
||||
|
||||
if (oldPlaylistFilename) await video.removeStreamingPlaylistFile(playlist, oldPlaylistFilename)
|
||||
if (oldSegmentsSha256Filename) await video.removeStreamingPlaylistFile(playlist, oldSegmentsSha256Filename)
|
||||
|
||||
// Build the new playlist file
|
||||
const extname = extnameUtil(videoFilename)
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname,
|
||||
extname: extnameUtil(videoFilename),
|
||||
size: 0,
|
||||
filename: videoFilename,
|
||||
fps: -1,
|
||||
|
@ -350,8 +322,6 @@ async function generateHlsPlaylistCommon (options: {
|
|||
})
|
||||
|
||||
const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
|
||||
|
||||
// Move files from tmp transcoded directory to the appropriate place
|
||||
await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
|
||||
|
||||
// Move playlist file
|
||||
|
@ -369,21 +339,14 @@ async function generateHlsPlaylistCommon (options: {
|
|||
await createTorrentAndSetInfoHash(playlist, newVideoFile)
|
||||
|
||||
const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution })
|
||||
if (oldFile) await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
|
||||
if (oldFile) {
|
||||
await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
|
||||
await oldFile.destroy()
|
||||
}
|
||||
|
||||
const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
|
||||
|
||||
const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
|
||||
playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
|
||||
playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
|
||||
playlist.storage = VideoStorage.FILE_SYSTEM
|
||||
|
||||
await playlist.save()
|
||||
|
||||
video.setHLSPlaylist(playlist)
|
||||
|
||||
await updateMasterHLSPlaylist(video, playlistWithFiles)
|
||||
await updateSha256VODSegments(video, playlistWithFiles)
|
||||
await updatePlaylistAfterFileChange(video, playlist)
|
||||
|
||||
return { resolutionPlaylistPath, videoFile: savedVideoFile }
|
||||
}
|
||||
|
|
69
server/lib/video-file.ts
Normal file
69
server/lib/video-file.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { logger } from '@server/helpers/logger'
|
||||
import { MVideoWithAllFiles } from '@server/types/models'
|
||||
import { lTags } from './object-storage/shared'
|
||||
|
||||
async function removeHLSPlaylist (video: MVideoWithAllFiles) {
|
||||
const hls = video.getHLSPlaylist()
|
||||
if (!hls) return
|
||||
|
||||
await video.removeStreamingPlaylistFiles(hls)
|
||||
await hls.destroy()
|
||||
|
||||
video.VideoStreamingPlaylists = video.VideoStreamingPlaylists.filter(p => p.id !== hls.id)
|
||||
}
|
||||
|
||||
async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
|
||||
logger.info('Deleting HLS file %d of %s.', fileToDeleteId, video.url, lTags(video.uuid))
|
||||
|
||||
const hls = video.getHLSPlaylist()
|
||||
const files = hls.VideoFiles
|
||||
|
||||
if (files.length === 1) {
|
||||
await removeHLSPlaylist(video)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toDelete = files.find(f => f.id === fileToDeleteId)
|
||||
await video.removeStreamingPlaylistVideoFile(video.getHLSPlaylist(), toDelete)
|
||||
await toDelete.destroy()
|
||||
|
||||
hls.VideoFiles = hls.VideoFiles.filter(f => f.id !== toDelete.id)
|
||||
|
||||
return hls
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function removeAllWebTorrentFiles (video: MVideoWithAllFiles) {
|
||||
for (const file of video.VideoFiles) {
|
||||
await video.removeWebTorrentFile(file)
|
||||
await file.destroy()
|
||||
}
|
||||
|
||||
video.VideoFiles = []
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
async function removeWebTorrentFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
|
||||
const files = video.VideoFiles
|
||||
|
||||
if (files.length === 1) {
|
||||
return removeAllWebTorrentFiles(video)
|
||||
}
|
||||
|
||||
const toDelete = files.find(f => f.id === fileToDeleteId)
|
||||
await video.removeWebTorrentFile(toDelete)
|
||||
await toDelete.destroy()
|
||||
|
||||
video.VideoFiles = files.filter(f => f.id !== toDelete.id)
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
export {
|
||||
removeHLSPlaylist,
|
||||
removeHLSFile,
|
||||
removeAllWebTorrentFiles,
|
||||
removeWebTorrentFile
|
||||
}
|
|
@ -3,6 +3,8 @@ import { MVideo } from '@server/types/models'
|
|||
import { HttpStatusCode } from '@shared/models'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc'
|
||||
import { param } from 'express-validator'
|
||||
|
||||
const videoFilesDeleteWebTorrentValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
@ -35,6 +37,43 @@ const videoFilesDeleteWebTorrentValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const videoFilesDeleteWebTorrentFileValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid).withMessage('Should have a valid file id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking videoFilesDeleteWebTorrentFile parameters', { parameters: req.params })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
const files = video.VideoFiles
|
||||
if (!files.find(f => f.id === +req.params.videoFileId)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'This video does not have this WebTorrent file id'
|
||||
})
|
||||
}
|
||||
|
||||
if (files.length === 1 && !video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete WebTorrent files since this video does not have HLS playlist'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const videoFilesDeleteHLSValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
|
@ -66,9 +105,55 @@ const videoFilesDeleteHLSValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const videoFilesDeleteHLSFileValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid).withMessage('Should have a valid file id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking videoFilesDeleteHLSFile parameters', { parameters: req.params })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
if (!video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have HLS files'
|
||||
})
|
||||
}
|
||||
|
||||
const hlsFiles = video.getHLSPlaylist().VideoFiles
|
||||
if (!hlsFiles.find(f => f.id === +req.params.videoFileId)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'This HLS playlist does not have this file id'
|
||||
})
|
||||
}
|
||||
|
||||
// Last file to delete
|
||||
if (hlsFiles.length === 1 && !video.hasWebTorrentFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete last HLS playlist file since this video does not have WebTorrent files'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
videoFilesDeleteWebTorrentValidator,
|
||||
videoFilesDeleteHLSValidator
|
||||
videoFilesDeleteWebTorrentFileValidator,
|
||||
|
||||
videoFilesDeleteHLSValidator,
|
||||
videoFilesDeleteHLSFileValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -162,7 +162,7 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu
|
|||
const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
|
||||
logger.info('Removing duplicated video file %s.', logIdentifier)
|
||||
|
||||
videoFile.Video.removeWebTorrentFileAndTorrent(videoFile, true)
|
||||
videoFile.Video.removeWebTorrentFile(videoFile, true)
|
||||
.catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,9 @@ import {
|
|||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { getHLSPublicFileUrl } from '@server/lib/object-storage'
|
||||
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { MStreamingPlaylist, MVideo } from '@server/types/models'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models'
|
||||
import { sha1 } from '@shared/extra-utils'
|
||||
import { VideoStorage } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
|
@ -167,6 +168,22 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
|||
return VideoStreamingPlaylistModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadWithVideoAndFiles (id: number) {
|
||||
const options = {
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
model: VideoFileModel.unscoped()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return VideoStreamingPlaylistModel.findByPk<MStreamingPlaylistFilesVideo>(id, options)
|
||||
}
|
||||
|
||||
static loadWithVideo (id: number) {
|
||||
const options = {
|
||||
include: [
|
||||
|
@ -194,9 +211,22 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
|||
|
||||
static async loadOrGenerate (video: MVideo, transaction?: Transaction) {
|
||||
let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id, transaction)
|
||||
if (!playlist) playlist = new VideoStreamingPlaylistModel()
|
||||
|
||||
return Object.assign(playlist, { videoId: video.id, Video: video })
|
||||
if (!playlist) {
|
||||
playlist = new VideoStreamingPlaylistModel({
|
||||
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
type: VideoStreamingPlaylistType.HLS,
|
||||
storage: VideoStorage.FILE_SYSTEM,
|
||||
p2pMediaLoaderInfohashes: [],
|
||||
playlistFilename: generateHLSMasterPlaylistFilename(video.isLive),
|
||||
segmentsSha256Filename: generateHlsSha256SegmentsFilename(video.isLive),
|
||||
videoId: video.id
|
||||
})
|
||||
|
||||
await playlist.save({ transaction })
|
||||
}
|
||||
|
||||
return Object.assign(playlist, { Video: video })
|
||||
}
|
||||
|
||||
static doesOwnedHLSPlaylistExist (videoUUID: string) {
|
||||
|
|
|
@ -28,7 +28,7 @@ import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation
|
|||
import { LiveManager } from '@server/lib/live/live-manager'
|
||||
import { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage'
|
||||
import { tracer } from '@server/lib/opentelemetry/tracing'
|
||||
import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths'
|
||||
import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { ModelCache } from '@server/models/model-cache'
|
||||
|
@ -769,7 +769,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
|
||||
// Remove physical files and torrents
|
||||
instance.VideoFiles.forEach(file => {
|
||||
tasks.push(instance.removeWebTorrentFileAndTorrent(file))
|
||||
tasks.push(instance.removeWebTorrentFile(file))
|
||||
})
|
||||
|
||||
// Remove playlists file
|
||||
|
@ -1783,7 +1783,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
.concat(toAdd)
|
||||
}
|
||||
|
||||
removeWebTorrentFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
|
||||
removeWebTorrentFile (videoFile: MVideoFile, isRedundancy = false) {
|
||||
const filePath = isRedundancy
|
||||
? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile)
|
||||
: VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile)
|
||||
|
@ -1829,8 +1829,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
await videoFile.removeTorrent()
|
||||
await remove(filePath)
|
||||
|
||||
const resolutionFilename = getHlsResolutionPlaylistFilename(videoFile.filename)
|
||||
await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename))
|
||||
|
||||
if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), videoFile.filename)
|
||||
await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), resolutionFilename)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,12 @@ describe('Test videos files', function () {
|
|||
let validId1: string
|
||||
let validId2: string
|
||||
|
||||
let hlsFileId: number
|
||||
let webtorrentFileId: number
|
||||
|
||||
let remoteHLSFileId: number
|
||||
let remoteWebtorrentFileId: number
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
|
@ -39,7 +45,12 @@ describe('Test videos files', function () {
|
|||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
|
||||
remoteId = uuid
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await servers[1].videos.get({ id: uuid })
|
||||
remoteId = video.uuid
|
||||
remoteHLSFileId = video.streamingPlaylists[0].files[0].id
|
||||
remoteWebtorrentFileId = video.files[0].id
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -47,7 +58,12 @@ describe('Test videos files', function () {
|
|||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' })
|
||||
validId1 = uuid
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
validId1 = video.uuid
|
||||
hlsFileId = video.streamingPlaylists[0].files[0].id
|
||||
webtorrentFileId = video.files[0].id
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -76,43 +92,67 @@ describe('Test videos files', function () {
|
|||
})
|
||||
|
||||
it('Should not delete files of a unknown video', async function () {
|
||||
await servers[0].videos.removeHLSFiles({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
const expectedStatus = HttpStatusCode.NOT_FOUND_404
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: 404, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus })
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: 404, fileId: webtorrentFileId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete unknown files', async function () {
|
||||
const expectedStatus = HttpStatusCode.NOT_FOUND_404
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webtorrentFileId, expectedStatus })
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: hlsFileId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete files of a remote video', async function () {
|
||||
await servers[0].videos.removeHLSFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: remoteId, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus })
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: remoteId, fileId: remoteWebtorrentFileId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete files by a non admin user', async function () {
|
||||
const expectedStatus = HttpStatusCode.FORBIDDEN_403
|
||||
|
||||
await servers[0].videos.removeHLSFiles({ videoId: validId1, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeHLSFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: moderatorToken, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete files if the files are not available', async function () {
|
||||
await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not delete files if no both versions are available', async function () {
|
||||
await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should not delete files if no both versions are available', async function () {
|
||||
await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should delete files if both versions are available', async function () {
|
||||
await servers[0].videos.removeHLSFiles({ videoId: validId1 })
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: validId2 })
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId })
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId })
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId1 })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId2 })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
|
|
|
@ -122,7 +122,7 @@ function runTests (objectStorage: boolean) {
|
|||
it('Should generate WebTorrent from HLS only video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: videoUUID })
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: videoUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' })
|
||||
|
@ -142,7 +142,7 @@ function runTests (objectStorage: boolean) {
|
|||
it('Should only generate WebTorrent', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].videos.removeHLSFiles({ videoId: videoUUID })
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' })
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { HttpStatusCode } from '@shared/models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
|
@ -13,8 +15,6 @@ import {
|
|||
|
||||
describe('Test videos files', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let validId1: string
|
||||
let validId2: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
|
@ -27,48 +27,160 @@ describe('Test videos files', function () {
|
|||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableTranscoding(true, true)
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' })
|
||||
validId1 = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' })
|
||||
validId2 = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should delete webtorrent files', async function () {
|
||||
this.timeout(30_000)
|
||||
describe('When deleting all files', function () {
|
||||
let validId1: string
|
||||
let validId2: string
|
||||
|
||||
await servers[0].videos.removeWebTorrentFiles({ videoId: validId1 })
|
||||
before(async function () {
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' })
|
||||
validId1 = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' })
|
||||
validId2 = uuid
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: validId1 })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
}
|
||||
it('Should delete webtorrent files', async function () {
|
||||
this.timeout(30_000)
|
||||
|
||||
await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1 })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: validId1 })
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete HLS files', async function () {
|
||||
this.timeout(30_000)
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId2 })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: validId2 })
|
||||
|
||||
expect(video.files).to.have.length.above(0)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should delete HLS files', async function () {
|
||||
this.timeout(30_000)
|
||||
describe('When deleting a specific file', function () {
|
||||
let webtorrentId: string
|
||||
let hlsId: string
|
||||
|
||||
await servers[0].videos.removeHLSFiles({ videoId: validId2 })
|
||||
before(async function () {
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' })
|
||||
webtorrentId = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' })
|
||||
hlsId = uuid
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: validId2 })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
expect(video.files).to.have.length.above(0)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
}
|
||||
it('Shoulde delete a webtorrent file', async function () {
|
||||
const video = await servers[0].videos.get({ id: webtorrentId })
|
||||
const files = video.files
|
||||
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: files[0].id })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: webtorrentId })
|
||||
|
||||
expect(video.files).to.have.lengthOf(files.length - 1)
|
||||
expect(video.files.find(f => f.id === files[0].id)).to.not.exist
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete all webtorrent files', async function () {
|
||||
const video = await servers[0].videos.get({ id: webtorrentId })
|
||||
const files = video.files
|
||||
|
||||
for (const file of files) {
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: file.id })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: webtorrentId })
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete a hls file', async function () {
|
||||
const video = await servers[0].videos.get({ id: hlsId })
|
||||
const files = video.streamingPlaylists[0].files
|
||||
const toDelete = files[0]
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: toDelete.id })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: hlsId })
|
||||
|
||||
expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1)
|
||||
expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist
|
||||
|
||||
const { text } = await makeRawRequest(video.streamingPlaylists[0].playlistUrl)
|
||||
|
||||
expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false
|
||||
expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete all hls files', async function () {
|
||||
const video = await servers[0].videos.get({ id: hlsId })
|
||||
const files = video.streamingPlaylists[0].files
|
||||
|
||||
for (const file of files) {
|
||||
await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: file.id })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: hlsId })
|
||||
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not delete last file of a video', async function () {
|
||||
const webtorrentOnly = await servers[0].videos.get({ id: hlsId })
|
||||
const hlsOnly = await servers[0].videos.get({ id: webtorrentId })
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[i].id })
|
||||
await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id })
|
||||
}
|
||||
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[4].id, expectedStatus })
|
||||
await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
|
|
|
@ -20,10 +20,10 @@ import {
|
|||
VideosCommonQuery,
|
||||
VideoTranscodingCreate
|
||||
} from '@shared/models'
|
||||
import { VideoSource } from '@shared/models/videos/video-source'
|
||||
import { unwrapBody } from '../requests'
|
||||
import { waitJobs } from '../server'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
import { VideoSource } from '@shared/models/videos/video-source'
|
||||
|
||||
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
|
||||
fixture?: string
|
||||
|
@ -605,7 +605,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
removeHLSFiles (options: OverrideCommandOptions & {
|
||||
removeHLSPlaylist (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/hls'
|
||||
|
@ -619,7 +619,22 @@ export class VideosCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
removeWebTorrentFiles (options: OverrideCommandOptions & {
|
||||
removeHLSFile (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fileId: number
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/hls/' + options.fileId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeAllWebTorrentFiles (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/webtorrent'
|
||||
|
@ -633,6 +648,21 @@ export class VideosCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
removeWebTorrentFile (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fileId: number
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/webtorrent/' + options.fileId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
runTranscoding (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
transcodingType: 'hls' | 'webtorrent'
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -4635,6 +4635,11 @@ eventemitter-asyncresource@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b"
|
||||
integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==
|
||||
|
||||
eventemitter3@^4.0.4:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
events@3.3.0, events@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
|
@ -7122,6 +7127,14 @@ p-map@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
||||
|
||||
p-queue@^6:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
|
||||
integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.4"
|
||||
p-timeout "^3.2.0"
|
||||
|
||||
p-timeout@^3.0.0, p-timeout@^3.1.0, p-timeout@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
|
||||
|
|
Loading…
Reference in a new issue