diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/videos/files.ts | 62 | ||||
-rw-r--r-- | server/helpers/ffmpeg/ffprobe-utils.ts | 8 | ||||
-rw-r--r-- | server/lib/activitypub/videos/shared/abstract-builder.ts | 25 | ||||
-rw-r--r-- | server/lib/hls.ts | 147 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-file-import.ts | 2 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-studio-edition.ts | 18 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-transcoding.ts | 2 | ||||
-rw-r--r-- | server/lib/transcoding/transcoding.ts | 61 | ||||
-rw-r--r-- | server/lib/video-file.ts | 69 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-files.ts | 87 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 36 | ||||
-rw-r--r-- | server/models/video/video.ts | 10 | ||||
-rw-r--r-- | server/tests/api/check-params/video-files.ts | 80 | ||||
-rw-r--r-- | server/tests/api/transcoding/create-transcoding.ts | 4 | ||||
-rw-r--r-- | server/tests/api/videos/video-files.ts | 172 |
16 files changed, 586 insertions, 199 deletions
diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts index 0fbda280e..6d9c0b843 100644 --- a/server/controllers/api/videos/files.ts +++ b/server/controllers/api/videos/files.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import toInt from 'validator/lib/toInt' | 2 | import toInt from 'validator/lib/toInt' |
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
5 | import { removeAllWebTorrentFiles, removeHLSFile, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file' | ||
5 | import { VideoFileModel } from '@server/models/video/video-file' | 6 | import { VideoFileModel } from '@server/models/video/video-file' |
6 | import { HttpStatusCode, UserRight } from '@shared/models' | 7 | import { HttpStatusCode, UserRight } from '@shared/models' |
7 | import { | 8 | import { |
@@ -9,10 +10,13 @@ import { | |||
9 | authenticate, | 10 | authenticate, |
10 | ensureUserHasRight, | 11 | ensureUserHasRight, |
11 | videoFileMetadataGetValidator, | 12 | videoFileMetadataGetValidator, |
13 | videoFilesDeleteHLSFileValidator, | ||
12 | videoFilesDeleteHLSValidator, | 14 | videoFilesDeleteHLSValidator, |
15 | videoFilesDeleteWebTorrentFileValidator, | ||
13 | videoFilesDeleteWebTorrentValidator, | 16 | videoFilesDeleteWebTorrentValidator, |
14 | videosGetValidator | 17 | videosGetValidator |
15 | } from '../../../middlewares' | 18 | } from '../../../middlewares' |
19 | import { updatePlaylistAfterFileChange } from '@server/lib/hls' | ||
16 | 20 | ||
17 | const lTags = loggerTagsFactory('api', 'video') | 21 | const lTags = loggerTagsFactory('api', 'video') |
18 | const filesRouter = express.Router() | 22 | const filesRouter = express.Router() |
@@ -27,14 +31,26 @@ filesRouter.delete('/:id/hls', | |||
27 | authenticate, | 31 | authenticate, |
28 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | 32 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), |
29 | asyncMiddleware(videoFilesDeleteHLSValidator), | 33 | asyncMiddleware(videoFilesDeleteHLSValidator), |
30 | asyncMiddleware(removeHLSPlaylist) | 34 | asyncMiddleware(removeHLSPlaylistController) |
35 | ) | ||
36 | filesRouter.delete('/:id/hls/:videoFileId', | ||
37 | authenticate, | ||
38 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
39 | asyncMiddleware(videoFilesDeleteHLSFileValidator), | ||
40 | asyncMiddleware(removeHLSFileController) | ||
31 | ) | 41 | ) |
32 | 42 | ||
33 | filesRouter.delete('/:id/webtorrent', | 43 | filesRouter.delete('/:id/webtorrent', |
34 | authenticate, | 44 | authenticate, |
35 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | 45 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), |
36 | asyncMiddleware(videoFilesDeleteWebTorrentValidator), | 46 | asyncMiddleware(videoFilesDeleteWebTorrentValidator), |
37 | asyncMiddleware(removeWebTorrentFiles) | 47 | asyncMiddleware(removeAllWebTorrentFilesController) |
48 | ) | ||
49 | filesRouter.delete('/:id/webtorrent/:videoFileId', | ||
50 | authenticate, | ||
51 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
52 | asyncMiddleware(videoFilesDeleteWebTorrentFileValidator), | ||
53 | asyncMiddleware(removeWebTorrentFileController) | ||
38 | ) | 54 | ) |
39 | 55 | ||
40 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
@@ -51,33 +67,53 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response | |||
51 | return res.json(videoFile.metadata) | 67 | return res.json(videoFile.metadata) |
52 | } | 68 | } |
53 | 69 | ||
54 | async function removeHLSPlaylist (req: express.Request, res: express.Response) { | 70 | // --------------------------------------------------------------------------- |
71 | |||
72 | async function removeHLSPlaylistController (req: express.Request, res: express.Response) { | ||
55 | const video = res.locals.videoAll | 73 | const video = res.locals.videoAll |
56 | 74 | ||
57 | logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid)) | 75 | logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid)) |
76 | await removeHLSPlaylist(video) | ||
77 | |||
78 | await federateVideoIfNeeded(video, false, undefined) | ||
79 | |||
80 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
81 | } | ||
82 | |||
83 | async function removeHLSFileController (req: express.Request, res: express.Response) { | ||
84 | const video = res.locals.videoAll | ||
85 | const videoFileId = +req.params.videoFileId | ||
58 | 86 | ||
59 | const hls = video.getHLSPlaylist() | 87 | logger.info('Deleting HLS file %d of %s.', videoFileId, video.url, lTags(video.uuid)) |
60 | await video.removeStreamingPlaylistFiles(hls) | ||
61 | await hls.destroy() | ||
62 | 88 | ||
63 | video.VideoStreamingPlaylists = video.VideoStreamingPlaylists.filter(p => p.id !== hls.id) | 89 | const playlist = await removeHLSFile(video, videoFileId) |
90 | if (playlist) await updatePlaylistAfterFileChange(video, playlist) | ||
64 | 91 | ||
65 | await federateVideoIfNeeded(video, false, undefined) | 92 | await federateVideoIfNeeded(video, false, undefined) |
66 | 93 | ||
67 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 94 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
68 | } | 95 | } |
69 | 96 | ||
70 | async function removeWebTorrentFiles (req: express.Request, res: express.Response) { | 97 | // --------------------------------------------------------------------------- |
98 | |||
99 | async function removeAllWebTorrentFilesController (req: express.Request, res: express.Response) { | ||
71 | const video = res.locals.videoAll | 100 | const video = res.locals.videoAll |
72 | 101 | ||
73 | logger.info('Deleting WebTorrent files of %s.', video.url, lTags(video.uuid)) | 102 | logger.info('Deleting WebTorrent files of %s.', video.url, lTags(video.uuid)) |
74 | 103 | ||
75 | for (const file of video.VideoFiles) { | 104 | await removeAllWebTorrentFiles(video) |
76 | await video.removeWebTorrentFileAndTorrent(file) | 105 | await federateVideoIfNeeded(video, false, undefined) |
77 | await file.destroy() | 106 | |
78 | } | 107 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
108 | } | ||
109 | |||
110 | async function removeWebTorrentFileController (req: express.Request, res: express.Response) { | ||
111 | const video = res.locals.videoAll | ||
112 | |||
113 | const videoFileId = +req.params.videoFileId | ||
114 | logger.info('Deleting WebTorrent file %d of %s.', videoFileId, video.url, lTags(video.uuid)) | ||
79 | 115 | ||
80 | video.VideoFiles = [] | 116 | await removeWebTorrentFile(video, videoFileId) |
81 | await federateVideoIfNeeded(video, false, undefined) | 117 | await federateVideoIfNeeded(video, false, undefined) |
82 | 118 | ||
83 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 119 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
diff --git a/server/helpers/ffmpeg/ffprobe-utils.ts b/server/helpers/ffmpeg/ffprobe-utils.ts index a9b4fb456..9529162eb 100644 --- a/server/helpers/ffmpeg/ffprobe-utils.ts +++ b/server/helpers/ffmpeg/ffprobe-utils.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import { FfprobeData } from 'fluent-ffmpeg' | 1 | import { FfprobeData } from 'fluent-ffmpeg' |
2 | import { getMaxBitrate } from '@shared/core-utils' | 2 | import { getMaxBitrate } from '@shared/core-utils' |
3 | import { | 3 | import { |
4 | buildFileMetadata, | ||
4 | ffprobePromise, | 5 | ffprobePromise, |
5 | getAudioStream, | 6 | getAudioStream, |
6 | getVideoStreamDuration, | ||
7 | getMaxAudioBitrate, | 7 | getMaxAudioBitrate, |
8 | buildFileMetadata, | ||
9 | getVideoStreamBitrate, | ||
10 | getVideoStreamFPS, | ||
11 | getVideoStream, | 8 | getVideoStream, |
9 | getVideoStreamBitrate, | ||
12 | getVideoStreamDimensionsInfo, | 10 | getVideoStreamDimensionsInfo, |
11 | getVideoStreamDuration, | ||
12 | getVideoStreamFPS, | ||
13 | hasAudioStream | 13 | hasAudioStream |
14 | } from '@shared/extra-utils/ffprobe' | 14 | } from '@shared/extra-utils/ffprobe' |
15 | import { VideoResolution, VideoTranscodingFPS } from '@shared/models' | 15 | import { VideoResolution, VideoTranscodingFPS } from '@shared/models' |
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts index f299ba4fd..c0b92c93d 100644 --- a/server/lib/activitypub/videos/shared/abstract-builder.ts +++ b/server/lib/activitypub/videos/shared/abstract-builder.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { CreationAttributes, Transaction } from 'sequelize/types' |
2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' | 2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' |
3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
4 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | 4 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' |
@@ -7,7 +7,15 @@ import { VideoCaptionModel } from '@server/models/video/video-caption' | |||
7 | import { VideoFileModel } from '@server/models/video/video-file' | 7 | import { VideoFileModel } from '@server/models/video/video-file' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 8 | import { VideoLiveModel } from '@server/models/video/video-live' |
9 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 9 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
10 | import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | 10 | import { |
11 | MStreamingPlaylistFiles, | ||
12 | MStreamingPlaylistFilesVideo, | ||
13 | MThumbnail, | ||
14 | MVideoCaption, | ||
15 | MVideoFile, | ||
16 | MVideoFullLight, | ||
17 | MVideoThumbnail | ||
18 | } from '@server/types/models' | ||
11 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' | 19 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' |
12 | import { getOrCreateAPActor } from '../../actors' | 20 | import { getOrCreateAPActor } from '../../actors' |
13 | import { checkUrlsSameHost } from '../../url' | 21 | import { checkUrlsSameHost } from '../../url' |
@@ -125,38 +133,39 @@ export abstract class APVideoAbstractBuilder { | |||
125 | // Remove video playlists that do not exist anymore | 133 | // Remove video playlists that do not exist anymore |
126 | await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t) | 134 | await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t) |
127 | 135 | ||
136 | const oldPlaylists = video.VideoStreamingPlaylists | ||
128 | video.VideoStreamingPlaylists = [] | 137 | video.VideoStreamingPlaylists = [] |
129 | 138 | ||
130 | for (const playlistAttributes of streamingPlaylistAttributes) { | 139 | for (const playlistAttributes of streamingPlaylistAttributes) { |
131 | const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t) | 140 | const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t) |
132 | streamingPlaylistModel.Video = video | 141 | streamingPlaylistModel.Video = video |
133 | 142 | ||
134 | await this.setStreamingPlaylistFiles(video, streamingPlaylistModel, playlistAttributes.tagAPObject, t) | 143 | await this.setStreamingPlaylistFiles(oldPlaylists, streamingPlaylistModel, playlistAttributes.tagAPObject, t) |
135 | 144 | ||
136 | video.VideoStreamingPlaylists.push(streamingPlaylistModel) | 145 | video.VideoStreamingPlaylists.push(streamingPlaylistModel) |
137 | } | 146 | } |
138 | } | 147 | } |
139 | 148 | ||
140 | private async insertOrReplaceStreamingPlaylist (attributes: VideoStreamingPlaylistModel['_creationAttributes'], t: Transaction) { | 149 | private async insertOrReplaceStreamingPlaylist (attributes: CreationAttributes<VideoStreamingPlaylistModel>, t: Transaction) { |
141 | const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t }) | 150 | const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t }) |
142 | 151 | ||
143 | return streamingPlaylist as MStreamingPlaylistFilesVideo | 152 | return streamingPlaylist as MStreamingPlaylistFilesVideo |
144 | } | 153 | } |
145 | 154 | ||
146 | private getStreamingPlaylistFiles (video: MVideoFullLight, type: VideoStreamingPlaylistType) { | 155 | private getStreamingPlaylistFiles (oldPlaylists: MStreamingPlaylistFiles[], type: VideoStreamingPlaylistType) { |
147 | const playlist = video.VideoStreamingPlaylists.find(s => s.type === type) | 156 | const playlist = oldPlaylists.find(s => s.type === type) |
148 | if (!playlist) return [] | 157 | if (!playlist) return [] |
149 | 158 | ||
150 | return playlist.VideoFiles | 159 | return playlist.VideoFiles |
151 | } | 160 | } |
152 | 161 | ||
153 | private async setStreamingPlaylistFiles ( | 162 | private async setStreamingPlaylistFiles ( |
154 | video: MVideoFullLight, | 163 | oldPlaylists: MStreamingPlaylistFiles[], |
155 | playlistModel: MStreamingPlaylistFilesVideo, | 164 | playlistModel: MStreamingPlaylistFilesVideo, |
156 | tagObjects: ActivityTagObject[], | 165 | tagObjects: ActivityTagObject[], |
157 | t: Transaction | 166 | t: Transaction |
158 | ) { | 167 | ) { |
159 | const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(video, playlistModel.type) | 168 | const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type) |
160 | 169 | ||
161 | const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a)) | 170 | const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a)) |
162 | 171 | ||
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 43043315b..20754219f 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra' | 1 | import { close, ensureDir, move, open, outputJSON, read, readFile, remove, stat, writeFile } from 'fs-extra' |
2 | import { flatten, uniq } from 'lodash' | 2 | import { flatten, uniq } from 'lodash' |
3 | import PQueue from 'p-queue' | ||
3 | import { basename, dirname, join } from 'path' | 4 | import { basename, dirname, join } from 'path' |
4 | import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' | 5 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' |
5 | import { sha256 } from '@shared/extra-utils' | 6 | import { sha256 } from '@shared/extra-utils' |
6 | import { VideoStorage } from '@shared/models' | 7 | import { VideoStorage } from '@shared/models' |
7 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg' | 8 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg' |
@@ -14,7 +15,7 @@ import { sequelizeTypescript } from '../initializers/database' | |||
14 | import { VideoFileModel } from '../models/video/video-file' | 15 | import { VideoFileModel } from '../models/video/video-file' |
15 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 16 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
16 | import { storeHLSFile } from './object-storage' | 17 | import { storeHLSFile } from './object-storage' |
17 | import { getHlsResolutionPlaylistFilename } from './paths' | 18 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths' |
18 | import { VideoPathManager } from './video-path-manager' | 19 | import { VideoPathManager } from './video-path-manager' |
19 | 20 | ||
20 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 21 | async function updateStreamingPlaylistsInfohashesIfNeeded () { |
@@ -33,80 +34,123 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () { | |||
33 | } | 34 | } |
34 | } | 35 | } |
35 | 36 | ||
36 | async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlaylistFilesVideo) { | 37 | async function updatePlaylistAfterFileChange (video: MVideo, playlist: MStreamingPlaylist) { |
37 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | 38 | let playlistWithFiles = await updateMasterHLSPlaylist(video, playlist) |
39 | playlistWithFiles = await updateSha256VODSegments(video, playlist) | ||
38 | 40 | ||
39 | for (const file of playlist.VideoFiles) { | 41 | // Refresh playlist, operations can take some time |
40 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) | 42 | playlistWithFiles = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlist.id) |
43 | playlistWithFiles.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles) | ||
44 | await playlistWithFiles.save() | ||
41 | 45 | ||
42 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { | 46 | video.setHLSPlaylist(playlistWithFiles) |
43 | const size = await getVideoStreamDimensionsInfo(videoFilePath) | 47 | } |
44 | 48 | ||
45 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | 49 | // --------------------------------------------------------------------------- |
46 | const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}` | ||
47 | 50 | ||
48 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | 51 | // Avoid concurrency issues when updating streaming playlist files |
49 | if (file.fps) line += ',FRAME-RATE=' + file.fps | 52 | const playlistFilesQueue = new PQueue({ concurrency: 1 }) |
50 | 53 | ||
51 | const codecs = await Promise.all([ | 54 | function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> { |
52 | getVideoStreamCodec(videoFilePath), | 55 | return playlistFilesQueue.add(async () => { |
53 | getAudioStreamCodec(videoFilePath) | 56 | const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id) |
54 | ]) | ||
55 | 57 | ||
56 | line += `,CODECS="${codecs.filter(c => !!c).join(',')}"` | 58 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] |
57 | 59 | ||
58 | masterPlaylists.push(line) | 60 | for (const file of playlist.VideoFiles) { |
59 | masterPlaylists.push(playlistFilename) | 61 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) |
60 | }) | 62 | |
61 | } | 63 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { |
64 | const size = await getVideoStreamDimensionsInfo(videoFilePath) | ||
65 | |||
66 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | ||
67 | const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}` | ||
68 | |||
69 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | ||
70 | if (file.fps) line += ',FRAME-RATE=' + file.fps | ||
71 | |||
72 | const codecs = await Promise.all([ | ||
73 | getVideoStreamCodec(videoFilePath), | ||
74 | getAudioStreamCodec(videoFilePath) | ||
75 | ]) | ||
62 | 76 | ||
63 | await VideoPathManager.Instance.makeAvailablePlaylistFile(playlist, playlist.playlistFilename, async masterPlaylistPath => { | 77 | line += `,CODECS="${codecs.filter(c => !!c).join(',')}"` |
78 | |||
79 | masterPlaylists.push(line) | ||
80 | masterPlaylists.push(playlistFilename) | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | if (playlist.playlistFilename) { | ||
85 | await video.removeStreamingPlaylistFile(playlist, playlist.playlistFilename) | ||
86 | } | ||
87 | playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive) | ||
88 | |||
89 | const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename) | ||
64 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | 90 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') |
65 | 91 | ||
66 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { | 92 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { |
67 | await storeHLSFile(playlist, playlist.playlistFilename, masterPlaylistPath) | 93 | playlist.playlistUrl = await storeHLSFile(playlist, playlist.playlistFilename) |
94 | await remove(masterPlaylistPath) | ||
68 | } | 95 | } |
96 | |||
97 | return playlist.save() | ||
69 | }) | 98 | }) |
70 | } | 99 | } |
71 | 100 | ||
72 | async function updateSha256VODSegments (video: MVideoUUID, playlist: MStreamingPlaylistFilesVideo) { | 101 | // --------------------------------------------------------------------------- |
73 | const json: { [filename: string]: { [range: string]: string } } = {} | 102 | |
103 | async function updateSha256VODSegments (video: MVideo, playlistArg: MStreamingPlaylist): Promise<MStreamingPlaylistFilesVideo> { | ||
104 | return playlistFilesQueue.add(async () => { | ||
105 | const json: { [filename: string]: { [range: string]: string } } = {} | ||
74 | 106 | ||
75 | // For all the resolutions available for this video | 107 | const playlist = await VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id) |
76 | for (const file of playlist.VideoFiles) { | ||
77 | const rangeHashes: { [range: string]: string } = {} | ||
78 | const fileWithPlaylist = file.withVideoOrPlaylist(playlist) | ||
79 | 108 | ||
80 | await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => { | 109 | // For all the resolutions available for this video |
110 | for (const file of playlist.VideoFiles) { | ||
111 | const rangeHashes: { [range: string]: string } = {} | ||
112 | const fileWithPlaylist = file.withVideoOrPlaylist(playlist) | ||
81 | 113 | ||
82 | return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => { | 114 | await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => { |
83 | const playlistContent = await readFile(resolutionPlaylistPath) | ||
84 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | ||
85 | 115 | ||
86 | const fd = await open(videoPath, 'r') | 116 | return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => { |
87 | for (const range of ranges) { | 117 | const playlistContent = await readFile(resolutionPlaylistPath) |
88 | const buf = Buffer.alloc(range.length) | 118 | const ranges = getRangesFromPlaylist(playlistContent.toString()) |
89 | await read(fd, buf, 0, range.length, range.offset) | ||
90 | 119 | ||
91 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | 120 | const fd = await open(videoPath, 'r') |
92 | } | 121 | for (const range of ranges) { |
93 | await close(fd) | 122 | const buf = Buffer.alloc(range.length) |
123 | await read(fd, buf, 0, range.length, range.offset) | ||
94 | 124 | ||
95 | const videoFilename = file.filename | 125 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) |
96 | json[videoFilename] = rangeHashes | 126 | } |
127 | await close(fd) | ||
128 | |||
129 | const videoFilename = file.filename | ||
130 | json[videoFilename] = rangeHashes | ||
131 | }) | ||
97 | }) | 132 | }) |
98 | }) | 133 | } |
99 | } | ||
100 | 134 | ||
101 | const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename) | 135 | if (playlist.segmentsSha256Filename) { |
102 | await outputJSON(outputPath, json) | 136 | await video.removeStreamingPlaylistFile(playlist, playlist.segmentsSha256Filename) |
137 | } | ||
138 | playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive) | ||
103 | 139 | ||
104 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { | 140 | const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename) |
105 | await storeHLSFile(playlist, playlist.segmentsSha256Filename) | 141 | await outputJSON(outputPath, json) |
106 | await remove(outputPath) | 142 | |
107 | } | 143 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { |
144 | playlist.segmentsSha256Url = await storeHLSFile(playlist, playlist.segmentsSha256Filename) | ||
145 | await remove(outputPath) | ||
146 | } | ||
147 | |||
148 | return playlist.save() | ||
149 | }) | ||
108 | } | 150 | } |
109 | 151 | ||
152 | // --------------------------------------------------------------------------- | ||
153 | |||
110 | async function buildSha256Segment (segmentPath: string) { | 154 | async function buildSha256Segment (segmentPath: string) { |
111 | const buf = await readFile(segmentPath) | 155 | const buf = await readFile(segmentPath) |
112 | return sha256(buf) | 156 | return sha256(buf) |
@@ -190,7 +234,8 @@ export { | |||
190 | updateSha256VODSegments, | 234 | updateSha256VODSegments, |
191 | buildSha256Segment, | 235 | buildSha256Segment, |
192 | downloadPlaylistSegments, | 236 | downloadPlaylistSegments, |
193 | updateStreamingPlaylistsInfohashesIfNeeded | 237 | updateStreamingPlaylistsInfohashesIfNeeded, |
238 | updatePlaylistAfterFileChange | ||
194 | } | 239 | } |
195 | 240 | ||
196 | // --------------------------------------------------------------------------- | 241 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 1c600e2a7..71c5444af 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -55,7 +55,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
55 | 55 | ||
56 | if (currentVideoFile) { | 56 | if (currentVideoFile) { |
57 | // Remove old file and old torrent | 57 | // Remove old file and old torrent |
58 | await video.removeWebTorrentFileAndTorrent(currentVideoFile) | 58 | await video.removeWebTorrentFile(currentVideoFile) |
59 | // Remove the old video file from the array | 59 | // Remove the old video file from the array |
60 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) | 60 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) |
61 | 61 | ||
diff --git a/server/lib/job-queue/handlers/video-studio-edition.ts b/server/lib/job-queue/handlers/video-studio-edition.ts index 434d0ffe8..735150d57 100644 --- a/server/lib/job-queue/handlers/video-studio-edition.ts +++ b/server/lib/job-queue/handlers/video-studio-edition.ts | |||
@@ -9,6 +9,7 @@ import { generateWebTorrentVideoFilename } from '@server/lib/paths' | |||
9 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' | 9 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' |
10 | import { isAbleToUploadVideo } from '@server/lib/user' | 10 | import { isAbleToUploadVideo } from '@server/lib/user' |
11 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' | 11 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' |
12 | import { removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file' | ||
12 | import { VideoPathManager } from '@server/lib/video-path-manager' | 13 | import { VideoPathManager } from '@server/lib/video-path-manager' |
13 | import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio' | 14 | import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio' |
14 | import { UserModel } from '@server/models/user/user' | 15 | import { UserModel } from '@server/models/user/user' |
@@ -27,12 +28,12 @@ import { | |||
27 | } from '@shared/extra-utils' | 28 | } from '@shared/extra-utils' |
28 | import { | 29 | import { |
29 | VideoStudioEditionPayload, | 30 | VideoStudioEditionPayload, |
30 | VideoStudioTaskPayload, | 31 | VideoStudioTask, |
31 | VideoStudioTaskCutPayload, | 32 | VideoStudioTaskCutPayload, |
32 | VideoStudioTaskIntroPayload, | 33 | VideoStudioTaskIntroPayload, |
33 | VideoStudioTaskOutroPayload, | 34 | VideoStudioTaskOutroPayload, |
34 | VideoStudioTaskWatermarkPayload, | 35 | VideoStudioTaskPayload, |
35 | VideoStudioTask | 36 | VideoStudioTaskWatermarkPayload |
36 | } from '@shared/models' | 37 | } from '@shared/models' |
37 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 38 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
38 | 39 | ||
@@ -89,7 +90,6 @@ async function processVideoStudioEdition (job: Job) { | |||
89 | await move(editionResultPath, outputPath) | 90 | await move(editionResultPath, outputPath) |
90 | 91 | ||
91 | await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath) | 92 | await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath) |
92 | |||
93 | await removeAllFiles(video, newFile) | 93 | await removeAllFiles(video, newFile) |
94 | 94 | ||
95 | await newFile.save() | 95 | await newFile.save() |
@@ -197,18 +197,12 @@ async function buildNewFile (video: MVideoId, path: string) { | |||
197 | } | 197 | } |
198 | 198 | ||
199 | async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { | 199 | async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { |
200 | const hls = video.getHLSPlaylist() | 200 | await removeHLSPlaylist(video) |
201 | |||
202 | if (hls) { | ||
203 | await video.removeStreamingPlaylistFiles(hls) | ||
204 | await hls.destroy() | ||
205 | } | ||
206 | 201 | ||
207 | for (const file of video.VideoFiles) { | 202 | for (const file of video.VideoFiles) { |
208 | if (file.id === webTorrentFileException.id) continue | 203 | if (file.id === webTorrentFileException.id) continue |
209 | 204 | ||
210 | await video.removeWebTorrentFileAndTorrent(file) | 205 | await removeWebTorrentFile(video, file.id) |
211 | await file.destroy() | ||
212 | } | 206 | } |
213 | } | 207 | } |
214 | 208 | ||
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 5afca65ca..1b34ced14 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -149,7 +149,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay | |||
149 | if (payload.isMaxQuality && payload.autoDeleteWebTorrentIfNeeded && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { | 149 | if (payload.isMaxQuality && payload.autoDeleteWebTorrentIfNeeded && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { |
150 | // Remove webtorrent files if not enabled | 150 | // Remove webtorrent files if not enabled |
151 | for (const file of video.VideoFiles) { | 151 | for (const file of video.VideoFiles) { |
152 | await video.removeWebTorrentFileAndTorrent(file) | 152 | await video.removeWebTorrentFile(file) |
153 | await file.destroy() | 153 | await file.destroy() |
154 | } | 154 | } |
155 | 155 | ||
diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts index 69a973fbd..924141d1c 100644 --- a/server/lib/transcoding/transcoding.ts +++ b/server/lib/transcoding/transcoding.ts | |||
@@ -5,9 +5,8 @@ import { toEven } from '@server/helpers/core-utils' | |||
5 | import { retryTransactionWrapper } from '@server/helpers/database-utils' | 5 | import { retryTransactionWrapper } from '@server/helpers/database-utils' |
6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
7 | import { sequelizeTypescript } from '@server/initializers/database' | 7 | import { sequelizeTypescript } from '@server/initializers/database' |
8 | import { MStreamingPlaylistFilesVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 8 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
9 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' | 9 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' |
10 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
11 | import { | 10 | import { |
12 | buildFileMetadata, | 11 | buildFileMetadata, |
13 | canDoQuickTranscode, | 12 | canDoQuickTranscode, |
@@ -18,17 +17,10 @@ import { | |||
18 | TranscodeVODOptionsType | 17 | TranscodeVODOptionsType |
19 | } from '../../helpers/ffmpeg' | 18 | } from '../../helpers/ffmpeg' |
20 | import { CONFIG } from '../../initializers/config' | 19 | import { CONFIG } from '../../initializers/config' |
21 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' | ||
22 | import { VideoFileModel } from '../../models/video/video-file' | 20 | import { VideoFileModel } from '../../models/video/video-file' |
23 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | 21 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
24 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' | 22 | import { updatePlaylistAfterFileChange } from '../hls' |
25 | import { | 23 | import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getHlsResolutionPlaylistFilename } from '../paths' |
26 | generateHLSMasterPlaylistFilename, | ||
27 | generateHlsSha256SegmentsFilename, | ||
28 | generateHLSVideoFilename, | ||
29 | generateWebTorrentVideoFilename, | ||
30 | getHlsResolutionPlaylistFilename | ||
31 | } from '../paths' | ||
32 | import { VideoPathManager } from '../video-path-manager' | 24 | import { VideoPathManager } from '../video-path-manager' |
33 | import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' | 25 | import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' |
34 | 26 | ||
@@ -260,7 +252,7 @@ async function onWebTorrentVideoFileTranscoding ( | |||
260 | await createTorrentAndSetInfoHash(video, videoFile) | 252 | await createTorrentAndSetInfoHash(video, videoFile) |
261 | 253 | ||
262 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | 254 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) |
263 | if (oldFile) await video.removeWebTorrentFileAndTorrent(oldFile) | 255 | if (oldFile) await video.removeWebTorrentFile(oldFile) |
264 | 256 | ||
265 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | 257 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) |
266 | video.VideoFiles = await video.$get('VideoFiles') | 258 | video.VideoFiles = await video.$get('VideoFiles') |
@@ -314,35 +306,15 @@ async function generateHlsPlaylistCommon (options: { | |||
314 | await transcodeVOD(transcodeOptions) | 306 | await transcodeVOD(transcodeOptions) |
315 | 307 | ||
316 | // Create or update the playlist | 308 | // Create or update the playlist |
317 | const { playlist, oldPlaylistFilename, oldSegmentsSha256Filename } = await retryTransactionWrapper(() => { | 309 | const playlist = await retryTransactionWrapper(() => { |
318 | return sequelizeTypescript.transaction(async transaction => { | 310 | return sequelizeTypescript.transaction(async transaction => { |
319 | const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video, transaction) | 311 | return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction) |
320 | |||
321 | const oldPlaylistFilename = playlist.playlistFilename | ||
322 | const oldSegmentsSha256Filename = playlist.segmentsSha256Filename | ||
323 | |||
324 | playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive) | ||
325 | playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive) | ||
326 | |||
327 | playlist.p2pMediaLoaderInfohashes = [] | ||
328 | playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION | ||
329 | |||
330 | playlist.type = VideoStreamingPlaylistType.HLS | ||
331 | |||
332 | await playlist.save({ transaction }) | ||
333 | |||
334 | return { playlist, oldPlaylistFilename, oldSegmentsSha256Filename } | ||
335 | }) | 312 | }) |
336 | }) | 313 | }) |
337 | 314 | ||
338 | if (oldPlaylistFilename) await video.removeStreamingPlaylistFile(playlist, oldPlaylistFilename) | ||
339 | if (oldSegmentsSha256Filename) await video.removeStreamingPlaylistFile(playlist, oldSegmentsSha256Filename) | ||
340 | |||
341 | // Build the new playlist file | ||
342 | const extname = extnameUtil(videoFilename) | ||
343 | const newVideoFile = new VideoFileModel({ | 315 | const newVideoFile = new VideoFileModel({ |
344 | resolution, | 316 | resolution, |
345 | extname, | 317 | extname: extnameUtil(videoFilename), |
346 | size: 0, | 318 | size: 0, |
347 | filename: videoFilename, | 319 | filename: videoFilename, |
348 | fps: -1, | 320 | fps: -1, |
@@ -350,8 +322,6 @@ async function generateHlsPlaylistCommon (options: { | |||
350 | }) | 322 | }) |
351 | 323 | ||
352 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) | 324 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) |
353 | |||
354 | // Move files from tmp transcoded directory to the appropriate place | ||
355 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) | 325 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) |
356 | 326 | ||
357 | // Move playlist file | 327 | // Move playlist file |
@@ -369,21 +339,14 @@ async function generateHlsPlaylistCommon (options: { | |||
369 | await createTorrentAndSetInfoHash(playlist, newVideoFile) | 339 | await createTorrentAndSetInfoHash(playlist, newVideoFile) |
370 | 340 | ||
371 | const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution }) | 341 | const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution }) |
372 | if (oldFile) await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | 342 | if (oldFile) { |
343 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | ||
344 | await oldFile.destroy() | ||
345 | } | ||
373 | 346 | ||
374 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) | 347 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) |
375 | 348 | ||
376 | const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo | 349 | await updatePlaylistAfterFileChange(video, playlist) |
377 | playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles') | ||
378 | playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles) | ||
379 | playlist.storage = VideoStorage.FILE_SYSTEM | ||
380 | |||
381 | await playlist.save() | ||
382 | |||
383 | video.setHLSPlaylist(playlist) | ||
384 | |||
385 | await updateMasterHLSPlaylist(video, playlistWithFiles) | ||
386 | await updateSha256VODSegments(video, playlistWithFiles) | ||
387 | 350 | ||
388 | return { resolutionPlaylistPath, videoFile: savedVideoFile } | 351 | return { resolutionPlaylistPath, videoFile: savedVideoFile } |
389 | } | 352 | } |
diff --git a/server/lib/video-file.ts b/server/lib/video-file.ts new file mode 100644 index 000000000..2ab7190f1 --- /dev/null +++ b/server/lib/video-file.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { MVideoWithAllFiles } from '@server/types/models' | ||
3 | import { lTags } from './object-storage/shared' | ||
4 | |||
5 | async function removeHLSPlaylist (video: MVideoWithAllFiles) { | ||
6 | const hls = video.getHLSPlaylist() | ||
7 | if (!hls) return | ||
8 | |||
9 | await video.removeStreamingPlaylistFiles(hls) | ||
10 | await hls.destroy() | ||
11 | |||
12 | video.VideoStreamingPlaylists = video.VideoStreamingPlaylists.filter(p => p.id !== hls.id) | ||
13 | } | ||
14 | |||
15 | async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) { | ||
16 | logger.info('Deleting HLS file %d of %s.', fileToDeleteId, video.url, lTags(video.uuid)) | ||
17 | |||
18 | const hls = video.getHLSPlaylist() | ||
19 | const files = hls.VideoFiles | ||
20 | |||
21 | if (files.length === 1) { | ||
22 | await removeHLSPlaylist(video) | ||
23 | return undefined | ||
24 | } | ||
25 | |||
26 | const toDelete = files.find(f => f.id === fileToDeleteId) | ||
27 | await video.removeStreamingPlaylistVideoFile(video.getHLSPlaylist(), toDelete) | ||
28 | await toDelete.destroy() | ||
29 | |||
30 | hls.VideoFiles = hls.VideoFiles.filter(f => f.id !== toDelete.id) | ||
31 | |||
32 | return hls | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | async function removeAllWebTorrentFiles (video: MVideoWithAllFiles) { | ||
38 | for (const file of video.VideoFiles) { | ||
39 | await video.removeWebTorrentFile(file) | ||
40 | await file.destroy() | ||
41 | } | ||
42 | |||
43 | video.VideoFiles = [] | ||
44 | |||
45 | return video | ||
46 | } | ||
47 | |||
48 | async function removeWebTorrentFile (video: MVideoWithAllFiles, fileToDeleteId: number) { | ||
49 | const files = video.VideoFiles | ||
50 | |||
51 | if (files.length === 1) { | ||
52 | return removeAllWebTorrentFiles(video) | ||
53 | } | ||
54 | |||
55 | const toDelete = files.find(f => f.id === fileToDeleteId) | ||
56 | await video.removeWebTorrentFile(toDelete) | ||
57 | await toDelete.destroy() | ||
58 | |||
59 | video.VideoFiles = files.filter(f => f.id !== toDelete.id) | ||
60 | |||
61 | return video | ||
62 | } | ||
63 | |||
64 | export { | ||
65 | removeHLSPlaylist, | ||
66 | removeHLSFile, | ||
67 | removeAllWebTorrentFiles, | ||
68 | removeWebTorrentFile | ||
69 | } | ||
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts index 35b0ac757..b3db3f4f7 100644 --- a/server/middlewares/validators/videos/video-files.ts +++ b/server/middlewares/validators/videos/video-files.ts | |||
@@ -3,6 +3,8 @@ import { MVideo } from '@server/types/models' | |||
3 | import { HttpStatusCode } from '@shared/models' | 3 | import { HttpStatusCode } from '@shared/models' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | 5 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
6 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
7 | import { param } from 'express-validator' | ||
6 | 8 | ||
7 | const videoFilesDeleteWebTorrentValidator = [ | 9 | const videoFilesDeleteWebTorrentValidator = [ |
8 | isValidVideoIdParam('id'), | 10 | isValidVideoIdParam('id'), |
@@ -35,6 +37,43 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
35 | } | 37 | } |
36 | ] | 38 | ] |
37 | 39 | ||
40 | const videoFilesDeleteWebTorrentFileValidator = [ | ||
41 | isValidVideoIdParam('id'), | ||
42 | |||
43 | param('videoFileId') | ||
44 | .custom(isIdValid).withMessage('Should have a valid file id'), | ||
45 | |||
46 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
47 | logger.debug('Checking videoFilesDeleteWebTorrentFile parameters', { parameters: req.params }) | ||
48 | |||
49 | if (areValidationErrors(req, res)) return | ||
50 | if (!await doesVideoExist(req.params.id, res)) return | ||
51 | |||
52 | const video = res.locals.videoAll | ||
53 | |||
54 | if (!checkLocalVideo(video, res)) return | ||
55 | |||
56 | const files = video.VideoFiles | ||
57 | if (!files.find(f => f.id === +req.params.videoFileId)) { | ||
58 | return res.fail({ | ||
59 | status: HttpStatusCode.NOT_FOUND_404, | ||
60 | message: 'This video does not have this WebTorrent file id' | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | if (files.length === 1 && !video.getHLSPlaylist()) { | ||
65 | return res.fail({ | ||
66 | status: HttpStatusCode.BAD_REQUEST_400, | ||
67 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | ||
68 | }) | ||
69 | } | ||
70 | |||
71 | return next() | ||
72 | } | ||
73 | ] | ||
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
38 | const videoFilesDeleteHLSValidator = [ | 77 | const videoFilesDeleteHLSValidator = [ |
39 | isValidVideoIdParam('id'), | 78 | isValidVideoIdParam('id'), |
40 | 79 | ||
@@ -66,9 +105,55 @@ const videoFilesDeleteHLSValidator = [ | |||
66 | } | 105 | } |
67 | ] | 106 | ] |
68 | 107 | ||
108 | const videoFilesDeleteHLSFileValidator = [ | ||
109 | isValidVideoIdParam('id'), | ||
110 | |||
111 | param('videoFileId') | ||
112 | .custom(isIdValid).withMessage('Should have a valid file id'), | ||
113 | |||
114 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
115 | logger.debug('Checking videoFilesDeleteHLSFile parameters', { parameters: req.params }) | ||
116 | |||
117 | if (areValidationErrors(req, res)) return | ||
118 | if (!await doesVideoExist(req.params.id, res)) return | ||
119 | |||
120 | const video = res.locals.videoAll | ||
121 | |||
122 | if (!checkLocalVideo(video, res)) return | ||
123 | |||
124 | if (!video.getHLSPlaylist()) { | ||
125 | return res.fail({ | ||
126 | status: HttpStatusCode.BAD_REQUEST_400, | ||
127 | message: 'This video does not have HLS files' | ||
128 | }) | ||
129 | } | ||
130 | |||
131 | const hlsFiles = video.getHLSPlaylist().VideoFiles | ||
132 | if (!hlsFiles.find(f => f.id === +req.params.videoFileId)) { | ||
133 | return res.fail({ | ||
134 | status: HttpStatusCode.NOT_FOUND_404, | ||
135 | message: 'This HLS playlist does not have this file id' | ||
136 | }) | ||
137 | } | ||
138 | |||
139 | // Last file to delete | ||
140 | if (hlsFiles.length === 1 && !video.hasWebTorrentFiles()) { | ||
141 | return res.fail({ | ||
142 | status: HttpStatusCode.BAD_REQUEST_400, | ||
143 | message: 'Cannot delete last HLS playlist file since this video does not have WebTorrent files' | ||
144 | }) | ||
145 | } | ||
146 | |||
147 | return next() | ||
148 | } | ||
149 | ] | ||
150 | |||
69 | export { | 151 | export { |
70 | videoFilesDeleteWebTorrentValidator, | 152 | videoFilesDeleteWebTorrentValidator, |
71 | videoFilesDeleteHLSValidator | 153 | videoFilesDeleteWebTorrentFileValidator, |
154 | |||
155 | videoFilesDeleteHLSValidator, | ||
156 | videoFilesDeleteHLSFileValidator | ||
72 | } | 157 | } |
73 | 158 | ||
74 | // --------------------------------------------------------------------------- | 159 | // --------------------------------------------------------------------------- |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index b363afb28..15909d5f3 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -162,7 +162,7 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu | |||
162 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 162 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
163 | logger.info('Removing duplicated video file %s.', logIdentifier) | 163 | logger.info('Removing duplicated video file %s.', logIdentifier) |
164 | 164 | ||
165 | videoFile.Video.removeWebTorrentFileAndTorrent(videoFile, true) | 165 | videoFile.Video.removeWebTorrentFile(videoFile, true) |
166 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 166 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
167 | } | 167 | } |
168 | 168 | ||
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 2c4dbd8ec..f587989dc 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -16,8 +16,9 @@ import { | |||
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { getHLSPublicFileUrl } from '@server/lib/object-storage' | 18 | import { getHLSPublicFileUrl } from '@server/lib/object-storage' |
19 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths' | ||
19 | import { VideoFileModel } from '@server/models/video/video-file' | 20 | import { VideoFileModel } from '@server/models/video/video-file' |
20 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | 21 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' |
21 | import { sha1 } from '@shared/extra-utils' | 22 | import { sha1 } from '@shared/extra-utils' |
22 | import { VideoStorage } from '@shared/models' | 23 | import { VideoStorage } from '@shared/models' |
23 | import { AttributesOnly } from '@shared/typescript-utils' | 24 | import { AttributesOnly } from '@shared/typescript-utils' |
@@ -167,6 +168,22 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
167 | return VideoStreamingPlaylistModel.findAll(query) | 168 | return VideoStreamingPlaylistModel.findAll(query) |
168 | } | 169 | } |
169 | 170 | ||
171 | static loadWithVideoAndFiles (id: number) { | ||
172 | const options = { | ||
173 | include: [ | ||
174 | { | ||
175 | model: VideoModel.unscoped(), | ||
176 | required: true | ||
177 | }, | ||
178 | { | ||
179 | model: VideoFileModel.unscoped() | ||
180 | } | ||
181 | ] | ||
182 | } | ||
183 | |||
184 | return VideoStreamingPlaylistModel.findByPk<MStreamingPlaylistFilesVideo>(id, options) | ||
185 | } | ||
186 | |||
170 | static loadWithVideo (id: number) { | 187 | static loadWithVideo (id: number) { |
171 | const options = { | 188 | const options = { |
172 | include: [ | 189 | include: [ |
@@ -194,9 +211,22 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
194 | 211 | ||
195 | static async loadOrGenerate (video: MVideo, transaction?: Transaction) { | 212 | static async loadOrGenerate (video: MVideo, transaction?: Transaction) { |
196 | let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id, transaction) | 213 | let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id, transaction) |
197 | if (!playlist) playlist = new VideoStreamingPlaylistModel() | ||
198 | 214 | ||
199 | return Object.assign(playlist, { videoId: video.id, Video: video }) | 215 | if (!playlist) { |
216 | playlist = new VideoStreamingPlaylistModel({ | ||
217 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | ||
218 | type: VideoStreamingPlaylistType.HLS, | ||
219 | storage: VideoStorage.FILE_SYSTEM, | ||
220 | p2pMediaLoaderInfohashes: [], | ||
221 | playlistFilename: generateHLSMasterPlaylistFilename(video.isLive), | ||
222 | segmentsSha256Filename: generateHlsSha256SegmentsFilename(video.isLive), | ||
223 | videoId: video.id | ||
224 | }) | ||
225 | |||
226 | await playlist.save({ transaction }) | ||
227 | } | ||
228 | |||
229 | return Object.assign(playlist, { Video: video }) | ||
200 | } | 230 | } |
201 | 231 | ||
202 | static doesOwnedHLSPlaylistExist (videoUUID: string) { | 232 | static doesOwnedHLSPlaylistExist (videoUUID: string) { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 7e9bb4db4..b8e383502 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -28,7 +28,7 @@ import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation | |||
28 | import { LiveManager } from '@server/lib/live/live-manager' | 28 | import { LiveManager } from '@server/lib/live/live-manager' |
29 | import { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' | 29 | import { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' |
30 | import { tracer } from '@server/lib/opentelemetry/tracing' | 30 | import { tracer } from '@server/lib/opentelemetry/tracing' |
31 | import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths' | 31 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
32 | import { VideoPathManager } from '@server/lib/video-path-manager' | 32 | import { VideoPathManager } from '@server/lib/video-path-manager' |
33 | import { getServerActor } from '@server/models/application/application' | 33 | import { getServerActor } from '@server/models/application/application' |
34 | import { ModelCache } from '@server/models/model-cache' | 34 | import { ModelCache } from '@server/models/model-cache' |
@@ -769,7 +769,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
769 | 769 | ||
770 | // Remove physical files and torrents | 770 | // Remove physical files and torrents |
771 | instance.VideoFiles.forEach(file => { | 771 | instance.VideoFiles.forEach(file => { |
772 | tasks.push(instance.removeWebTorrentFileAndTorrent(file)) | 772 | tasks.push(instance.removeWebTorrentFile(file)) |
773 | }) | 773 | }) |
774 | 774 | ||
775 | // Remove playlists file | 775 | // Remove playlists file |
@@ -1783,7 +1783,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1783 | .concat(toAdd) | 1783 | .concat(toAdd) |
1784 | } | 1784 | } |
1785 | 1785 | ||
1786 | removeWebTorrentFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { | 1786 | removeWebTorrentFile (videoFile: MVideoFile, isRedundancy = false) { |
1787 | const filePath = isRedundancy | 1787 | const filePath = isRedundancy |
1788 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) | 1788 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) |
1789 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) | 1789 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) |
@@ -1829,8 +1829,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1829 | await videoFile.removeTorrent() | 1829 | await videoFile.removeTorrent() |
1830 | await remove(filePath) | 1830 | await remove(filePath) |
1831 | 1831 | ||
1832 | const resolutionFilename = getHlsResolutionPlaylistFilename(videoFile.filename) | ||
1833 | await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename)) | ||
1834 | |||
1832 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 1835 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
1833 | await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), videoFile.filename) | 1836 | await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), videoFile.filename) |
1837 | await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), resolutionFilename) | ||
1834 | } | 1838 | } |
1835 | } | 1839 | } |
1836 | 1840 | ||
diff --git a/server/tests/api/check-params/video-files.ts b/server/tests/api/check-params/video-files.ts index 8c0795092..c698bea82 100644 --- a/server/tests/api/check-params/video-files.ts +++ b/server/tests/api/check-params/video-files.ts | |||
@@ -24,6 +24,12 @@ describe('Test videos files', function () { | |||
24 | let validId1: string | 24 | let validId1: string |
25 | let validId2: string | 25 | let validId2: string |
26 | 26 | ||
27 | let hlsFileId: number | ||
28 | let webtorrentFileId: number | ||
29 | |||
30 | let remoteHLSFileId: number | ||
31 | let remoteWebtorrentFileId: number | ||
32 | |||
27 | // --------------------------------------------------------------- | 33 | // --------------------------------------------------------------- |
28 | 34 | ||
29 | before(async function () { | 35 | before(async function () { |
@@ -39,7 +45,12 @@ describe('Test videos files', function () { | |||
39 | 45 | ||
40 | { | 46 | { |
41 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | 47 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) |
42 | remoteId = uuid | 48 | await waitJobs(servers) |
49 | |||
50 | const video = await servers[1].videos.get({ id: uuid }) | ||
51 | remoteId = video.uuid | ||
52 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id | ||
53 | remoteWebtorrentFileId = video.files[0].id | ||
43 | } | 54 | } |
44 | 55 | ||
45 | { | 56 | { |
@@ -47,7 +58,12 @@ describe('Test videos files', function () { | |||
47 | 58 | ||
48 | { | 59 | { |
49 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) | 60 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) |
50 | validId1 = uuid | 61 | await waitJobs(servers) |
62 | |||
63 | const video = await servers[0].videos.get({ id: uuid }) | ||
64 | validId1 = video.uuid | ||
65 | hlsFileId = video.streamingPlaylists[0].files[0].id | ||
66 | webtorrentFileId = video.files[0].id | ||
51 | } | 67 | } |
52 | 68 | ||
53 | { | 69 | { |
@@ -76,43 +92,67 @@ describe('Test videos files', function () { | |||
76 | }) | 92 | }) |
77 | 93 | ||
78 | it('Should not delete files of a unknown video', async function () { | 94 | it('Should not delete files of a unknown video', async function () { |
79 | await servers[0].videos.removeHLSFiles({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 95 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
80 | await servers[0].videos.removeWebTorrentFiles({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 96 | |
97 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) | ||
98 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: 404, expectedStatus }) | ||
99 | |||
100 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) | ||
101 | await servers[0].videos.removeWebTorrentFile({ videoId: 404, fileId: webtorrentFileId, expectedStatus }) | ||
102 | }) | ||
103 | |||
104 | it('Should not delete unknown files', async function () { | ||
105 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | ||
106 | |||
107 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webtorrentFileId, expectedStatus }) | ||
108 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) | ||
81 | }) | 109 | }) |
82 | 110 | ||
83 | it('Should not delete files of a remote video', async function () { | 111 | it('Should not delete files of a remote video', async function () { |
84 | await servers[0].videos.removeHLSFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 112 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
85 | await servers[0].videos.removeWebTorrentFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 113 | |
114 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) | ||
115 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: remoteId, expectedStatus }) | ||
116 | |||
117 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) | ||
118 | await servers[0].videos.removeWebTorrentFile({ videoId: remoteId, fileId: remoteWebtorrentFileId, expectedStatus }) | ||
86 | }) | 119 | }) |
87 | 120 | ||
88 | it('Should not delete files by a non admin user', async function () { | 121 | it('Should not delete files by a non admin user', async function () { |
89 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | 122 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 |
90 | 123 | ||
91 | await servers[0].videos.removeHLSFiles({ videoId: validId1, token: userToken, expectedStatus }) | 124 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) |
92 | await servers[0].videos.removeHLSFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | 125 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) |
126 | |||
127 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus }) | ||
128 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | ||
93 | 129 | ||
94 | await servers[0].videos.removeWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus }) | 130 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) |
95 | await servers[0].videos.removeWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | 131 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) |
132 | |||
133 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: userToken, expectedStatus }) | ||
134 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: moderatorToken, expectedStatus }) | ||
96 | }) | 135 | }) |
97 | 136 | ||
98 | it('Should not delete files if the files are not available', async function () { | 137 | it('Should not delete files if the files are not available', async function () { |
99 | await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 138 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
100 | await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 139 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
101 | }) | ||
102 | 140 | ||
103 | it('Should not delete files if no both versions are available', async function () { | 141 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
104 | await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 142 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
105 | await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
106 | }) | 143 | }) |
107 | 144 | ||
108 | it('Should not delete files if no both versions are available', async function () { | 145 | it('Should not delete files if no both versions are available', async function () { |
109 | await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 146 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
110 | await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 147 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
111 | }) | 148 | }) |
112 | 149 | ||
113 | it('Should delete files if both versions are available', async function () { | 150 | it('Should delete files if both versions are available', async function () { |
114 | await servers[0].videos.removeHLSFiles({ videoId: validId1 }) | 151 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) |
115 | await servers[0].videos.removeWebTorrentFiles({ videoId: validId2 }) | 152 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId }) |
153 | |||
154 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) | ||
155 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId2 }) | ||
116 | }) | 156 | }) |
117 | 157 | ||
118 | after(async function () { | 158 | after(async function () { |
diff --git a/server/tests/api/transcoding/create-transcoding.ts b/server/tests/api/transcoding/create-transcoding.ts index e3867fdad..b59bef772 100644 --- a/server/tests/api/transcoding/create-transcoding.ts +++ b/server/tests/api/transcoding/create-transcoding.ts | |||
@@ -122,7 +122,7 @@ function runTests (objectStorage: boolean) { | |||
122 | it('Should generate WebTorrent from HLS only video', async function () { | 122 | it('Should generate WebTorrent from HLS only video', async function () { |
123 | this.timeout(60000) | 123 | this.timeout(60000) |
124 | 124 | ||
125 | await servers[0].videos.removeWebTorrentFiles({ videoId: videoUUID }) | 125 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: videoUUID }) |
126 | await waitJobs(servers) | 126 | await waitJobs(servers) |
127 | 127 | ||
128 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | 128 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) |
@@ -142,7 +142,7 @@ function runTests (objectStorage: boolean) { | |||
142 | it('Should only generate WebTorrent', async function () { | 142 | it('Should only generate WebTorrent', async function () { |
143 | this.timeout(60000) | 143 | this.timeout(60000) |
144 | 144 | ||
145 | await servers[0].videos.removeHLSFiles({ videoId: videoUUID }) | 145 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) |
146 | await waitJobs(servers) | 146 | await waitJobs(servers) |
147 | 147 | ||
148 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | 148 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) |
diff --git a/server/tests/api/videos/video-files.ts b/server/tests/api/videos/video-files.ts index b0ef4a2e9..313f020e9 100644 --- a/server/tests/api/videos/video-files.ts +++ b/server/tests/api/videos/video-files.ts | |||
@@ -2,10 +2,12 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { HttpStatusCode } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createMultipleServers, | 8 | createMultipleServers, |
8 | doubleFollow, | 9 | doubleFollow, |
10 | makeRawRequest, | ||
9 | PeerTubeServer, | 11 | PeerTubeServer, |
10 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
11 | waitJobs | 13 | waitJobs |
@@ -13,8 +15,6 @@ import { | |||
13 | 15 | ||
14 | describe('Test videos files', function () { | 16 | describe('Test videos files', function () { |
15 | let servers: PeerTubeServer[] | 17 | let servers: PeerTubeServer[] |
16 | let validId1: string | ||
17 | let validId2: string | ||
18 | 18 | ||
19 | // --------------------------------------------------------------- | 19 | // --------------------------------------------------------------- |
20 | 20 | ||
@@ -27,48 +27,160 @@ describe('Test videos files', function () { | |||
27 | await doubleFollow(servers[0], servers[1]) | 27 | await doubleFollow(servers[0], servers[1]) |
28 | 28 | ||
29 | await servers[0].config.enableTranscoding(true, true) | 29 | await servers[0].config.enableTranscoding(true, true) |
30 | }) | ||
30 | 31 | ||
31 | { | 32 | describe('When deleting all files', function () { |
32 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) | 33 | let validId1: string |
33 | validId1 = uuid | 34 | let validId2: string |
34 | } | ||
35 | 35 | ||
36 | { | 36 | before(async function () { |
37 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' }) | 37 | { |
38 | validId2 = uuid | 38 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) |
39 | } | 39 | validId1 = uuid |
40 | } | ||
40 | 41 | ||
41 | await waitJobs(servers) | 42 | { |
42 | }) | 43 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' }) |
44 | validId2 = uuid | ||
45 | } | ||
46 | |||
47 | await waitJobs(servers) | ||
48 | }) | ||
49 | |||
50 | it('Should delete webtorrent files', async function () { | ||
51 | this.timeout(30_000) | ||
52 | |||
53 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1 }) | ||
54 | |||
55 | await waitJobs(servers) | ||
56 | |||
57 | for (const server of servers) { | ||
58 | const video = await server.videos.get({ id: validId1 }) | ||
59 | |||
60 | expect(video.files).to.have.lengthOf(0) | ||
61 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
62 | } | ||
63 | }) | ||
43 | 64 | ||
44 | it('Should delete webtorrent files', async function () { | 65 | it('Should delete HLS files', async function () { |
45 | this.timeout(30_000) | 66 | this.timeout(30_000) |
46 | 67 | ||
47 | await servers[0].videos.removeWebTorrentFiles({ videoId: validId1 }) | 68 | await servers[0].videos.removeHLSPlaylist({ videoId: validId2 }) |
48 | 69 | ||
49 | await waitJobs(servers) | 70 | await waitJobs(servers) |
50 | 71 | ||
51 | for (const server of servers) { | 72 | for (const server of servers) { |
52 | const video = await server.videos.get({ id: validId1 }) | 73 | const video = await server.videos.get({ id: validId2 }) |
53 | 74 | ||
54 | expect(video.files).to.have.lengthOf(0) | 75 | expect(video.files).to.have.length.above(0) |
55 | expect(video.streamingPlaylists).to.have.lengthOf(1) | 76 | expect(video.streamingPlaylists).to.have.lengthOf(0) |
56 | } | 77 | } |
78 | }) | ||
57 | }) | 79 | }) |
58 | 80 | ||
59 | it('Should delete HLS files', async function () { | 81 | describe('When deleting a specific file', function () { |
60 | this.timeout(30_000) | 82 | let webtorrentId: string |
83 | let hlsId: string | ||
84 | |||
85 | before(async function () { | ||
86 | { | ||
87 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) | ||
88 | webtorrentId = uuid | ||
89 | } | ||
90 | |||
91 | { | ||
92 | const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' }) | ||
93 | hlsId = uuid | ||
94 | } | ||
95 | |||
96 | await waitJobs(servers) | ||
97 | }) | ||
98 | |||
99 | it('Shoulde delete a webtorrent file', async function () { | ||
100 | const video = await servers[0].videos.get({ id: webtorrentId }) | ||
101 | const files = video.files | ||
102 | |||
103 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: files[0].id }) | ||
104 | |||
105 | await waitJobs(servers) | ||
106 | |||
107 | for (const server of servers) { | ||
108 | const video = await server.videos.get({ id: webtorrentId }) | ||
109 | |||
110 | expect(video.files).to.have.lengthOf(files.length - 1) | ||
111 | expect(video.files.find(f => f.id === files[0].id)).to.not.exist | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | it('Should delete all webtorrent files', async function () { | ||
116 | const video = await servers[0].videos.get({ id: webtorrentId }) | ||
117 | const files = video.files | ||
118 | |||
119 | for (const file of files) { | ||
120 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: file.id }) | ||
121 | } | ||
122 | |||
123 | await waitJobs(servers) | ||
124 | |||
125 | for (const server of servers) { | ||
126 | const video = await server.videos.get({ id: webtorrentId }) | ||
127 | |||
128 | expect(video.files).to.have.lengthOf(0) | ||
129 | } | ||
130 | }) | ||
131 | |||
132 | it('Should delete a hls file', async function () { | ||
133 | const video = await servers[0].videos.get({ id: hlsId }) | ||
134 | const files = video.streamingPlaylists[0].files | ||
135 | const toDelete = files[0] | ||
136 | |||
137 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: toDelete.id }) | ||
138 | |||
139 | await waitJobs(servers) | ||
140 | |||
141 | for (const server of servers) { | ||
142 | const video = await server.videos.get({ id: hlsId }) | ||
143 | |||
144 | expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1) | ||
145 | expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist | ||
146 | |||
147 | const { text } = await makeRawRequest(video.streamingPlaylists[0].playlistUrl) | ||
148 | |||
149 | expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false | ||
150 | expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true | ||
151 | } | ||
152 | }) | ||
153 | |||
154 | it('Should delete all hls files', async function () { | ||
155 | const video = await servers[0].videos.get({ id: hlsId }) | ||
156 | const files = video.streamingPlaylists[0].files | ||
157 | |||
158 | for (const file of files) { | ||
159 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: file.id }) | ||
160 | } | ||
161 | |||
162 | await waitJobs(servers) | ||
163 | |||
164 | for (const server of servers) { | ||
165 | const video = await server.videos.get({ id: hlsId }) | ||
61 | 166 | ||
62 | await servers[0].videos.removeHLSFiles({ videoId: validId2 }) | 167 | expect(video.streamingPlaylists).to.have.lengthOf(0) |
168 | } | ||
169 | }) | ||
63 | 170 | ||
64 | await waitJobs(servers) | 171 | it('Should not delete last file of a video', async function () { |
172 | const webtorrentOnly = await servers[0].videos.get({ id: hlsId }) | ||
173 | const hlsOnly = await servers[0].videos.get({ id: webtorrentId }) | ||
65 | 174 | ||
66 | for (const server of servers) { | 175 | for (let i = 0; i < 4; i++) { |
67 | const video = await server.videos.get({ id: validId2 }) | 176 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[i].id }) |
177 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id }) | ||
178 | } | ||
68 | 179 | ||
69 | expect(video.files).to.have.length.above(0) | 180 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
70 | expect(video.streamingPlaylists).to.have.lengthOf(0) | 181 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[4].id, expectedStatus }) |
71 | } | 182 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus }) |
183 | }) | ||
72 | }) | 184 | }) |
73 | 185 | ||
74 | after(async function () { | 186 | after(async function () { |