diff options
Diffstat (limited to 'server')
61 files changed, 1083 insertions, 481 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 113c1e9db..70e8aa970 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -95,6 +95,9 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
95 | hls: { | 95 | hls: { |
96 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 96 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
97 | }, | 97 | }, |
98 | webtorrent: { | ||
99 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | ||
100 | }, | ||
98 | enabledResolutions: getEnabledResolutions() | 101 | enabledResolutions: getEnabledResolutions() |
99 | }, | 102 | }, |
100 | import: { | 103 | import: { |
@@ -304,6 +307,9 @@ function customConfig (): CustomConfig { | |||
304 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ], | 307 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ], |
305 | '2160p': CONFIG.TRANSCODING.RESOLUTIONS[ '2160p' ] | 308 | '2160p': CONFIG.TRANSCODING.RESOLUTIONS[ '2160p' ] |
306 | }, | 309 | }, |
310 | webtorrent: { | ||
311 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | ||
312 | }, | ||
307 | hls: { | 313 | hls: { |
308 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 314 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
309 | } | 315 | } |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 0d1fbc8f4..78948ff24 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -64,6 +64,8 @@ import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | |||
64 | import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding' | 64 | import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding' |
65 | import { Hooks } from '../../../lib/plugins/hooks' | 65 | import { Hooks } from '../../../lib/plugins/hooks' |
66 | import { MVideoDetails, MVideoFullLight } from '@server/typings/models' | 66 | import { MVideoDetails, MVideoFullLight } from '@server/typings/models' |
67 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | ||
68 | import { getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
67 | 69 | ||
68 | const auditLogger = auditLoggerFactory('videos') | 70 | const auditLogger = auditLoggerFactory('videos') |
69 | const videosRouter = express.Router() | 71 | const videosRouter = express.Router() |
@@ -203,7 +205,8 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
203 | 205 | ||
204 | const videoFile = new VideoFileModel({ | 206 | const videoFile = new VideoFileModel({ |
205 | extname: extname(videoPhysicalFile.filename), | 207 | extname: extname(videoPhysicalFile.filename), |
206 | size: videoPhysicalFile.size | 208 | size: videoPhysicalFile.size, |
209 | videoStreamingPlaylistId: null | ||
207 | }) | 210 | }) |
208 | 211 | ||
209 | if (videoFile.isAudio()) { | 212 | if (videoFile.isAudio()) { |
@@ -214,11 +217,10 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
214 | } | 217 | } |
215 | 218 | ||
216 | // Move physical file | 219 | // Move physical file |
217 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | 220 | const destination = getVideoFilePath(video, videoFile) |
218 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | ||
219 | await move(videoPhysicalFile.path, destination) | 221 | await move(videoPhysicalFile.path, destination) |
220 | // This is important in case if there is another attempt in the retry process | 222 | // This is important in case if there is another attempt in the retry process |
221 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) | 223 | videoPhysicalFile.filename = getVideoFilePath(video, videoFile) |
222 | videoPhysicalFile.path = destination | 224 | videoPhysicalFile.path = destination |
223 | 225 | ||
224 | // Process thumbnail or create it from the video | 226 | // Process thumbnail or create it from the video |
@@ -234,7 +236,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
234 | : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW) | 236 | : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW) |
235 | 237 | ||
236 | // Create the torrent file | 238 | // Create the torrent file |
237 | await video.createTorrentAndSetInfoHash(videoFile) | 239 | await createTorrentAndSetInfoHash(video, videoFile) |
238 | 240 | ||
239 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | 241 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { |
240 | const sequelizeOptions = { transaction: t } | 242 | const sequelizeOptions = { transaction: t } |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 0f4772310..06123518f 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -19,6 +19,9 @@ import { join } from 'path' | |||
19 | import { root } from '../helpers/core-utils' | 19 | import { root } from '../helpers/core-utils' |
20 | import { CONFIG } from '../initializers/config' | 20 | import { CONFIG } from '../initializers/config' |
21 | import { getPreview, getVideoCaption } from './lazy-static' | 21 | import { getPreview, getVideoCaption } from './lazy-static' |
22 | import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type' | ||
23 | import { MVideoFile, MVideoFullLight } from '@server/typings/models' | ||
24 | import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths' | ||
22 | 25 | ||
23 | const staticRouter = express.Router() | 26 | const staticRouter = express.Router() |
24 | 27 | ||
@@ -39,6 +42,11 @@ staticRouter.use( | |||
39 | asyncMiddleware(videosGetValidator), | 42 | asyncMiddleware(videosGetValidator), |
40 | asyncMiddleware(downloadTorrent) | 43 | asyncMiddleware(downloadTorrent) |
41 | ) | 44 | ) |
45 | staticRouter.use( | ||
46 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent', | ||
47 | asyncMiddleware(videosGetValidator), | ||
48 | asyncMiddleware(downloadHLSVideoFileTorrent) | ||
49 | ) | ||
42 | 50 | ||
43 | // Videos path for webseeding | 51 | // Videos path for webseeding |
44 | staticRouter.use( | 52 | staticRouter.use( |
@@ -58,6 +66,12 @@ staticRouter.use( | |||
58 | asyncMiddleware(downloadVideoFile) | 66 | asyncMiddleware(downloadVideoFile) |
59 | ) | 67 | ) |
60 | 68 | ||
69 | staticRouter.use( | ||
70 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+).:extension', | ||
71 | asyncMiddleware(videosGetValidator), | ||
72 | asyncMiddleware(downloadHLSVideoFile) | ||
73 | ) | ||
74 | |||
61 | // HLS | 75 | // HLS |
62 | staticRouter.use( | 76 | staticRouter.use( |
63 | STATIC_PATHS.STREAMING_PLAYLISTS.HLS, | 77 | STATIC_PATHS.STREAMING_PLAYLISTS.HLS, |
@@ -227,24 +241,55 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
227 | } | 241 | } |
228 | 242 | ||
229 | async function downloadTorrent (req: express.Request, res: express.Response) { | 243 | async function downloadTorrent (req: express.Request, res: express.Response) { |
230 | const { video, videoFile } = getVideoAndFile(req, res) | 244 | const video = res.locals.videoAll |
245 | |||
246 | const videoFile = getVideoFile(req, video.VideoFiles) | ||
247 | if (!videoFile) return res.status(404).end() | ||
248 | |||
249 | return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`) | ||
250 | } | ||
251 | |||
252 | async function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) { | ||
253 | const video = res.locals.videoAll | ||
254 | |||
255 | const playlist = getHLSPlaylist(video) | ||
256 | if (!playlist) return res.status(404).end | ||
257 | |||
258 | const videoFile = getVideoFile(req, playlist.VideoFiles) | ||
231 | if (!videoFile) return res.status(404).end() | 259 | if (!videoFile) return res.status(404).end() |
232 | 260 | ||
233 | return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`) | 261 | return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`) |
234 | } | 262 | } |
235 | 263 | ||
236 | async function downloadVideoFile (req: express.Request, res: express.Response) { | 264 | async function downloadVideoFile (req: express.Request, res: express.Response) { |
237 | const { video, videoFile } = getVideoAndFile(req, res) | 265 | const video = res.locals.videoAll |
266 | |||
267 | const videoFile = getVideoFile(req, video.VideoFiles) | ||
238 | if (!videoFile) return res.status(404).end() | 268 | if (!videoFile) return res.status(404).end() |
239 | 269 | ||
240 | return res.download(video.getVideoFilePath(videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) | 270 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) |
241 | } | 271 | } |
242 | 272 | ||
243 | function getVideoAndFile (req: express.Request, res: express.Response) { | 273 | async function downloadHLSVideoFile (req: express.Request, res: express.Response) { |
244 | const resolution = parseInt(req.params.resolution, 10) | ||
245 | const video = res.locals.videoAll | 274 | const video = res.locals.videoAll |
275 | const playlist = getHLSPlaylist(video) | ||
276 | if (!playlist) return res.status(404).end | ||
277 | |||
278 | const videoFile = getVideoFile(req, playlist.VideoFiles) | ||
279 | if (!videoFile) return res.status(404).end() | ||
280 | |||
281 | const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` | ||
282 | return res.download(getVideoFilePath(playlist, videoFile), filename) | ||
283 | } | ||
284 | |||
285 | function getVideoFile (req: express.Request, files: MVideoFile[]) { | ||
286 | const resolution = parseInt(req.params.resolution, 10) | ||
287 | return files.find(f => f.resolution === resolution) | ||
288 | } | ||
246 | 289 | ||
247 | const videoFile = video.VideoFiles.find(f => f.resolution === resolution) | 290 | function getHLSPlaylist (video: MVideoFullLight) { |
291 | const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
292 | if (!playlist) return undefined | ||
248 | 293 | ||
249 | return { video, videoFile } | 294 | return Object.assign(playlist, { Video: video }) |
250 | } | 295 | } |
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 02f914326..a28bebf2d 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | } from '../videos' | 12 | } from '../videos' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
14 | import { VideoState } from '../../../../shared/models/videos' | 14 | import { VideoState } from '../../../../shared/models/videos' |
15 | import { logger } from '@server/helpers/logger' | ||
15 | 16 | ||
16 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 17 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
17 | return isBaseActivityValid(activity, 'Update') && | 18 | return isBaseActivityValid(activity, 'Update') && |
@@ -30,11 +31,26 @@ function isActivityPubVideoDurationValid (value: string) { | |||
30 | function sanitizeAndCheckVideoTorrentObject (video: any) { | 31 | function sanitizeAndCheckVideoTorrentObject (video: any) { |
31 | if (!video || video.type !== 'Video') return false | 32 | if (!video || video.type !== 'Video') return false |
32 | 33 | ||
33 | if (!setValidRemoteTags(video)) return false | 34 | if (!setValidRemoteTags(video)) { |
34 | if (!setValidRemoteVideoUrls(video)) return false | 35 | logger.debug('Video has invalid tags', { video }) |
35 | if (!setRemoteVideoTruncatedContent(video)) return false | 36 | return false |
36 | if (!setValidAttributedTo(video)) return false | 37 | } |
37 | if (!setValidRemoteCaptions(video)) return false | 38 | if (!setValidRemoteVideoUrls(video)) { |
39 | logger.debug('Video has invalid urls', { video }) | ||
40 | return false | ||
41 | } | ||
42 | if (!setRemoteVideoTruncatedContent(video)) { | ||
43 | logger.debug('Video has invalid content', { video }) | ||
44 | return false | ||
45 | } | ||
46 | if (!setValidAttributedTo(video)) { | ||
47 | logger.debug('Video has invalid attributedTo', { video }) | ||
48 | return false | ||
49 | } | ||
50 | if (!setValidRemoteCaptions(video)) { | ||
51 | logger.debug('Video has invalid captions', { video }) | ||
52 | return false | ||
53 | } | ||
38 | 54 | ||
39 | // Default attributes | 55 | // Default attributes |
40 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED | 56 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED |
@@ -62,25 +78,21 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
62 | } | 78 | } |
63 | 79 | ||
64 | function isRemoteVideoUrlValid (url: any) { | 80 | function isRemoteVideoUrlValid (url: any) { |
65 | // FIXME: Old bug, we used the width to represent the resolution. Remove it in a few release (currently beta.11) | ||
66 | if (url.width && !url.height) url.height = url.width | ||
67 | |||
68 | return url.type === 'Link' && | 81 | return url.type === 'Link' && |
69 | ( | 82 | ( |
70 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) | 83 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType) !== -1 && |
71 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 && | ||
72 | isActivityPubUrlValid(url.href) && | 84 | isActivityPubUrlValid(url.href) && |
73 | validator.isInt(url.height + '', { min: 0 }) && | 85 | validator.isInt(url.height + '', { min: 0 }) && |
74 | validator.isInt(url.size + '', { min: 0 }) && | 86 | validator.isInt(url.size + '', { min: 0 }) && |
75 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) | 87 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) |
76 | ) || | 88 | ) || |
77 | ( | 89 | ( |
78 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 && | 90 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType) !== -1 && |
79 | isActivityPubUrlValid(url.href) && | 91 | isActivityPubUrlValid(url.href) && |
80 | validator.isInt(url.height + '', { min: 0 }) | 92 | validator.isInt(url.height + '', { min: 0 }) |
81 | ) || | 93 | ) || |
82 | ( | 94 | ( |
83 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && | 95 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType) !== -1 && |
84 | validator.isLength(url.href, { min: 5 }) && | 96 | validator.isLength(url.href, { min: 5 }) && |
85 | validator.isInt(url.height + '', { min: 0 }) | 97 | validator.isInt(url.height + '', { min: 0 }) |
86 | ) || | 98 | ) || |
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index 6c5068fb0..87f10f913 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts | |||
@@ -79,6 +79,15 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) { | |||
79 | return fn() | 79 | return fn() |
80 | } | 80 | } |
81 | 81 | ||
82 | function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> ( | ||
83 | fromDatabase: T[], | ||
84 | newModels: T[], | ||
85 | t: Transaction | ||
86 | ) { | ||
87 | return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f))) | ||
88 | .map(f => f.destroy({ transaction: t })) | ||
89 | } | ||
90 | |||
82 | // --------------------------------------------------------------------------- | 91 | // --------------------------------------------------------------------------- |
83 | 92 | ||
84 | export { | 93 | export { |
@@ -86,5 +95,6 @@ export { | |||
86 | retryTransactionWrapper, | 95 | retryTransactionWrapper, |
87 | transactionRetryer, | 96 | transactionRetryer, |
88 | updateInstanceWithAnother, | 97 | updateInstanceWithAnother, |
89 | afterCommitIfTransaction | 98 | afterCommitIfTransaction, |
99 | deleteNonExistingModels | ||
90 | } | 100 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index c0e9702a8..7a4ac0970 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -130,6 +130,7 @@ interface BaseTranscodeOptions { | |||
130 | 130 | ||
131 | interface HLSTranscodeOptions extends BaseTranscodeOptions { | 131 | interface HLSTranscodeOptions extends BaseTranscodeOptions { |
132 | type: 'hls' | 132 | type: 'hls' |
133 | copyCodecs: boolean | ||
133 | hlsPlaylist: { | 134 | hlsPlaylist: { |
134 | videoFilename: string | 135 | videoFilename: string |
135 | } | 136 | } |
@@ -232,7 +233,7 @@ export { | |||
232 | 233 | ||
233 | // --------------------------------------------------------------------------- | 234 | // --------------------------------------------------------------------------- |
234 | 235 | ||
235 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) { | 236 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { |
236 | let fps = await getVideoFileFPS(options.inputPath) | 237 | let fps = await getVideoFileFPS(options.inputPath) |
237 | // On small/medium resolutions, limit FPS | 238 | // On small/medium resolutions, limit FPS |
238 | if ( | 239 | if ( |
@@ -287,7 +288,8 @@ async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | |||
287 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { | 288 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { |
288 | const videoPath = getHLSVideoPath(options) | 289 | const videoPath = getHLSVideoPath(options) |
289 | 290 | ||
290 | command = await presetCopy(command) | 291 | if (options.copyCodecs) command = await presetCopy(command) |
292 | else command = await buildx264Command(command, options) | ||
291 | 293 | ||
292 | command = command.outputOption('-hls_time 4') | 294 | command = command.outputOption('-hls_time 4') |
293 | .outputOption('-hls_list_size 0') | 295 | .outputOption('-hls_list_size 0') |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index d066e2b1f..5b9c026b1 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -45,10 +45,6 @@ function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird | |||
45 | if (fetchType === 'only-video') return VideoModel.loadByUrl(url) | 45 | if (fetchType === 'only-video') return VideoModel.loadByUrl(url) |
46 | } | 46 | } |
47 | 47 | ||
48 | function getVideo (res: Response) { | ||
49 | return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights || res.locals.videoId | ||
50 | } | ||
51 | |||
52 | function getVideoWithAttributes (res: Response) { | 48 | function getVideoWithAttributes (res: Response) { |
53 | return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights | 49 | return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights |
54 | } | 50 | } |
@@ -57,7 +53,6 @@ export { | |||
57 | VideoFetchType, | 53 | VideoFetchType, |
58 | VideoFetchByUrlType, | 54 | VideoFetchByUrlType, |
59 | fetchVideo, | 55 | fetchVideo, |
60 | getVideo, | ||
61 | getVideoWithAttributes, | 56 | getVideoWithAttributes, |
62 | fetchVideoByUrl | 57 | fetchVideoByUrl |
63 | } | 58 | } |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index d2a22e8f0..f3e41f8d6 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -1,11 +1,22 @@ | |||
1 | import { logger } from './logger' | 1 | import { logger } from './logger' |
2 | import { generateVideoImportTmpPath } from './utils' | 2 | import { generateVideoImportTmpPath } from './utils' |
3 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' | 4 | import { createWriteStream, ensureDir, remove, writeFile } from 'fs-extra' |
5 | import { CONFIG } from '../initializers/config' | 5 | import { CONFIG } from '../initializers/config' |
6 | import { dirname, join } from 'path' | 6 | import { dirname, join } from 'path' |
7 | import * as createTorrent from 'create-torrent' | 7 | import * as createTorrent from 'create-torrent' |
8 | import { promisify2 } from './core-utils' | 8 | import { promisify2 } from './core-utils' |
9 | import { MVideo } from '@server/typings/models/video/video' | ||
10 | import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file' | ||
11 | import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist' | ||
12 | import { STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' | ||
13 | import * as parseTorrent from 'parse-torrent' | ||
14 | import * as magnetUtil from 'magnet-uri' | ||
15 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
16 | import { extractVideo } from '@server/lib/videos' | ||
17 | import { getTorrentFileName, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
18 | |||
19 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) | ||
9 | 20 | ||
10 | async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) { | 21 | async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) { |
11 | const id = target.magnetUri || target.torrentName | 22 | const id = target.magnetUri || target.torrentName |
@@ -59,12 +70,64 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName | |||
59 | }) | 70 | }) |
60 | } | 71 | } |
61 | 72 | ||
62 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) | 73 | async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { |
74 | const video = extractVideo(videoOrPlaylist) | ||
75 | |||
76 | const options = { | ||
77 | // Keep the extname, it's used by the client to stream the file inside a web browser | ||
78 | name: `${video.name} ${videoFile.resolution}p${videoFile.extname}`, | ||
79 | createdBy: 'PeerTube', | ||
80 | announceList: [ | ||
81 | [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ], | ||
82 | [ WEBSERVER.URL + '/tracker/announce' ] | ||
83 | ], | ||
84 | urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + getVideoFilename(videoOrPlaylist, videoFile) ] | ||
85 | } | ||
86 | |||
87 | const torrent = await createTorrentPromise(getVideoFilePath(videoOrPlaylist, videoFile), options) | ||
88 | |||
89 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile)) | ||
90 | logger.info('Creating torrent %s.', filePath) | ||
91 | |||
92 | await writeFile(filePath, torrent) | ||
93 | |||
94 | const parsedTorrent = parseTorrent(torrent) | ||
95 | videoFile.infoHash = parsedTorrent.infoHash | ||
96 | } | ||
97 | |||
98 | function generateMagnetUri ( | ||
99 | videoOrPlaylist: MVideo | MStreamingPlaylistVideo, | ||
100 | videoFile: MVideoFileRedundanciesOpt, | ||
101 | baseUrlHttp: string, | ||
102 | baseUrlWs: string | ||
103 | ) { | ||
104 | const video = isStreamingPlaylist(videoOrPlaylist) | ||
105 | ? videoOrPlaylist.Video | ||
106 | : videoOrPlaylist | ||
107 | |||
108 | const xs = videoOrPlaylist.getTorrentUrl(videoFile, baseUrlHttp) | ||
109 | const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs) | ||
110 | let urlList = [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ] | ||
111 | |||
112 | const redundancies = videoFile.RedundancyVideos | ||
113 | if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) | ||
114 | |||
115 | const magnetHash = { | ||
116 | xs, | ||
117 | announce, | ||
118 | urlList, | ||
119 | infoHash: videoFile.infoHash, | ||
120 | name: video.name | ||
121 | } | ||
122 | |||
123 | return magnetUtil.encode(magnetHash) | ||
124 | } | ||
63 | 125 | ||
64 | // --------------------------------------------------------------------------- | 126 | // --------------------------------------------------------------------------- |
65 | 127 | ||
66 | export { | 128 | export { |
67 | createTorrentPromise, | 129 | createTorrentAndSetInfoHash, |
130 | generateMagnetUri, | ||
68 | downloadWebTorrentVideo | 131 | downloadWebTorrentVideo |
69 | } | 132 | } |
70 | 133 | ||
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index db3115085..9fefba769 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -101,6 +101,13 @@ function checkConfig () { | |||
101 | } | 101 | } |
102 | } | 102 | } |
103 | 103 | ||
104 | // Transcoding | ||
105 | if (CONFIG.TRANSCODING.ENABLED) { | ||
106 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { | ||
107 | return 'You need to enable at least WebTorrent transcoding or HLS transcoding.' | ||
108 | } | ||
109 | } | ||
110 | |||
104 | return null | 111 | return null |
105 | } | 112 | } |
106 | 113 | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 164d714d6..6d5d55487 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -177,6 +177,9 @@ const CONFIG = { | |||
177 | }, | 177 | }, |
178 | HLS: { | 178 | HLS: { |
179 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } | 179 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } |
180 | }, | ||
181 | WEBTORRENT: { | ||
182 | get ENABLED () { return config.get<boolean>('transcoding.webtorrent.enabled') } | ||
180 | } | 183 | } |
181 | }, | 184 | }, |
182 | IMPORT: { | 185 | IMPORT: { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index fd4c0fdaa..eaad84bee 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 445 | 17 | const LAST_MIGRATION_VERSION = 450 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
@@ -505,7 +505,8 @@ const STATIC_PATHS = { | |||
505 | } | 505 | } |
506 | const STATIC_DOWNLOAD_PATHS = { | 506 | const STATIC_DOWNLOAD_PATHS = { |
507 | TORRENTS: '/download/torrents/', | 507 | TORRENTS: '/download/torrents/', |
508 | VIDEOS: '/download/videos/' | 508 | VIDEOS: '/download/videos/', |
509 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' | ||
509 | } | 510 | } |
510 | const LAZY_STATIC_PATHS = { | 511 | const LAZY_STATIC_PATHS = { |
511 | AVATARS: '/lazy-static/avatars/', | 512 | AVATARS: '/lazy-static/avatars/', |
diff --git a/server/initializers/migrations/0065-video-file-size.ts b/server/initializers/migrations/0065-video-file-size.ts index 66f25016a..e9ce77e50 100644 --- a/server/initializers/migrations/0065-video-file-size.ts +++ b/server/initializers/migrations/0065-video-file-size.ts | |||
@@ -2,6 +2,7 @@ import * as Sequelize from 'sequelize' | |||
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | import { stat } from 'fs-extra' | 3 | import { stat } from 'fs-extra' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { getVideoFilePath } from '@server/lib/video-paths' | ||
5 | 6 | ||
6 | function up (utils: { | 7 | function up (utils: { |
7 | transaction: Sequelize.Transaction, | 8 | transaction: Sequelize.Transaction, |
@@ -16,7 +17,7 @@ function up (utils: { | |||
16 | videos.forEach(video => { | 17 | videos.forEach(video => { |
17 | video.VideoFiles.forEach(videoFile => { | 18 | video.VideoFiles.forEach(videoFile => { |
18 | const p = new Promise((res, rej) => { | 19 | const p = new Promise((res, rej) => { |
19 | stat(video.getVideoFilePath(videoFile), (err, stats) => { | 20 | stat(getVideoFilePath(video, videoFile), (err, stats) => { |
20 | if (err) return rej(err) | 21 | if (err) return rej(err) |
21 | 22 | ||
22 | videoFile.size = stats.size | 23 | videoFile.size = stats.size |
diff --git a/server/initializers/migrations/0450-streaming-playlist-files.ts b/server/initializers/migrations/0450-streaming-playlist-files.ts new file mode 100644 index 000000000..536ef00f9 --- /dev/null +++ b/server/initializers/migrations/0450-streaming-playlist-files.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.INTEGER, | ||
12 | allowNull: true, | ||
13 | references: { | ||
14 | model: 'videoStreamingPlaylist', | ||
15 | key: 'id' | ||
16 | }, | ||
17 | onDelete: 'CASCADE' | ||
18 | } | ||
19 | |||
20 | await utils.queryInterface.addColumn('videoFile', 'videoStreamingPlaylistId', data) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | const data = { | ||
25 | type: Sequelize.INTEGER, | ||
26 | allowNull: true | ||
27 | } | ||
28 | |||
29 | await utils.queryInterface.changeColumn('videoFile', 'videoId', data) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | function down (options) { | ||
34 | throw new Error('Not implemented.') | ||
35 | } | ||
36 | |||
37 | export { | ||
38 | up, | ||
39 | down | ||
40 | } | ||
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index c318978fd..d80173e03 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -3,8 +3,10 @@ import * as sequelize from 'sequelize' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as request from 'request' | 4 | import * as request from 'request' |
5 | import { | 5 | import { |
6 | ActivityHashTagObject, | ||
7 | ActivityMagnetUrlObject, | ||
6 | ActivityPlaylistSegmentHashesObject, | 8 | ActivityPlaylistSegmentHashesObject, |
7 | ActivityPlaylistUrlObject, | 9 | ActivityPlaylistUrlObject, ActivityTagObject, |
8 | ActivityUrlObject, | 10 | ActivityUrlObject, |
9 | ActivityVideoUrlObject, | 11 | ActivityVideoUrlObject, |
10 | VideoState | 12 | VideoState |
@@ -13,7 +15,7 @@ import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | |||
13 | import { VideoPrivacy } from '../../../shared/models/videos' | 15 | import { VideoPrivacy } from '../../../shared/models/videos' |
14 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 16 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
15 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 17 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 18 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
17 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
18 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 20 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
19 | import { | 21 | import { |
@@ -57,6 +59,7 @@ import { | |||
57 | MChannelAccountLight, | 59 | MChannelAccountLight, |
58 | MChannelDefault, | 60 | MChannelDefault, |
59 | MChannelId, | 61 | MChannelId, |
62 | MStreamingPlaylist, | ||
60 | MVideo, | 63 | MVideo, |
61 | MVideoAccountLight, | 64 | MVideoAccountLight, |
62 | MVideoAccountLightBlacklistAllFiles, | 65 | MVideoAccountLightBlacklistAllFiles, |
@@ -330,21 +333,15 @@ async function updateVideoFromAP (options: { | |||
330 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | 333 | await videoUpdated.addAndSaveThumbnail(previewModel, t) |
331 | 334 | ||
332 | { | 335 | { |
333 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject) | 336 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) |
334 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 337 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
335 | 338 | ||
336 | // Remove video files that do not exist anymore | 339 | // Remove video files that do not exist anymore |
337 | const destroyTasks = videoUpdated.VideoFiles | 340 | const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t) |
338 | .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f))) | ||
339 | .map(f => f.destroy(sequelizeOptions)) | ||
340 | await Promise.all(destroyTasks) | 341 | await Promise.all(destroyTasks) |
341 | 342 | ||
342 | // Update or add other one | 343 | // Update or add other one |
343 | const upsertTasks = videoFileAttributes.map(a => { | 344 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) |
344 | return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t }) | ||
345 | .then(([ file ]) => file) | ||
346 | }) | ||
347 | |||
348 | videoUpdated.VideoFiles = await Promise.all(upsertTasks) | 345 | videoUpdated.VideoFiles = await Promise.all(upsertTasks) |
349 | } | 346 | } |
350 | 347 | ||
@@ -352,24 +349,39 @@ async function updateVideoFromAP (options: { | |||
352 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles) | 349 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles) |
353 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | 350 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) |
354 | 351 | ||
355 | // Remove video files that do not exist anymore | 352 | // Remove video playlists that do not exist anymore |
356 | const destroyTasks = videoUpdated.VideoStreamingPlaylists | 353 | const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t) |
357 | .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) | ||
358 | .map(f => f.destroy(sequelizeOptions)) | ||
359 | await Promise.all(destroyTasks) | 354 | await Promise.all(destroyTasks) |
360 | 355 | ||
361 | // Update or add other one | 356 | let oldStreamingPlaylistFiles: MVideoFile[] = [] |
362 | const upsertTasks = streamingPlaylistAttributes.map(a => { | 357 | for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) { |
363 | return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) | 358 | oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles) |
364 | .then(([ streamingPlaylist ]) => streamingPlaylist) | 359 | } |
365 | }) | 360 | |
361 | videoUpdated.VideoStreamingPlaylists = [] | ||
362 | |||
363 | for (const playlistAttributes of streamingPlaylistAttributes) { | ||
364 | const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t }) | ||
365 | .then(([ streamingPlaylist ]) => streamingPlaylist) | ||
366 | 366 | ||
367 | videoUpdated.VideoStreamingPlaylists = await Promise.all(upsertTasks) | 367 | const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject) |
368 | .map(a => new VideoFileModel(a)) | ||
369 | const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t) | ||
370 | await Promise.all(destroyTasks) | ||
371 | |||
372 | // Update or add other one | ||
373 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) | ||
374 | streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks) | ||
375 | |||
376 | videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel) | ||
377 | } | ||
368 | } | 378 | } |
369 | 379 | ||
370 | { | 380 | { |
371 | // Update Tags | 381 | // Update Tags |
372 | const tags = videoObject.tag.map(tag => tag.name) | 382 | const tags = videoObject.tag |
383 | .filter(isAPHashTagObject) | ||
384 | .map(tag => tag.name) | ||
373 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 385 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
374 | await videoUpdated.$set('Tags', tagInstances, sequelizeOptions) | 386 | await videoUpdated.$set('Tags', tagInstances, sequelizeOptions) |
375 | } | 387 | } |
@@ -478,23 +490,27 @@ export { | |||
478 | 490 | ||
479 | // --------------------------------------------------------------------------- | 491 | // --------------------------------------------------------------------------- |
480 | 492 | ||
481 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 493 | function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject { |
482 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | 494 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
483 | 495 | ||
484 | const urlMediaType = url.mediaType || url.mimeType | 496 | const urlMediaType = url.mediaType |
485 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | 497 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') |
486 | } | 498 | } |
487 | 499 | ||
488 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | 500 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { |
489 | const urlMediaType = url.mediaType || url.mimeType | 501 | return url && url.mediaType === 'application/x-mpegURL' |
490 | |||
491 | return urlMediaType === 'application/x-mpegURL' | ||
492 | } | 502 | } |
493 | 503 | ||
494 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | 504 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { |
495 | const urlMediaType = tag.mediaType || tag.mimeType | 505 | return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json' |
506 | } | ||
507 | |||
508 | function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject { | ||
509 | return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' | ||
510 | } | ||
496 | 511 | ||
497 | return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' | 512 | function isAPHashTagObject (url: any): url is ActivityHashTagObject { |
513 | return url && url.type === 'Hashtag' | ||
498 | } | 514 | } |
499 | 515 | ||
500 | async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) { | 516 | async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) { |
@@ -524,21 +540,27 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
524 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 540 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) |
525 | 541 | ||
526 | // Process files | 542 | // Process files |
527 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) | 543 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) |
528 | if (videoFileAttributes.length === 0) { | ||
529 | throw new Error('Cannot find valid files for video %s ' + videoObject.url) | ||
530 | } | ||
531 | 544 | ||
532 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 545 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
533 | const videoFiles = await Promise.all(videoFilePromises) | 546 | const videoFiles = await Promise.all(videoFilePromises) |
534 | 547 | ||
535 | const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles) | 548 | const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles) |
536 | const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) | 549 | videoCreated.VideoStreamingPlaylists = [] |
537 | const streamingPlaylists = await Promise.all(playlistPromises) | 550 | |
551 | for (const playlistAttributes of streamingPlaylistsAttributes) { | ||
552 | const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) | ||
553 | |||
554 | const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject) | ||
555 | const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t })) | ||
556 | playlistModel.VideoFiles = await Promise.all(videoFilePromises) | ||
557 | |||
558 | videoCreated.VideoStreamingPlaylists.push(playlistModel) | ||
559 | } | ||
538 | 560 | ||
539 | // Process tags | 561 | // Process tags |
540 | const tags = videoObject.tag | 562 | const tags = videoObject.tag |
541 | .filter(t => t.type === 'Hashtag') | 563 | .filter(isAPHashTagObject) |
542 | .map(t => t.name) | 564 | .map(t => t.name) |
543 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 565 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
544 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 566 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
@@ -550,7 +572,6 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
550 | await Promise.all(videoCaptionsPromises) | 572 | await Promise.all(videoCaptionsPromises) |
551 | 573 | ||
552 | videoCreated.VideoFiles = videoFiles | 574 | videoCreated.VideoFiles = videoFiles |
553 | videoCreated.VideoStreamingPlaylists = streamingPlaylists | ||
554 | videoCreated.Tags = tagInstances | 575 | videoCreated.Tags = tagInstances |
555 | 576 | ||
556 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | 577 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ |
@@ -628,20 +649,19 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide | |||
628 | } | 649 | } |
629 | } | 650 | } |
630 | 651 | ||
631 | function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) { | 652 | function videoFileActivityUrlToDBAttributes ( |
632 | const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] | 653 | videoOrPlaylist: MVideo | MStreamingPlaylist, |
654 | urls: (ActivityTagObject | ActivityUrlObject)[] | ||
655 | ) { | ||
656 | const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] | ||
633 | 657 | ||
634 | if (fileUrls.length === 0) { | 658 | if (fileUrls.length === 0) return [] |
635 | throw new Error('Cannot find video files for ' + video.url) | ||
636 | } | ||
637 | 659 | ||
638 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] | 660 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] |
639 | for (const fileUrl of fileUrls) { | 661 | for (const fileUrl of fileUrls) { |
640 | // Fetch associated magnet uri | 662 | // Fetch associated magnet uri |
641 | const magnet = videoObject.url.find(u => { | 663 | const magnet = urls.filter(isAPMagnetUrlObject) |
642 | const mediaType = u.mediaType || u.mimeType | 664 | .find(u => u.height === fileUrl.height) |
643 | return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height | ||
644 | }) | ||
645 | 665 | ||
646 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | 666 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) |
647 | 667 | ||
@@ -650,14 +670,17 @@ function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTo | |||
650 | throw new Error('Cannot parse magnet URI ' + magnet.href) | 670 | throw new Error('Cannot parse magnet URI ' + magnet.href) |
651 | } | 671 | } |
652 | 672 | ||
653 | const mediaType = fileUrl.mediaType || fileUrl.mimeType | 673 | const mediaType = fileUrl.mediaType |
654 | const attribute = { | 674 | const attribute = { |
655 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], | 675 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], |
656 | infoHash: parsed.infoHash, | 676 | infoHash: parsed.infoHash, |
657 | resolution: fileUrl.height, | 677 | resolution: fileUrl.height, |
658 | size: fileUrl.size, | 678 | size: fileUrl.size, |
659 | videoId: video.id, | 679 | fps: fileUrl.fps || -1, |
660 | fps: fileUrl.fps || -1 | 680 | |
681 | // This is a video file owned by a video or by a streaming playlist | ||
682 | videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id, | ||
683 | videoStreamingPlaylistId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null | ||
661 | } | 684 | } |
662 | 685 | ||
663 | attributes.push(attribute) | 686 | attributes.push(attribute) |
@@ -670,12 +693,15 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec | |||
670 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | 693 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] |
671 | if (playlistUrls.length === 0) return [] | 694 | if (playlistUrls.length === 0) return [] |
672 | 695 | ||
673 | const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] | 696 | const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = [] |
674 | for (const playlistUrlObject of playlistUrls) { | 697 | for (const playlistUrlObject of playlistUrls) { |
675 | const segmentsSha256UrlObject = playlistUrlObject.tag | 698 | const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject) |
676 | .find(t => { | 699 | |
677 | return isAPPlaylistSegmentHashesUrlObject(t) | 700 | let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] |
678 | }) as ActivityPlaylistSegmentHashesObject | 701 | |
702 | // FIXME: backward compatibility introduced in v2.1.0 | ||
703 | if (files.length === 0) files = videoFiles | ||
704 | |||
679 | if (!segmentsSha256UrlObject) { | 705 | if (!segmentsSha256UrlObject) { |
680 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | 706 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) |
681 | continue | 707 | continue |
@@ -685,9 +711,10 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec | |||
685 | type: VideoStreamingPlaylistType.HLS, | 711 | type: VideoStreamingPlaylistType.HLS, |
686 | playlistUrl: playlistUrlObject.href, | 712 | playlistUrl: playlistUrlObject.href, |
687 | segmentsSha256Url: segmentsSha256UrlObject.href, | 713 | segmentsSha256Url: segmentsSha256UrlObject.href, |
688 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles), | 714 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), |
689 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | 715 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, |
690 | videoId: video.id | 716 | videoId: video.id, |
717 | tagAPObject: playlistUrlObject.tag | ||
691 | } | 718 | } |
692 | 719 | ||
693 | attributes.push(attribute) | 720 | attributes.push(attribute) |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 05136c21c..943721dd7 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -12,6 +12,7 @@ import { VideoFileModel } from '../models/video/video-file' | |||
12 | import { CONFIG } from '../initializers/config' | 12 | import { CONFIG } from '../initializers/config' |
13 | import { sequelizeTypescript } from '../initializers/database' | 13 | import { sequelizeTypescript } from '../initializers/database' |
14 | import { MVideoWithFile } from '@server/typings/models' | 14 | import { MVideoWithFile } from '@server/typings/models' |
15 | import { getVideoFilename, getVideoFilePath } from './video-paths' | ||
15 | 16 | ||
16 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 17 | async function updateStreamingPlaylistsInfohashesIfNeeded () { |
17 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() | 18 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() |
@@ -32,13 +33,14 @@ async function updateMasterHLSPlaylist (video: MVideoWithFile) { | |||
32 | const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 33 | const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
33 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | 34 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] |
34 | const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | 35 | const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) |
36 | const streamingPlaylist = video.getHLSPlaylist() | ||
35 | 37 | ||
36 | for (const file of video.VideoFiles) { | 38 | for (const file of streamingPlaylist.VideoFiles) { |
37 | // If we did not generated a playlist for this resolution, skip | 39 | // If we did not generated a playlist for this resolution, skip |
38 | const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | 40 | const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) |
39 | if (await pathExists(filePlaylistPath) === false) continue | 41 | if (await pathExists(filePlaylistPath) === false) continue |
40 | 42 | ||
41 | const videoFilePath = video.getVideoFilePath(file) | 43 | const videoFilePath = getVideoFilePath(streamingPlaylist, file) |
42 | 44 | ||
43 | const size = await getVideoFileSize(videoFilePath) | 45 | const size = await getVideoFileSize(videoFilePath) |
44 | 46 | ||
@@ -59,12 +61,13 @@ async function updateSha256Segments (video: MVideoWithFile) { | |||
59 | const json: { [filename: string]: { [range: string]: string } } = {} | 61 | const json: { [filename: string]: { [range: string]: string } } = {} |
60 | 62 | ||
61 | const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 63 | const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
64 | const hlsPlaylist = video.getHLSPlaylist() | ||
62 | 65 | ||
63 | // For all the resolutions available for this video | 66 | // For all the resolutions available for this video |
64 | for (const file of video.VideoFiles) { | 67 | for (const file of hlsPlaylist.VideoFiles) { |
65 | const rangeHashes: { [range: string]: string } = {} | 68 | const rangeHashes: { [range: string]: string } = {} |
66 | 69 | ||
67 | const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)) | 70 | const videoPath = getVideoFilePath(hlsPlaylist, file) |
68 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | 71 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) |
69 | 72 | ||
70 | // Maybe the playlist is not generated for this resolution yet | 73 | // Maybe the playlist is not generated for this resolution yet |
@@ -82,7 +85,7 @@ async function updateSha256Segments (video: MVideoWithFile) { | |||
82 | } | 85 | } |
83 | await close(fd) | 86 | await close(fd) |
84 | 87 | ||
85 | const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution) | 88 | const videoFilename = getVideoFilename(hlsPlaylist, file) |
86 | json[videoFilename] = rangeHashes | 89 | json[videoFilename] = rangeHashes |
87 | } | 90 | } |
88 | 91 | ||
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 5c5b7dccb..99c991e72 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -7,6 +7,8 @@ import { copy, stat } from 'fs-extra' | |||
7 | import { VideoFileModel } from '../../../models/video/video-file' | 7 | import { VideoFileModel } from '../../../models/video/video-file' |
8 | import { extname } from 'path' | 8 | import { extname } from 'path' |
9 | import { MVideoFile, MVideoWithFile } from '@server/typings/models' | 9 | import { MVideoFile, MVideoWithFile } from '@server/typings/models' |
10 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | ||
11 | import { getVideoFilePath } from '@server/lib/video-paths' | ||
10 | 12 | ||
11 | export type VideoFileImportPayload = { | 13 | export type VideoFileImportPayload = { |
12 | videoUUID: string, | 14 | videoUUID: string, |
@@ -68,10 +70,10 @@ async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) { | |||
68 | updatedVideoFile = currentVideoFile | 70 | updatedVideoFile = currentVideoFile |
69 | } | 71 | } |
70 | 72 | ||
71 | const outputPath = video.getVideoFilePath(updatedVideoFile) | 73 | const outputPath = getVideoFilePath(video, updatedVideoFile) |
72 | await copy(inputFilePath, outputPath) | 74 | await copy(inputFilePath, outputPath) |
73 | 75 | ||
74 | await video.createTorrentAndSetInfoHash(updatedVideoFile) | 76 | await createTorrentAndSetInfoHash(video, updatedVideoFile) |
75 | 77 | ||
76 | await updatedVideoFile.save() | 78 | await updatedVideoFile.save() |
77 | 79 | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 93a3e9d90..1fca17584 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -4,14 +4,14 @@ import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl' | |||
4 | import { VideoImportModel } from '../../../models/video/video-import' | 4 | import { VideoImportModel } from '../../../models/video/video-import' |
5 | import { VideoImportState } from '../../../../shared/models/videos' | 5 | import { VideoImportState } from '../../../../shared/models/videos' |
6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
7 | import { extname, join } from 'path' | 7 | import { extname } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' | 9 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' |
10 | import { VideoState } from '../../../../shared' | 10 | import { VideoState } from '../../../../shared' |
11 | import { JobQueue } from '../index' | 11 | import { JobQueue } from '../index' |
12 | import { federateVideoIfNeeded } from '../../activitypub' | 12 | import { federateVideoIfNeeded } from '../../activitypub' |
13 | import { VideoModel } from '../../../models/video/video' | 13 | import { VideoModel } from '../../../models/video/video' |
14 | import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 14 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' |
15 | import { getSecureTorrentName } from '../../../helpers/utils' | 15 | import { getSecureTorrentName } from '../../../helpers/utils' |
16 | import { move, remove, stat } from 'fs-extra' | 16 | import { move, remove, stat } from 'fs-extra' |
17 | import { Notifier } from '../../notifier' | 17 | import { Notifier } from '../../notifier' |
@@ -21,7 +21,7 @@ import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumb | |||
21 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
22 | import { MThumbnail } from '../../../typings/models/video/thumbnail' | 22 | import { MThumbnail } from '../../../typings/models/video/thumbnail' |
23 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' | 23 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' |
24 | import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models' | 24 | import { getVideoFilePath } from '@server/lib/video-paths' |
25 | 25 | ||
26 | type VideoImportYoutubeDLPayload = { | 26 | type VideoImportYoutubeDLPayload = { |
27 | type: 'youtube-dl' | 27 | type: 'youtube-dl' |
@@ -142,12 +142,12 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
142 | } | 142 | } |
143 | videoFile = new VideoFileModel(videoFileData) | 143 | videoFile = new VideoFileModel(videoFileData) |
144 | 144 | ||
145 | const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ] }) | 145 | const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ], VideoStreamingPlaylists: [] }) |
146 | // To clean files if the import fails | 146 | // To clean files if the import fails |
147 | const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles }) | 147 | const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles }) |
148 | 148 | ||
149 | // Move file | 149 | // Move file |
150 | videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImportWithFiles.Video.getVideoFilename(videoFile)) | 150 | videoDestFile = getVideoFilePath(videoImportWithFiles.Video, videoFile) |
151 | await move(tempVideoPath, videoDestFile) | 151 | await move(tempVideoPath, videoDestFile) |
152 | tempVideoPath = null // This path is not used anymore | 152 | tempVideoPath = null // This path is not used anymore |
153 | 153 | ||
@@ -168,7 +168,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
168 | } | 168 | } |
169 | 169 | ||
170 | // Create torrent | 170 | // Create torrent |
171 | await videoImportWithFiles.Video.createTorrentAndSetInfoHash(videoFile) | 171 | await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile) |
172 | 172 | ||
173 | const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => { | 173 | const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => { |
174 | const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo | 174 | const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 2ebe15bcb..39b9fac98 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { VideoResolution, VideoState } from '../../../../shared' | 2 | import { VideoResolution } from '../../../../shared' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
5 | import { JobQueue } from '../job-queue' | 5 | import { JobQueue } from '../job-queue' |
@@ -8,10 +8,10 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding' | 11 | import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | 12 | import { Notifier } from '../../notifier' |
13 | import { CONFIG } from '../../../initializers/config' | 13 | import { CONFIG } from '../../../initializers/config' |
14 | import { MVideoUUID, MVideoWithFile } from '@server/typings/models' | 14 | import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models' |
15 | 15 | ||
16 | interface BaseTranscodingPayload { | 16 | interface BaseTranscodingPayload { |
17 | videoUUID: string | 17 | videoUUID: string |
@@ -22,6 +22,7 @@ interface HLSTranscodingPayload extends BaseTranscodingPayload { | |||
22 | type: 'hls' | 22 | type: 'hls' |
23 | isPortraitMode?: boolean | 23 | isPortraitMode?: boolean |
24 | resolution: VideoResolution | 24 | resolution: VideoResolution |
25 | copyCodecs: boolean | ||
25 | } | 26 | } |
26 | 27 | ||
27 | interface NewResolutionTranscodingPayload extends BaseTranscodingPayload { | 28 | interface NewResolutionTranscodingPayload extends BaseTranscodingPayload { |
@@ -54,11 +55,11 @@ async function processVideoTranscoding (job: Bull.Job) { | |||
54 | } | 55 | } |
55 | 56 | ||
56 | if (payload.type === 'hls') { | 57 | if (payload.type === 'hls') { |
57 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) | 58 | await generateHlsPlaylist(video, payload.resolution, payload.copyCodecs, payload.isPortraitMode || false) |
58 | 59 | ||
59 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | 60 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) |
60 | } else if (payload.type === 'new-resolution') { | 61 | } else if (payload.type === 'new-resolution') { |
61 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) | 62 | await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false) |
62 | 63 | ||
63 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) | 64 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) |
64 | } else if (payload.type === 'merge-audio') { | 65 | } else if (payload.type === 'merge-audio') { |
@@ -66,7 +67,7 @@ async function processVideoTranscoding (job: Bull.Job) { | |||
66 | 67 | ||
67 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) | 68 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) |
68 | } else { | 69 | } else { |
69 | await optimizeVideofile(video) | 70 | await optimizeOriginalVideofile(video) |
70 | 71 | ||
71 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload) | 72 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload) |
72 | } | 73 | } |
@@ -74,48 +75,24 @@ async function processVideoTranscoding (job: Bull.Job) { | |||
74 | return video | 75 | return video |
75 | } | 76 | } |
76 | 77 | ||
77 | async function onHlsPlaylistGenerationSuccess (video: MVideoUUID) { | 78 | async function onHlsPlaylistGenerationSuccess (video: MVideoFullLight) { |
78 | if (video === undefined) return undefined | 79 | if (video === undefined) return undefined |
79 | 80 | ||
80 | await sequelizeTypescript.transaction(async t => { | 81 | // We generated the HLS playlist, we don't need the webtorrent files anymore if the admin disabled it |
81 | // Maybe the video changed in database, refresh it | 82 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { |
82 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 83 | for (const file of video.VideoFiles) { |
83 | // Video does not exist anymore | 84 | await video.removeFile(file) |
84 | if (!videoDatabase) return undefined | 85 | await file.destroy() |
85 | |||
86 | // If the video was not published, we consider it is a new one for other instances | ||
87 | await federateVideoIfNeeded(videoDatabase, false, t) | ||
88 | }) | ||
89 | } | ||
90 | |||
91 | async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) { | ||
92 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | ||
93 | // Maybe the video changed in database, refresh it | ||
94 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | ||
95 | // Video does not exist anymore | ||
96 | if (!videoDatabase) return undefined | ||
97 | |||
98 | let videoPublished = false | ||
99 | |||
100 | // We transcoded the video file in another format, now we can publish it | ||
101 | if (videoDatabase.state !== VideoState.PUBLISHED) { | ||
102 | videoPublished = true | ||
103 | |||
104 | videoDatabase.state = VideoState.PUBLISHED | ||
105 | videoDatabase.publishedAt = new Date() | ||
106 | videoDatabase = await videoDatabase.save({ transaction: t }) | ||
107 | } | 86 | } |
108 | 87 | ||
109 | // If the video was not published, we consider it is a new one for other instances | 88 | video.VideoFiles = [] |
110 | await federateVideoIfNeeded(videoDatabase, videoPublished, t) | 89 | } |
111 | 90 | ||
112 | return { videoDatabase, videoPublished } | 91 | return publishAndFederateIfNeeded(video) |
113 | }) | 92 | } |
114 | 93 | ||
115 | if (videoPublished) { | 94 | async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) { |
116 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) | 95 | await publishAndFederateIfNeeded(video) |
117 | Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | ||
118 | } | ||
119 | 96 | ||
120 | await createHlsJobIfEnabled(payload) | 97 | await createHlsJobIfEnabled(payload) |
121 | } | 98 | } |
@@ -124,7 +101,7 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
124 | if (videoArg === undefined) return undefined | 101 | if (videoArg === undefined) return undefined |
125 | 102 | ||
126 | // Outside the transaction (IO on disk) | 103 | // Outside the transaction (IO on disk) |
127 | const { videoFileResolution } = await videoArg.getOriginalFileResolution() | 104 | const { videoFileResolution } = await videoArg.getMaxQualityResolution() |
128 | 105 | ||
129 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 106 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
130 | // Maybe the video changed in database, refresh it | 107 | // Maybe the video changed in database, refresh it |
@@ -141,14 +118,29 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
141 | 118 | ||
142 | let videoPublished = false | 119 | let videoPublished = false |
143 | 120 | ||
121 | const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution }) | ||
122 | await createHlsJobIfEnabled(hlsPayload) | ||
123 | |||
144 | if (resolutionsEnabled.length !== 0) { | 124 | if (resolutionsEnabled.length !== 0) { |
145 | const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = [] | 125 | const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = [] |
146 | 126 | ||
147 | for (const resolution of resolutionsEnabled) { | 127 | for (const resolution of resolutionsEnabled) { |
148 | const dataInput = { | 128 | let dataInput: VideoTranscodingPayload |
149 | type: 'new-resolution' as 'new-resolution', | 129 | |
150 | videoUUID: videoDatabase.uuid, | 130 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) { |
151 | resolution | 131 | dataInput = { |
132 | type: 'new-resolution' as 'new-resolution', | ||
133 | videoUUID: videoDatabase.uuid, | ||
134 | resolution | ||
135 | } | ||
136 | } else if (CONFIG.TRANSCODING.HLS.ENABLED) { | ||
137 | dataInput = { | ||
138 | type: 'hls', | ||
139 | videoUUID: videoDatabase.uuid, | ||
140 | resolution, | ||
141 | isPortraitMode: false, | ||
142 | copyCodecs: false | ||
143 | } | ||
152 | } | 144 | } |
153 | 145 | ||
154 | const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) | 146 | const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) |
@@ -159,11 +151,8 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
159 | 151 | ||
160 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) | 152 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) |
161 | } else { | 153 | } else { |
162 | videoPublished = true | ||
163 | |||
164 | // No transcoding to do, it's now published | 154 | // No transcoding to do, it's now published |
165 | videoDatabase.state = VideoState.PUBLISHED | 155 | videoPublished = await videoDatabase.publishIfNeededAndSave(t) |
166 | videoDatabase = await videoDatabase.save({ transaction: t }) | ||
167 | 156 | ||
168 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) | 157 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) |
169 | } | 158 | } |
@@ -175,9 +164,6 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
175 | 164 | ||
176 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) | 165 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) |
177 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | 166 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) |
178 | |||
179 | const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }) | ||
180 | await createHlsJobIfEnabled(hlsPayload) | ||
181 | } | 167 | } |
182 | 168 | ||
183 | // --------------------------------------------------------------------------- | 169 | // --------------------------------------------------------------------------- |
@@ -196,9 +182,32 @@ function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: numbe | |||
196 | type: 'hls' as 'hls', | 182 | type: 'hls' as 'hls', |
197 | videoUUID: payload.videoUUID, | 183 | videoUUID: payload.videoUUID, |
198 | resolution: payload.resolution, | 184 | resolution: payload.resolution, |
199 | isPortraitMode: payload.isPortraitMode | 185 | isPortraitMode: payload.isPortraitMode, |
186 | copyCodecs: true | ||
200 | } | 187 | } |
201 | 188 | ||
202 | return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) | 189 | return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) |
203 | } | 190 | } |
204 | } | 191 | } |
192 | |||
193 | async function publishAndFederateIfNeeded (video: MVideoUUID) { | ||
194 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | ||
195 | // Maybe the video changed in database, refresh it | ||
196 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | ||
197 | // Video does not exist anymore | ||
198 | if (!videoDatabase) return undefined | ||
199 | |||
200 | // We transcoded the video file in another format, now we can publish it | ||
201 | const videoPublished = await videoDatabase.publishIfNeededAndSave(t) | ||
202 | |||
203 | // If the video was not published, we consider it is a new one for other instances | ||
204 | await federateVideoIfNeeded(videoDatabase, videoPublished, t) | ||
205 | |||
206 | return { videoDatabase, videoPublished } | ||
207 | }) | ||
208 | |||
209 | if (videoPublished) { | ||
210 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) | ||
211 | Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | ||
212 | } | ||
213 | } | ||
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 5b673b913..293bba91f 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -6,8 +6,8 @@ import { federateVideoIfNeeded } from '../activitypub' | |||
6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | 7 | import { VideoPrivacy } from '../../../shared/models/videos' |
8 | import { Notifier } from '../notifier' | 8 | import { Notifier } from '../notifier' |
9 | import { VideoModel } from '../../models/video/video' | ||
10 | import { sequelizeTypescript } from '../../initializers/database' | 9 | import { sequelizeTypescript } from '../../initializers/database' |
10 | import { MVideoFullLight } from '@server/typings/models' | ||
11 | 11 | ||
12 | export class UpdateVideosScheduler extends AbstractScheduler { | 12 | export class UpdateVideosScheduler extends AbstractScheduler { |
13 | 13 | ||
@@ -28,7 +28,7 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
28 | 28 | ||
29 | const publishedVideos = await sequelizeTypescript.transaction(async t => { | 29 | const publishedVideos = await sequelizeTypescript.transaction(async t => { |
30 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) | 30 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) |
31 | const publishedVideos: VideoModel[] = [] | 31 | const publishedVideos: MVideoFullLight[] = [] |
32 | 32 | ||
33 | for (const schedule of schedules) { | 33 | for (const schedule of schedules) { |
34 | const video = schedule.Video | 34 | const video = schedule.Video |
@@ -45,8 +45,8 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
45 | await federateVideoIfNeeded(video, isNewVideo, t) | 45 | await federateVideoIfNeeded(video, isNewVideo, t) |
46 | 46 | ||
47 | if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { | 47 | if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { |
48 | video.ScheduleVideoUpdate = schedule | 48 | const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] }) |
49 | publishedVideos.push(video) | 49 | publishedVideos.push(videoToPublish) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | 52 | ||
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index 1e30f6ebc..f2bd75cb4 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -3,7 +3,7 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } | |||
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { VideosRedundancy } from '../../../shared/models/redundancy' | 4 | import { VideosRedundancy } from '../../../shared/models/redundancy' |
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
6 | import { downloadWebTorrentVideo } from '../../helpers/webtorrent' | 6 | import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' |
7 | import { join } from 'path' | 7 | import { join } from 'path' |
8 | import { move } from 'fs-extra' | 8 | import { move } from 'fs-extra' |
9 | import { getServerActor } from '../../helpers/utils' | 9 | import { getServerActor } from '../../helpers/utils' |
@@ -24,6 +24,7 @@ import { | |||
24 | MVideoRedundancyVideo, | 24 | MVideoRedundancyVideo, |
25 | MVideoWithAllFiles | 25 | MVideoWithAllFiles |
26 | } from '@server/typings/models' | 26 | } from '@server/typings/models' |
27 | import { getVideoFilename } from '../video-paths' | ||
27 | 28 | ||
28 | type CandidateToDuplicate = { | 29 | type CandidateToDuplicate = { |
29 | redundancy: VideosRedundancy, | 30 | redundancy: VideosRedundancy, |
@@ -195,11 +196,11 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
195 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) | 196 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) |
196 | 197 | ||
197 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 198 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
198 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) | 199 | const magnetUri = await generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) |
199 | 200 | ||
200 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) | 201 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) |
201 | 202 | ||
202 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) | 203 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, getVideoFilename(video, file)) |
203 | await move(tmpPath, destPath, { overwrite: true }) | 204 | await move(tmpPath, destPath, { overwrite: true }) |
204 | 205 | ||
205 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ | 206 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 84791955e..a99f71629 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -9,6 +9,7 @@ import { downloadImage } from '../helpers/requests' | |||
9 | import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist' | 9 | import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist' |
10 | import { MVideoFile, MVideoThumbnail } from '../typings/models' | 10 | import { MVideoFile, MVideoThumbnail } from '../typings/models' |
11 | import { MThumbnail } from '../typings/models/video/thumbnail' | 11 | import { MThumbnail } from '../typings/models/video/thumbnail' |
12 | import { getVideoFilePath } from './video-paths' | ||
12 | 13 | ||
13 | type ImageSize = { height: number, width: number } | 14 | type ImageSize = { height: number, width: number } |
14 | 15 | ||
@@ -55,7 +56,7 @@ function createVideoMiniatureFromExisting ( | |||
55 | } | 56 | } |
56 | 57 | ||
57 | function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) { | 58 | function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) { |
58 | const input = video.getVideoFilePath(videoFile) | 59 | const input = getVideoFilePath(video, videoFile) |
59 | 60 | ||
60 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) | 61 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) |
61 | const thumbnailCreator = videoFile.isAudio() | 62 | const thumbnailCreator = videoFile.isAudio() |
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts new file mode 100644 index 000000000..63011cdb2 --- /dev/null +++ b/server/lib/video-paths.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/typings/models' | ||
2 | import { extractVideo } from './videos' | ||
3 | import { join } from 'path' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' | ||
6 | |||
7 | // ################## Video file name ################## | ||
8 | |||
9 | function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { | ||
10 | const video = extractVideo(videoOrPlaylist) | ||
11 | |||
12 | if (isStreamingPlaylist(videoOrPlaylist)) { | ||
13 | return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution) | ||
14 | } | ||
15 | |||
16 | return generateWebTorrentVideoName(video.uuid, videoFile.resolution, videoFile.extname) | ||
17 | } | ||
18 | |||
19 | function generateVideoStreamingPlaylistName (uuid: string, resolution: number) { | ||
20 | return `${uuid}-${resolution}-fragmented.mp4` | ||
21 | } | ||
22 | |||
23 | function generateWebTorrentVideoName (uuid: string, resolution: number, extname: string) { | ||
24 | return uuid + '-' + resolution + extname | ||
25 | } | ||
26 | |||
27 | function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) { | ||
28 | if (isStreamingPlaylist(videoOrPlaylist)) { | ||
29 | const video = extractVideo(videoOrPlaylist) | ||
30 | return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid, getVideoFilename(videoOrPlaylist, videoFile)) | ||
31 | } | ||
32 | |||
33 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR | ||
34 | return join(baseDir, getVideoFilename(videoOrPlaylist, videoFile)) | ||
35 | } | ||
36 | |||
37 | // ################## Torrents ################## | ||
38 | |||
39 | function getTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { | ||
40 | const video = extractVideo(videoOrPlaylist) | ||
41 | const extension = '.torrent' | ||
42 | |||
43 | if (isStreamingPlaylist(videoOrPlaylist)) { | ||
44 | return `${video.uuid}-${videoFile.resolution}-${videoOrPlaylist.getStringType()}${extension}` | ||
45 | } | ||
46 | |||
47 | return video.uuid + '-' + videoFile.resolution + extension | ||
48 | } | ||
49 | |||
50 | function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { | ||
51 | return join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile)) | ||
52 | } | ||
53 | |||
54 | // --------------------------------------------------------------------------- | ||
55 | |||
56 | export { | ||
57 | generateVideoStreamingPlaylistName, | ||
58 | generateWebTorrentVideoName, | ||
59 | getVideoFilename, | ||
60 | getVideoFilePath, | ||
61 | |||
62 | getTorrentFileName, | ||
63 | getTorrentFilePath | ||
64 | } | ||
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 612d388ee..9243d1742 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' | 1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' |
2 | import { basename, join } from 'path' | 2 | import { basename, extname as extnameUtil, join } from 'path' |
3 | import { | 3 | import { |
4 | canDoQuickTranscode, | 4 | canDoQuickTranscode, |
5 | getDurationFromVideoFile, | 5 | getDurationFromVideoFile, |
@@ -16,18 +16,19 @@ import { updateMasterHLSPlaylist, updateSha256Segments } from './hls' | |||
16 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 16 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
17 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | 17 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' |
18 | import { CONFIG } from '../initializers/config' | 18 | import { CONFIG } from '../initializers/config' |
19 | import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models' | 19 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/typings/models' |
20 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | ||
21 | import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' | ||
20 | 22 | ||
21 | /** | 23 | /** |
22 | * Optimize the original video file and replace it. The resolution is not changed. | 24 | * Optimize the original video file and replace it. The resolution is not changed. |
23 | */ | 25 | */ |
24 | async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) { | 26 | async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) { |
25 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
26 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 27 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
27 | const newExtname = '.mp4' | 28 | const newExtname = '.mp4' |
28 | 29 | ||
29 | const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile() | 30 | const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile() |
30 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | 31 | const videoInputPath = getVideoFilePath(video, inputVideoFile) |
31 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 32 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
32 | 33 | ||
33 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) | 34 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) |
@@ -35,7 +36,7 @@ async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVi | |||
35 | : 'video' | 36 | : 'video' |
36 | 37 | ||
37 | const transcodeOptions: TranscodeOptions = { | 38 | const transcodeOptions: TranscodeOptions = { |
38 | type: transcodeType as any, // FIXME: typing issue | 39 | type: transcodeType, |
39 | inputPath: videoInputPath, | 40 | inputPath: videoInputPath, |
40 | outputPath: videoTranscodedPath, | 41 | outputPath: videoTranscodedPath, |
41 | resolution: inputVideoFile.resolution | 42 | resolution: inputVideoFile.resolution |
@@ -50,7 +51,7 @@ async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVi | |||
50 | // Important to do this before getVideoFilename() to take in account the new file extension | 51 | // Important to do this before getVideoFilename() to take in account the new file extension |
51 | inputVideoFile.extname = newExtname | 52 | inputVideoFile.extname = newExtname |
52 | 53 | ||
53 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | 54 | const videoOutputPath = getVideoFilePath(video, inputVideoFile) |
54 | 55 | ||
55 | await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 56 | await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
56 | } catch (err) { | 57 | } catch (err) { |
@@ -64,13 +65,12 @@ async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVi | |||
64 | /** | 65 | /** |
65 | * Transcode the original video file to a lower resolution. | 66 | * Transcode the original video file to a lower resolution. |
66 | */ | 67 | */ |
67 | async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) { | 68 | async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) { |
68 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
69 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 69 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
70 | const extname = '.mp4' | 70 | const extname = '.mp4' |
71 | 71 | ||
72 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | 72 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed |
73 | const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) | 73 | const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile()) |
74 | 74 | ||
75 | const newVideoFile = new VideoFileModel({ | 75 | const newVideoFile = new VideoFileModel({ |
76 | resolution, | 76 | resolution, |
@@ -78,8 +78,8 @@ async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: Vi | |||
78 | size: 0, | 78 | size: 0, |
79 | videoId: video.id | 79 | videoId: video.id |
80 | }) | 80 | }) |
81 | const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) | 81 | const videoOutputPath = getVideoFilePath(video, newVideoFile) |
82 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) | 82 | const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile)) |
83 | 83 | ||
84 | const transcodeOptions = { | 84 | const transcodeOptions = { |
85 | type: 'video' as 'video', | 85 | type: 'video' as 'video', |
@@ -94,14 +94,13 @@ async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: Vi | |||
94 | return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | 94 | return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) |
95 | } | 95 | } |
96 | 96 | ||
97 | async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) { | 97 | async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) { |
98 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
99 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 98 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
100 | const newExtname = '.mp4' | 99 | const newExtname = '.mp4' |
101 | 100 | ||
102 | const inputVideoFile = video.getOriginalFile() | 101 | const inputVideoFile = video.getMaxQualityFile() |
103 | 102 | ||
104 | const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) | 103 | const audioInputPath = getVideoFilePath(video, inputVideoFile) |
105 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 104 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
106 | 105 | ||
107 | // If the user updates the video preview during transcoding | 106 | // If the user updates the video preview during transcoding |
@@ -130,7 +129,7 @@ async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: | |||
130 | // Important to do this before getVideoFilename() to take in account the new file extension | 129 | // Important to do this before getVideoFilename() to take in account the new file extension |
131 | inputVideoFile.extname = newExtname | 130 | inputVideoFile.extname = newExtname |
132 | 131 | ||
133 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | 132 | const videoOutputPath = getVideoFilePath(video, inputVideoFile) |
134 | // ffmpeg generated a new video file, so update the video duration | 133 | // ffmpeg generated a new video file, so update the video duration |
135 | // See https://trac.ffmpeg.org/ticket/5456 | 134 | // See https://trac.ffmpeg.org/ticket/5456 |
136 | video.duration = await getDurationFromVideoFile(videoTranscodedPath) | 135 | video.duration = await getDurationFromVideoFile(videoTranscodedPath) |
@@ -139,33 +138,40 @@ async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: | |||
139 | return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 138 | return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
140 | } | 139 | } |
141 | 140 | ||
142 | async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) { | 141 | async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, copyCodecs: boolean, isPortraitMode: boolean) { |
143 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 142 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
144 | await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) | 143 | await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) |
145 | 144 | ||
146 | const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getFile(resolution))) | 145 | const videoFileInput = copyCodecs |
146 | ? video.getWebTorrentFile(resolution) | ||
147 | : video.getMaxQualityFile() | ||
148 | |||
149 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() | ||
150 | const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput) | ||
151 | |||
147 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | 152 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) |
153 | const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) | ||
148 | 154 | ||
149 | const transcodeOptions = { | 155 | const transcodeOptions = { |
150 | type: 'hls' as 'hls', | 156 | type: 'hls' as 'hls', |
151 | inputPath: videoInputPath, | 157 | inputPath: videoInputPath, |
152 | outputPath, | 158 | outputPath, |
153 | resolution, | 159 | resolution, |
160 | copyCodecs, | ||
154 | isPortraitMode, | 161 | isPortraitMode, |
155 | 162 | ||
156 | hlsPlaylist: { | 163 | hlsPlaylist: { |
157 | videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution) | 164 | videoFilename |
158 | } | 165 | } |
159 | } | 166 | } |
160 | 167 | ||
161 | await transcode(transcodeOptions) | 168 | logger.debug('Will run transcode.', { transcodeOptions }) |
162 | 169 | ||
163 | await updateMasterHLSPlaylist(video) | 170 | await transcode(transcodeOptions) |
164 | await updateSha256Segments(video) | ||
165 | 171 | ||
166 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | 172 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) |
167 | 173 | ||
168 | await VideoStreamingPlaylistModel.upsert({ | 174 | const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ |
169 | videoId: video.id, | 175 | videoId: video.id, |
170 | playlistUrl, | 176 | playlistUrl, |
171 | segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid), | 177 | segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid), |
@@ -173,15 +179,44 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso | |||
173 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | 179 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, |
174 | 180 | ||
175 | type: VideoStreamingPlaylistType.HLS | 181 | type: VideoStreamingPlaylistType.HLS |
182 | }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ] | ||
183 | videoStreamingPlaylist.Video = video | ||
184 | |||
185 | const newVideoFile = new VideoFileModel({ | ||
186 | resolution, | ||
187 | extname: extnameUtil(videoFilename), | ||
188 | size: 0, | ||
189 | fps: -1, | ||
190 | videoStreamingPlaylistId: videoStreamingPlaylist.id | ||
176 | }) | 191 | }) |
192 | |||
193 | const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile) | ||
194 | const stats = await stat(videoFilePath) | ||
195 | |||
196 | newVideoFile.size = stats.size | ||
197 | newVideoFile.fps = await getVideoFileFPS(videoFilePath) | ||
198 | |||
199 | await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) | ||
200 | |||
201 | const updatedVideoFile = await newVideoFile.save() | ||
202 | |||
203 | videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') as VideoFileModel[] | ||
204 | videoStreamingPlaylist.VideoFiles.push(updatedVideoFile) | ||
205 | |||
206 | video.setHLSPlaylist(videoStreamingPlaylist) | ||
207 | |||
208 | await updateMasterHLSPlaylist(video) | ||
209 | await updateSha256Segments(video) | ||
210 | |||
211 | return video | ||
177 | } | 212 | } |
178 | 213 | ||
179 | // --------------------------------------------------------------------------- | 214 | // --------------------------------------------------------------------------- |
180 | 215 | ||
181 | export { | 216 | export { |
182 | generateHlsPlaylist, | 217 | generateHlsPlaylist, |
183 | optimizeVideofile, | 218 | optimizeOriginalVideofile, |
184 | transcodeOriginalVideofile, | 219 | transcodeNewResolution, |
185 | mergeAudioVideofile | 220 | mergeAudioVideofile |
186 | } | 221 | } |
187 | 222 | ||
@@ -196,7 +231,7 @@ async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoF | |||
196 | videoFile.size = stats.size | 231 | videoFile.size = stats.size |
197 | videoFile.fps = fps | 232 | videoFile.fps = fps |
198 | 233 | ||
199 | await video.createTorrentAndSetInfoHash(videoFile) | 234 | await createTorrentAndSetInfoHash(video, videoFile) |
200 | 235 | ||
201 | const updatedVideoFile = await videoFile.save() | 236 | const updatedVideoFile = await videoFile.save() |
202 | 237 | ||
diff --git a/server/lib/videos.ts b/server/lib/videos.ts new file mode 100644 index 000000000..22e9afbf9 --- /dev/null +++ b/server/lib/videos.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models' | ||
2 | |||
3 | function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { | ||
4 | return isStreamingPlaylist(videoOrPlaylist) | ||
5 | ? videoOrPlaylist.Video | ||
6 | : videoOrPlaylist | ||
7 | } | ||
8 | |||
9 | export { | ||
10 | extractVideo | ||
11 | } | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index 5059ed0f2..1db907f91 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -43,6 +43,9 @@ const customConfigUpdateValidator = [ | |||
43 | body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), | 43 | body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), |
44 | body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), | 44 | body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), |
45 | 45 | ||
46 | body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'), | ||
47 | body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'), | ||
48 | |||
46 | body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), | 49 | body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), |
47 | body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), | 50 | body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), |
48 | 51 | ||
@@ -56,6 +59,7 @@ const customConfigUpdateValidator = [ | |||
56 | 59 | ||
57 | if (areValidationErrors(req, res)) return | 60 | if (areValidationErrors(req, res)) return |
58 | if (!checkInvalidConfigIfEmailDisabled(req.body as CustomConfig, res)) return | 61 | if (!checkInvalidConfigIfEmailDisabled(req.body as CustomConfig, res)) return |
62 | if (!checkInvalidTranscodingConfig(req.body as CustomConfig, res)) return | ||
59 | 63 | ||
60 | return next() | 64 | return next() |
61 | } | 65 | } |
@@ -79,3 +83,16 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp | |||
79 | 83 | ||
80 | return true | 84 | return true |
81 | } | 85 | } |
86 | |||
87 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { | ||
88 | if (customConfig.transcoding.enabled === false) return true | ||
89 | |||
90 | if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { | ||
91 | res.status(400) | ||
92 | .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' }) | ||
93 | .end() | ||
94 | return false | ||
95 | } | ||
96 | |||
97 | return true | ||
98 | } | ||
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 1449903b7..53a2f193d 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -270,7 +270,7 @@ const videosAcceptChangeOwnershipValidator = [ | |||
270 | 270 | ||
271 | const user = res.locals.oauth.token.User | 271 | const user = res.locals.oauth.token.User |
272 | const videoChangeOwnership = res.locals.videoChangeOwnership | 272 | const videoChangeOwnership = res.locals.videoChangeOwnership |
273 | const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile()) | 273 | const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile()) |
274 | if (isAble === false) { | 274 | if (isAble === false) { |
275 | res.status(403) | 275 | res.status(403) |
276 | .json({ error: 'The user video quota is exceeded with this video.' }) | 276 | .json({ error: 'The user video quota is exceeded with this video.' }) |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 61d9a5612..77f83d8aa 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -497,7 +497,6 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
497 | expires: this.expiresOn.toISOString(), | 497 | expires: this.expiresOn.toISOString(), |
498 | url: { | 498 | url: { |
499 | type: 'Link', | 499 | type: 'Link', |
500 | mimeType: 'application/x-mpegURL', | ||
501 | mediaType: 'application/x-mpegURL', | 500 | mediaType: 'application/x-mpegURL', |
502 | href: this.fileUrl | 501 | href: this.fileUrl |
503 | } | 502 | } |
@@ -511,7 +510,6 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
511 | expires: this.expiresOn.toISOString(), | 510 | expires: this.expiresOn.toISOString(), |
512 | url: { | 511 | url: { |
513 | type: 'Link', | 512 | type: 'Link', |
514 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, | ||
515 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 513 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
516 | href: this.fileUrl, | 514 | href: this.fileUrl, |
517 | height: this.VideoFile.resolution, | 515 | height: this.VideoFile.resolution, |
diff --git a/server/models/utils.ts b/server/models/utils.ts index e7e6ddde1..ccdbcd1cf 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Model, Sequelize } from 'sequelize-typescript' | 1 | import { Model, Sequelize } from 'sequelize-typescript' |
2 | import * as validator from 'validator' | 2 | import * as validator from 'validator' |
3 | import { Col } from 'sequelize/types/lib/utils' | 3 | import { Col } from 'sequelize/types/lib/utils' |
4 | import { col, literal, OrderItem } from 'sequelize' | 4 | import { literal, OrderItem } from 'sequelize' |
5 | 5 | ||
6 | type SortType = { sortModel: string, sortValue: string } | 6 | type SortType = { sortModel: string, sortValue: string } |
7 | 7 | ||
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts index fc2a424aa..eefc10f14 100644 --- a/server/models/video/schedule-video-update.ts +++ b/server/models/video/schedule-video-update.ts | |||
@@ -2,7 +2,7 @@ import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Ta | |||
2 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 2 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' |
3 | import { VideoPrivacy } from '../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../shared/models/videos' |
4 | import { Op, Transaction } from 'sequelize' | 4 | import { Op, Transaction } from 'sequelize' |
5 | import { MScheduleVideoUpdateFormattable } from '@server/typings/models' | 5 | import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/typings/models' |
6 | 6 | ||
7 | @Table({ | 7 | @Table({ |
8 | tableName: 'scheduleVideoUpdate', | 8 | tableName: 'scheduleVideoUpdate', |
@@ -72,10 +72,12 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> { | |||
72 | { | 72 | { |
73 | model: VideoModel.scope( | 73 | model: VideoModel.scope( |
74 | [ | 74 | [ |
75 | VideoScopeNames.WITH_FILES, | 75 | VideoScopeNames.WITH_WEBTORRENT_FILES, |
76 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, | ||
76 | VideoScopeNames.WITH_ACCOUNT_DETAILS, | 77 | VideoScopeNames.WITH_ACCOUNT_DETAILS, |
77 | VideoScopeNames.WITH_BLACKLISTED, | 78 | VideoScopeNames.WITH_BLACKLISTED, |
78 | VideoScopeNames.WITH_THUMBNAILS | 79 | VideoScopeNames.WITH_THUMBNAILS, |
80 | VideoScopeNames.WITH_TAGS | ||
79 | ] | 81 | ] |
80 | ) | 82 | ) |
81 | } | 83 | } |
@@ -83,7 +85,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> { | |||
83 | transaction: t | 85 | transaction: t |
84 | } | 86 | } |
85 | 87 | ||
86 | return ScheduleVideoUpdateModel.findAll(query) | 88 | return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdateVideoAll>(query) |
87 | } | 89 | } |
88 | 90 | ||
89 | static deleteByVideoId (videoId: number, t: Transaction) { | 91 | static deleteByVideoId (videoId: number, t: Transaction) { |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index f7a351329..3259b6c02 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -43,7 +43,11 @@ enum ScopeNames { | |||
43 | [ScopeNames.WITH_VIDEO]: { | 43 | [ScopeNames.WITH_VIDEO]: { |
44 | include: [ | 44 | include: [ |
45 | { | 45 | { |
46 | model: VideoModel.scope([ VideoScopeNames.WITH_THUMBNAILS, VideoScopeNames.WITH_FILES ]), | 46 | model: VideoModel.scope([ |
47 | VideoScopeNames.WITH_THUMBNAILS, | ||
48 | VideoScopeNames.WITH_WEBTORRENT_FILES, | ||
49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS | ||
50 | ]), | ||
47 | required: true | 51 | required: true |
48 | } | 52 | } |
49 | ] | 53 | ] |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 68e2d562a..cacef0106 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -23,22 +23,52 @@ import { parseAggregateResult, throwIfNotValid } from '../utils' | |||
23 | import { VideoModel } from './video' | 23 | import { VideoModel } from './video' |
24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
26 | import { FindOptions, QueryTypes, Transaction } from 'sequelize' | 26 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' |
27 | import { MIMETYPES } from '../../initializers/constants' | 27 | import { MIMETYPES } from '../../initializers/constants' |
28 | import { MVideoFile } from '@server/typings/models' | 28 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file' |
29 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models' | ||
29 | 30 | ||
30 | @Table({ | 31 | @Table({ |
31 | tableName: 'videoFile', | 32 | tableName: 'videoFile', |
32 | indexes: [ | 33 | indexes: [ |
33 | { | 34 | { |
34 | fields: [ 'videoId' ] | 35 | fields: [ 'videoId' ], |
36 | where: { | ||
37 | videoId: { | ||
38 | [Op.ne]: null | ||
39 | } | ||
40 | } | ||
41 | }, | ||
42 | { | ||
43 | fields: [ 'videoStreamingPlaylistId' ], | ||
44 | where: { | ||
45 | videoStreamingPlaylistId: { | ||
46 | [Op.ne]: null | ||
47 | } | ||
48 | } | ||
35 | }, | 49 | }, |
50 | |||
36 | { | 51 | { |
37 | fields: [ 'infoHash' ] | 52 | fields: [ 'infoHash' ] |
38 | }, | 53 | }, |
54 | |||
39 | { | 55 | { |
40 | fields: [ 'videoId', 'resolution', 'fps' ], | 56 | fields: [ 'videoId', 'resolution', 'fps' ], |
41 | unique: true | 57 | unique: true, |
58 | where: { | ||
59 | videoId: { | ||
60 | [Op.ne]: null | ||
61 | } | ||
62 | } | ||
63 | }, | ||
64 | { | ||
65 | fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ], | ||
66 | unique: true, | ||
67 | where: { | ||
68 | videoStreamingPlaylistId: { | ||
69 | [Op.ne]: null | ||
70 | } | ||
71 | } | ||
42 | } | 72 | } |
43 | ] | 73 | ] |
44 | }) | 74 | }) |
@@ -81,12 +111,24 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
81 | 111 | ||
82 | @BelongsTo(() => VideoModel, { | 112 | @BelongsTo(() => VideoModel, { |
83 | foreignKey: { | 113 | foreignKey: { |
84 | allowNull: false | 114 | allowNull: true |
85 | }, | 115 | }, |
86 | onDelete: 'CASCADE' | 116 | onDelete: 'CASCADE' |
87 | }) | 117 | }) |
88 | Video: VideoModel | 118 | Video: VideoModel |
89 | 119 | ||
120 | @ForeignKey(() => VideoStreamingPlaylistModel) | ||
121 | @Column | ||
122 | videoStreamingPlaylistId: number | ||
123 | |||
124 | @BelongsTo(() => VideoStreamingPlaylistModel, { | ||
125 | foreignKey: { | ||
126 | allowNull: true | ||
127 | }, | ||
128 | onDelete: 'CASCADE' | ||
129 | }) | ||
130 | VideoStreamingPlaylist: VideoStreamingPlaylistModel | ||
131 | |||
90 | @HasMany(() => VideoRedundancyModel, { | 132 | @HasMany(() => VideoRedundancyModel, { |
91 | foreignKey: { | 133 | foreignKey: { |
92 | allowNull: true | 134 | allowNull: true |
@@ -163,6 +205,36 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
163 | })) | 205 | })) |
164 | } | 206 | } |
165 | 207 | ||
208 | // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes | ||
209 | static async customUpsert ( | ||
210 | videoFile: MVideoFile, | ||
211 | mode: 'streaming-playlist' | 'video', | ||
212 | transaction: Transaction | ||
213 | ) { | ||
214 | const baseWhere = { | ||
215 | fps: videoFile.fps, | ||
216 | resolution: videoFile.resolution | ||
217 | } | ||
218 | |||
219 | if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId }) | ||
220 | else Object.assign(baseWhere, { videoId: videoFile.videoId }) | ||
221 | |||
222 | const element = await VideoFileModel.findOne({ where: baseWhere, transaction }) | ||
223 | if (!element) return videoFile.save({ transaction }) | ||
224 | |||
225 | for (const k of Object.keys(videoFile.toJSON())) { | ||
226 | element[k] = videoFile[k] | ||
227 | } | ||
228 | |||
229 | return element.save({ transaction }) | ||
230 | } | ||
231 | |||
232 | getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo { | ||
233 | if (this.videoId) return (this as MVideoFileVideo).Video | ||
234 | |||
235 | return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist | ||
236 | } | ||
237 | |||
166 | isAudio () { | 238 | isAudio () { |
167 | return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] | 239 | return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] |
168 | } | 240 | } |
@@ -170,6 +242,9 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
170 | hasSameUniqueKeysThan (other: MVideoFile) { | 242 | hasSameUniqueKeysThan (other: MVideoFile) { |
171 | return this.fps === other.fps && | 243 | return this.fps === other.fps && |
172 | this.resolution === other.resolution && | 244 | this.resolution === other.resolution && |
173 | this.videoId === other.videoId | 245 | ( |
246 | (this.videoId !== null && this.videoId === other.videoId) || | ||
247 | (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId) | ||
248 | ) | ||
174 | } | 249 | } |
175 | } | 250 | } |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index 2987aa780..9fed2d49d 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -1,11 +1,6 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 1 | import { Video, VideoDetails } from '../../../shared/models/videos' |
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { | 3 | import { ActivityTagObject, ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
4 | ActivityPlaylistInfohashesObject, | ||
5 | ActivityPlaylistSegmentHashesObject, | ||
6 | ActivityUrlObject, | ||
7 | VideoTorrentObject | ||
8 | } from '../../../shared/models/activitypub/objects' | ||
9 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 4 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
10 | import { VideoCaptionModel } from './video-caption' | 5 | import { VideoCaptionModel } from './video-caption' |
11 | import { | 6 | import { |
@@ -16,9 +11,18 @@ import { | |||
16 | } from '../../lib/activitypub' | 11 | } from '../../lib/activitypub' |
17 | import { isArray } from '../../helpers/custom-validators/misc' | 12 | import { isArray } from '../../helpers/custom-validators/misc' |
18 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | 13 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' |
19 | import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models' | 14 | import { |
20 | import { MStreamingPlaylistRedundancies } from '../../typings/models/video/video-streaming-playlist' | 15 | MStreamingPlaylistRedundanciesOpt, |
16 | MStreamingPlaylistVideo, | ||
17 | MVideo, | ||
18 | MVideoAP, | ||
19 | MVideoFile, | ||
20 | MVideoFormattable, | ||
21 | MVideoFormattableDetails | ||
22 | } from '../../typings/models' | ||
21 | import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' | 23 | import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' |
24 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
25 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
22 | 26 | ||
23 | export type VideoFormattingJSONOptions = { | 27 | export type VideoFormattingJSONOptions = { |
24 | completeDescription?: boolean | 28 | completeDescription?: boolean |
@@ -115,7 +119,7 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid | |||
115 | 119 | ||
116 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | 120 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] |
117 | 121 | ||
118 | const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video.VideoStreamingPlaylists) | 122 | const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) |
119 | 123 | ||
120 | const detailsJson = { | 124 | const detailsJson = { |
121 | support: video.support, | 125 | support: video.support, |
@@ -138,33 +142,43 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid | |||
138 | } | 142 | } |
139 | 143 | ||
140 | // Format and sort video files | 144 | // Format and sort video files |
141 | detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | 145 | detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles) |
142 | 146 | ||
143 | return Object.assign(formattedJson, detailsJson) | 147 | return Object.assign(formattedJson, detailsJson) |
144 | } | 148 | } |
145 | 149 | ||
146 | function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] { | 150 | function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] { |
147 | if (isArray(playlists) === false) return [] | 151 | if (isArray(playlists) === false) return [] |
148 | 152 | ||
153 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
154 | |||
149 | return playlists | 155 | return playlists |
150 | .map(playlist => { | 156 | .map(playlist => { |
157 | const playlistWithVideo = Object.assign(playlist, { Video: video }) | ||
158 | |||
151 | const redundancies = isArray(playlist.RedundancyVideos) | 159 | const redundancies = isArray(playlist.RedundancyVideos) |
152 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | 160 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) |
153 | : [] | 161 | : [] |
154 | 162 | ||
163 | const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles) | ||
164 | |||
155 | return { | 165 | return { |
156 | id: playlist.id, | 166 | id: playlist.id, |
157 | type: playlist.type, | 167 | type: playlist.type, |
158 | playlistUrl: playlist.playlistUrl, | 168 | playlistUrl: playlist.playlistUrl, |
159 | segmentsSha256Url: playlist.segmentsSha256Url, | 169 | segmentsSha256Url: playlist.segmentsSha256Url, |
160 | redundancies | 170 | redundancies, |
161 | } as VideoStreamingPlaylist | 171 | files |
172 | } | ||
162 | }) | 173 | }) |
163 | } | 174 | } |
164 | 175 | ||
165 | function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] { | 176 | function videoFilesModelToFormattedJSON ( |
166 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 177 | model: MVideo | MStreamingPlaylistVideo, |
167 | 178 | baseUrlHttp: string, | |
179 | baseUrlWs: string, | ||
180 | videoFiles: MVideoFileRedundanciesOpt[] | ||
181 | ): VideoFile[] { | ||
168 | return videoFiles | 182 | return videoFiles |
169 | .map(videoFile => { | 183 | .map(videoFile => { |
170 | let resolutionLabel = videoFile.resolution + 'p' | 184 | let resolutionLabel = videoFile.resolution + 'p' |
@@ -174,13 +188,13 @@ function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRe | |||
174 | id: videoFile.resolution, | 188 | id: videoFile.resolution, |
175 | label: resolutionLabel | 189 | label: resolutionLabel |
176 | }, | 190 | }, |
177 | magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | 191 | magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), |
178 | size: videoFile.size, | 192 | size: videoFile.size, |
179 | fps: videoFile.fps, | 193 | fps: videoFile.fps, |
180 | torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp), | 194 | torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp), |
181 | torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp), | 195 | torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), |
182 | fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp), | 196 | fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), |
183 | fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp) | 197 | fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp) |
184 | } as VideoFile | 198 | } as VideoFile |
185 | }) | 199 | }) |
186 | .sort((a, b) => { | 200 | .sort((a, b) => { |
@@ -190,6 +204,39 @@ function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRe | |||
190 | }) | 204 | }) |
191 | } | 205 | } |
192 | 206 | ||
207 | function addVideoFilesInAPAcc ( | ||
208 | acc: ActivityUrlObject[] | ActivityTagObject[], | ||
209 | model: MVideoAP | MStreamingPlaylistVideo, | ||
210 | baseUrlHttp: string, | ||
211 | baseUrlWs: string, | ||
212 | files: MVideoFile[] | ||
213 | ) { | ||
214 | for (const file of files) { | ||
215 | acc.push({ | ||
216 | type: 'Link', | ||
217 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, | ||
218 | href: model.getVideoFileUrl(file, baseUrlHttp), | ||
219 | height: file.resolution, | ||
220 | size: file.size, | ||
221 | fps: file.fps | ||
222 | }) | ||
223 | |||
224 | acc.push({ | ||
225 | type: 'Link', | ||
226 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
227 | href: model.getTorrentUrl(file, baseUrlHttp), | ||
228 | height: file.resolution | ||
229 | }) | ||
230 | |||
231 | acc.push({ | ||
232 | type: 'Link', | ||
233 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
234 | href: generateMagnetUri(model, file, baseUrlHttp, baseUrlWs), | ||
235 | height: file.resolution | ||
236 | }) | ||
237 | } | ||
238 | } | ||
239 | |||
193 | function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | 240 | function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { |
194 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 241 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
195 | if (!video.Tags) video.Tags = [] | 242 | if (!video.Tags) video.Tags = [] |
@@ -224,50 +271,25 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | |||
224 | } | 271 | } |
225 | 272 | ||
226 | const url: ActivityUrlObject[] = [] | 273 | const url: ActivityUrlObject[] = [] |
227 | for (const file of video.VideoFiles) { | 274 | addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) |
228 | url.push({ | ||
229 | type: 'Link', | ||
230 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, | ||
231 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, | ||
232 | href: video.getVideoFileUrl(file, baseUrlHttp), | ||
233 | height: file.resolution, | ||
234 | size: file.size, | ||
235 | fps: file.fps | ||
236 | }) | ||
237 | |||
238 | url.push({ | ||
239 | type: 'Link', | ||
240 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
241 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
242 | href: video.getTorrentUrl(file, baseUrlHttp), | ||
243 | height: file.resolution | ||
244 | }) | ||
245 | |||
246 | url.push({ | ||
247 | type: 'Link', | ||
248 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
249 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
250 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | ||
251 | height: file.resolution | ||
252 | }) | ||
253 | } | ||
254 | 275 | ||
255 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | 276 | for (const playlist of (video.VideoStreamingPlaylists || [])) { |
256 | let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[] | 277 | let tag: ActivityTagObject[] |
257 | 278 | ||
258 | tag = playlist.p2pMediaLoaderInfohashes | 279 | tag = playlist.p2pMediaLoaderInfohashes |
259 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) | 280 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) |
260 | tag.push({ | 281 | tag.push({ |
261 | type: 'Link', | 282 | type: 'Link', |
262 | name: 'sha256', | 283 | name: 'sha256', |
263 | mimeType: 'application/json' as 'application/json', | ||
264 | mediaType: 'application/json' as 'application/json', | 284 | mediaType: 'application/json' as 'application/json', |
265 | href: playlist.segmentsSha256Url | 285 | href: playlist.segmentsSha256Url |
266 | }) | 286 | }) |
267 | 287 | ||
288 | const playlistWithVideo = Object.assign(playlist, { Video: video }) | ||
289 | addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || []) | ||
290 | |||
268 | url.push({ | 291 | url.push({ |
269 | type: 'Link', | 292 | type: 'Link', |
270 | mimeType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
271 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | 293 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', |
272 | href: playlist.playlistUrl, | 294 | href: playlist.playlistUrl, |
273 | tag | 295 | tag |
@@ -277,7 +299,6 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | |||
277 | // Add video url too | 299 | // Add video url too |
278 | url.push({ | 300 | url.push({ |
279 | type: 'Link', | 301 | type: 'Link', |
280 | mimeType: 'text/html', | ||
281 | mediaType: 'text/html', | 302 | mediaType: 'text/html', |
282 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | 303 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid |
283 | }) | 304 | }) |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 0ea90d28c..faad4cc2d 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -5,12 +5,14 @@ import { VideoModel } from './video' | |||
5 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
6 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 6 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
8 | import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_DOWNLOAD_PATHS, STATIC_PATHS } from '../../initializers/constants' |
9 | import { join } from 'path' | 9 | import { join } from 'path' |
10 | import { sha1 } from '../../helpers/core-utils' | 10 | import { sha1 } from '../../helpers/core-utils' |
11 | import { isArrayOf } from '../../helpers/custom-validators/misc' | 11 | import { isArrayOf } from '../../helpers/custom-validators/misc' |
12 | import { Op, QueryTypes } from 'sequelize' | 12 | import { Op, QueryTypes } from 'sequelize' |
13 | import { MStreamingPlaylist, MVideoFile } from '@server/typings/models' | 13 | import { MStreamingPlaylist, MVideoFile } from '@server/typings/models' |
14 | import { VideoFileModel } from '@server/models/video/video-file' | ||
15 | import { getTorrentFileName, getVideoFilename } from '@server/lib/video-paths' | ||
14 | 16 | ||
15 | @Table({ | 17 | @Table({ |
16 | tableName: 'videoStreamingPlaylist', | 18 | tableName: 'videoStreamingPlaylist', |
@@ -70,6 +72,14 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod | |||
70 | }) | 72 | }) |
71 | Video: VideoModel | 73 | Video: VideoModel |
72 | 74 | ||
75 | @HasMany(() => VideoFileModel, { | ||
76 | foreignKey: { | ||
77 | allowNull: true | ||
78 | }, | ||
79 | onDelete: 'CASCADE' | ||
80 | }) | ||
81 | VideoFiles: VideoFileModel[] | ||
82 | |||
73 | @HasMany(() => VideoRedundancyModel, { | 83 | @HasMany(() => VideoRedundancyModel, { |
74 | foreignKey: { | 84 | foreignKey: { |
75 | allowNull: false | 85 | allowNull: false |
@@ -91,11 +101,11 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod | |||
91 | .then(results => results.length === 1) | 101 | .then(results => results.length === 1) |
92 | } | 102 | } |
93 | 103 | ||
94 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: MVideoFile[]) { | 104 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
95 | const hashes: string[] = [] | 105 | const hashes: string[] = [] |
96 | 106 | ||
97 | // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115 | 107 | // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115 |
98 | for (let i = 0; i < videoFiles.length; i++) { | 108 | for (let i = 0; i < files.length; i++) { |
99 | hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`)) | 109 | hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`)) |
100 | } | 110 | } |
101 | 111 | ||
@@ -139,10 +149,6 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod | |||
139 | return 'segments-sha256.json' | 149 | return 'segments-sha256.json' |
140 | } | 150 | } |
141 | 151 | ||
142 | static getHlsVideoName (uuid: string, resolution: number) { | ||
143 | return `${uuid}-${resolution}-fragmented.mp4` | ||
144 | } | ||
145 | |||
146 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | 152 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { |
147 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | 153 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) |
148 | } | 154 | } |
@@ -165,6 +171,26 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod | |||
165 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid | 171 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid |
166 | } | 172 | } |
167 | 173 | ||
174 | getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { | ||
175 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile) | ||
176 | } | ||
177 | |||
178 | getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { | ||
179 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile) | ||
180 | } | ||
181 | |||
182 | getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) { | ||
183 | return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile)) | ||
184 | } | ||
185 | |||
186 | getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) { | ||
187 | return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile)) | ||
188 | } | ||
189 | |||
190 | getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { | ||
191 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | ||
192 | } | ||
193 | |||
168 | hasSameUniqueKeysThan (other: MStreamingPlaylist) { | 194 | hasSameUniqueKeysThan (other: MStreamingPlaylist) { |
169 | return this.type === other.type && | 195 | return this.type === other.type && |
170 | this.videoId === other.videoId | 196 | this.videoId === other.videoId |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0d1dbf106..f84a90992 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,7 +1,5 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy } from 'lodash' | 2 | import { maxBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | ||
4 | import * as parseTorrent from 'parse-torrent' | ||
5 | import { join } from 'path' | 3 | import { join } from 'path' |
6 | import { | 4 | import { |
7 | CountOptions, | 5 | CountOptions, |
@@ -38,11 +36,11 @@ import { | |||
38 | } from 'sequelize-typescript' | 36 | } from 'sequelize-typescript' |
39 | import { UserRight, VideoPrivacy, VideoState } from '../../../shared' | 37 | import { UserRight, VideoPrivacy, VideoState } from '../../../shared' |
40 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 38 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
41 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 39 | import { Video, VideoDetails } from '../../../shared/models/videos' |
42 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 40 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
43 | import { peertubeTruncate } from '../../helpers/core-utils' | 41 | import { peertubeTruncate } from '../../helpers/core-utils' |
44 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 42 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
45 | import { isArray, isBooleanValid } from '../../helpers/custom-validators/misc' | 43 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
46 | import { | 44 | import { |
47 | isVideoCategoryValid, | 45 | isVideoCategoryValid, |
48 | isVideoDescriptionValid, | 46 | isVideoDescriptionValid, |
@@ -100,7 +98,7 @@ import { VideoTagModel } from './video-tag' | |||
100 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 98 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
101 | import { VideoCaptionModel } from './video-caption' | 99 | import { VideoCaptionModel } from './video-caption' |
102 | import { VideoBlacklistModel } from './video-blacklist' | 100 | import { VideoBlacklistModel } from './video-blacklist' |
103 | import { remove, writeFile } from 'fs-extra' | 101 | import { remove } from 'fs-extra' |
104 | import { VideoViewModel } from './video-views' | 102 | import { VideoViewModel } from './video-views' |
105 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 103 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
106 | import { | 104 | import { |
@@ -117,18 +115,20 @@ import { VideoPlaylistElementModel } from './video-playlist-element' | |||
117 | import { CONFIG } from '../../initializers/config' | 115 | import { CONFIG } from '../../initializers/config' |
118 | import { ThumbnailModel } from './thumbnail' | 116 | import { ThumbnailModel } from './thumbnail' |
119 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 117 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
120 | import { createTorrentPromise } from '../../helpers/webtorrent' | ||
121 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 118 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
122 | import { | 119 | import { |
123 | MChannel, | 120 | MChannel, |
124 | MChannelAccountDefault, | 121 | MChannelAccountDefault, |
125 | MChannelId, | 122 | MChannelId, |
123 | MStreamingPlaylist, | ||
124 | MStreamingPlaylistFilesVideo, | ||
126 | MUserAccountId, | 125 | MUserAccountId, |
127 | MUserId, | 126 | MUserId, |
128 | MVideoAccountLight, | 127 | MVideoAccountLight, |
129 | MVideoAccountLightBlacklistAllFiles, | 128 | MVideoAccountLightBlacklistAllFiles, |
130 | MVideoAP, | 129 | MVideoAP, |
131 | MVideoDetails, | 130 | MVideoDetails, |
131 | MVideoFileVideo, | ||
132 | MVideoFormattable, | 132 | MVideoFormattable, |
133 | MVideoFormattableDetails, | 133 | MVideoFormattableDetails, |
134 | MVideoForUser, | 134 | MVideoForUser, |
@@ -140,8 +140,10 @@ import { | |||
140 | MVideoWithFile, | 140 | MVideoWithFile, |
141 | MVideoWithRights | 141 | MVideoWithRights |
142 | } from '../../typings/models' | 142 | } from '../../typings/models' |
143 | import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' | 143 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' |
144 | import { MThumbnail } from '../../typings/models/video/thumbnail' | 144 | import { MThumbnail } from '../../typings/models/video/thumbnail' |
145 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
146 | import { getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
145 | 147 | ||
146 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 148 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
147 | const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ | 149 | const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ |
@@ -211,7 +213,7 @@ export enum ScopeNames { | |||
211 | FOR_API = 'FOR_API', | 213 | FOR_API = 'FOR_API', |
212 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | 214 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', |
213 | WITH_TAGS = 'WITH_TAGS', | 215 | WITH_TAGS = 'WITH_TAGS', |
214 | WITH_FILES = 'WITH_FILES', | 216 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', |
215 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 217 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
216 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 218 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
217 | WITH_BLOCKLIST = 'WITH_BLOCKLIST', | 219 | WITH_BLOCKLIST = 'WITH_BLOCKLIST', |
@@ -666,7 +668,7 @@ export type AvailableForListIDsOptions = { | |||
666 | } | 668 | } |
667 | ] | 669 | ] |
668 | }, | 670 | }, |
669 | [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => { | 671 | [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => { |
670 | let subInclude: any[] = [] | 672 | let subInclude: any[] = [] |
671 | 673 | ||
672 | if (withRedundancies === true) { | 674 | if (withRedundancies === true) { |
@@ -691,16 +693,19 @@ export type AvailableForListIDsOptions = { | |||
691 | } | 693 | } |
692 | }, | 694 | }, |
693 | [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { | 695 | [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { |
694 | let subInclude: any[] = [] | 696 | const subInclude: IncludeOptions[] = [ |
697 | { | ||
698 | model: VideoFileModel.unscoped(), | ||
699 | required: false | ||
700 | } | ||
701 | ] | ||
695 | 702 | ||
696 | if (withRedundancies === true) { | 703 | if (withRedundancies === true) { |
697 | subInclude = [ | 704 | subInclude.push({ |
698 | { | 705 | attributes: [ 'fileUrl' ], |
699 | attributes: [ 'fileUrl' ], | 706 | model: VideoRedundancyModel.unscoped(), |
700 | model: VideoRedundancyModel.unscoped(), | 707 | required: false |
701 | required: false | 708 | }) |
702 | } | ||
703 | ] | ||
704 | } | 709 | } |
705 | 710 | ||
706 | return { | 711 | return { |
@@ -913,7 +918,7 @@ export class VideoModel extends Model<VideoModel> { | |||
913 | @HasMany(() => VideoFileModel, { | 918 | @HasMany(() => VideoFileModel, { |
914 | foreignKey: { | 919 | foreignKey: { |
915 | name: 'videoId', | 920 | name: 'videoId', |
916 | allowNull: false | 921 | allowNull: true |
917 | }, | 922 | }, |
918 | hooks: true, | 923 | hooks: true, |
919 | onDelete: 'cascade' | 924 | onDelete: 'cascade' |
@@ -1071,7 +1076,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1071 | } | 1076 | } |
1072 | 1077 | ||
1073 | return VideoModel.scope([ | 1078 | return VideoModel.scope([ |
1074 | ScopeNames.WITH_FILES, | 1079 | ScopeNames.WITH_WEBTORRENT_FILES, |
1075 | ScopeNames.WITH_STREAMING_PLAYLISTS, | 1080 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1076 | ScopeNames.WITH_THUMBNAILS | 1081 | ScopeNames.WITH_THUMBNAILS |
1077 | ]).findAll(query) | 1082 | ]).findAll(query) |
@@ -1463,7 +1468,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1463 | } | 1468 | } |
1464 | 1469 | ||
1465 | return VideoModel.scope([ | 1470 | return VideoModel.scope([ |
1466 | ScopeNames.WITH_FILES, | 1471 | ScopeNames.WITH_WEBTORRENT_FILES, |
1467 | ScopeNames.WITH_STREAMING_PLAYLISTS, | 1472 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1468 | ScopeNames.WITH_THUMBNAILS | 1473 | ScopeNames.WITH_THUMBNAILS |
1469 | ]).findOne(query) | 1474 | ]).findOne(query) |
@@ -1500,7 +1505,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1500 | 1505 | ||
1501 | return VideoModel.scope([ | 1506 | return VideoModel.scope([ |
1502 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1507 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1503 | ScopeNames.WITH_FILES, | 1508 | ScopeNames.WITH_WEBTORRENT_FILES, |
1504 | ScopeNames.WITH_STREAMING_PLAYLISTS, | 1509 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1505 | ScopeNames.WITH_THUMBNAILS, | 1510 | ScopeNames.WITH_THUMBNAILS, |
1506 | ScopeNames.WITH_BLACKLISTED | 1511 | ScopeNames.WITH_BLACKLISTED |
@@ -1521,7 +1526,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1521 | ScopeNames.WITH_BLACKLISTED, | 1526 | ScopeNames.WITH_BLACKLISTED, |
1522 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1527 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1523 | ScopeNames.WITH_SCHEDULED_UPDATE, | 1528 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1524 | ScopeNames.WITH_FILES, | 1529 | ScopeNames.WITH_WEBTORRENT_FILES, |
1525 | ScopeNames.WITH_STREAMING_PLAYLISTS, | 1530 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1526 | ScopeNames.WITH_THUMBNAILS | 1531 | ScopeNames.WITH_THUMBNAILS |
1527 | ] | 1532 | ] |
@@ -1555,7 +1560,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1555 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1560 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1556 | ScopeNames.WITH_SCHEDULED_UPDATE, | 1561 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1557 | ScopeNames.WITH_THUMBNAILS, | 1562 | ScopeNames.WITH_THUMBNAILS, |
1558 | { method: [ ScopeNames.WITH_FILES, true ] }, | 1563 | { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] }, |
1559 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } | 1564 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } |
1560 | ] | 1565 | ] |
1561 | 1566 | ||
@@ -1787,17 +1792,31 @@ export class VideoModel extends Model<VideoModel> { | |||
1787 | this.VideoChannel.Account.isBlocked() | 1792 | this.VideoChannel.Account.isBlocked() |
1788 | } | 1793 | } |
1789 | 1794 | ||
1790 | getOriginalFile <T extends MVideoWithFile> (this: T) { | 1795 | getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { |
1791 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1796 | if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { |
1797 | const file = maxBy(this.VideoFiles, file => file.resolution) | ||
1798 | |||
1799 | return Object.assign(file, { Video: this }) | ||
1800 | } | ||
1801 | |||
1802 | // No webtorrent files, try with streaming playlist files | ||
1803 | if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) { | ||
1804 | const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this }) | ||
1805 | |||
1806 | const file = maxBy(streamingPlaylistWithVideo.VideoFiles, file => file.resolution) | ||
1807 | return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo }) | ||
1808 | } | ||
1792 | 1809 | ||
1793 | // The original file is the file that have the higher resolution | 1810 | return undefined |
1794 | return maxBy(this.VideoFiles, file => file.resolution) | ||
1795 | } | 1811 | } |
1796 | 1812 | ||
1797 | getFile <T extends MVideoWithFile> (this: T, resolution: number) { | 1813 | getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { |
1798 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1814 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1799 | 1815 | ||
1800 | return this.VideoFiles.find(f => f.resolution === resolution) | 1816 | const file = this.VideoFiles.find(f => f.resolution === resolution) |
1817 | if (!file) return undefined | ||
1818 | |||
1819 | return Object.assign(file, { Video: this }) | ||
1801 | } | 1820 | } |
1802 | 1821 | ||
1803 | async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { | 1822 | async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { |
@@ -1813,10 +1832,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1813 | this.Thumbnails.push(savedThumbnail) | 1832 | this.Thumbnails.push(savedThumbnail) |
1814 | } | 1833 | } |
1815 | 1834 | ||
1816 | getVideoFilename (videoFile: MVideoFile) { | ||
1817 | return this.uuid + '-' + videoFile.resolution + videoFile.extname | ||
1818 | } | ||
1819 | |||
1820 | generateThumbnailName () { | 1835 | generateThumbnailName () { |
1821 | return this.uuid + '.jpg' | 1836 | return this.uuid + '.jpg' |
1822 | } | 1837 | } |
@@ -1837,46 +1852,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1837 | return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) | 1852 | return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) |
1838 | } | 1853 | } |
1839 | 1854 | ||
1840 | getTorrentFileName (videoFile: MVideoFile) { | ||
1841 | const extension = '.torrent' | ||
1842 | return this.uuid + '-' + videoFile.resolution + extension | ||
1843 | } | ||
1844 | |||
1845 | isOwned () { | 1855 | isOwned () { |
1846 | return this.remote === false | 1856 | return this.remote === false |
1847 | } | 1857 | } |
1848 | 1858 | ||
1849 | getTorrentFilePath (videoFile: MVideoFile) { | ||
1850 | return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | ||
1851 | } | ||
1852 | |||
1853 | getVideoFilePath (videoFile: MVideoFile) { | ||
1854 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | ||
1855 | } | ||
1856 | |||
1857 | async createTorrentAndSetInfoHash (videoFile: MVideoFile) { | ||
1858 | const options = { | ||
1859 | // Keep the extname, it's used by the client to stream the file inside a web browser | ||
1860 | name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`, | ||
1861 | createdBy: 'PeerTube', | ||
1862 | announceList: [ | ||
1863 | [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ], | ||
1864 | [ WEBSERVER.URL + '/tracker/announce' ] | ||
1865 | ], | ||
1866 | urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] | ||
1867 | } | ||
1868 | |||
1869 | const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) | ||
1870 | |||
1871 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | ||
1872 | logger.info('Creating torrent %s.', filePath) | ||
1873 | |||
1874 | await writeFile(filePath, torrent) | ||
1875 | |||
1876 | const parsedTorrent = parseTorrent(torrent) | ||
1877 | videoFile.infoHash = parsedTorrent.infoHash | ||
1878 | } | ||
1879 | |||
1880 | getWatchStaticPath () { | 1859 | getWatchStaticPath () { |
1881 | return '/videos/watch/' + this.uuid | 1860 | return '/videos/watch/' + this.uuid |
1882 | } | 1861 | } |
@@ -1909,7 +1888,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1909 | } | 1888 | } |
1910 | 1889 | ||
1911 | getFormattedVideoFilesJSON (): VideoFile[] { | 1890 | getFormattedVideoFilesJSON (): VideoFile[] { |
1912 | return videoFilesModelToFormattedJSON(this, this.VideoFiles) | 1891 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() |
1892 | return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles) | ||
1913 | } | 1893 | } |
1914 | 1894 | ||
1915 | toActivityPubObject (this: MVideoAP): VideoTorrentObject { | 1895 | toActivityPubObject (this: MVideoAP): VideoTorrentObject { |
@@ -1923,8 +1903,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1923 | return peertubeTruncate(this.description, { length: maxLength }) | 1903 | return peertubeTruncate(this.description, { length: maxLength }) |
1924 | } | 1904 | } |
1925 | 1905 | ||
1926 | getOriginalFileResolution () { | 1906 | getMaxQualityResolution () { |
1927 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | 1907 | const file = this.getMaxQualityFile() |
1908 | const videoOrPlaylist = file.getVideoOrStreamingPlaylist() | ||
1909 | const originalFilePath = getVideoFilePath(videoOrPlaylist, file) | ||
1928 | 1910 | ||
1929 | return getVideoFileResolution(originalFilePath) | 1911 | return getVideoFileResolution(originalFilePath) |
1930 | } | 1912 | } |
@@ -1933,22 +1915,36 @@ export class VideoModel extends Model<VideoModel> { | |||
1933 | return `/api/${API_VERSION}/videos/${this.uuid}/description` | 1915 | return `/api/${API_VERSION}/videos/${this.uuid}/description` |
1934 | } | 1916 | } |
1935 | 1917 | ||
1936 | getHLSPlaylist () { | 1918 | getHLSPlaylist (): MStreamingPlaylistFilesVideo { |
1937 | if (!this.VideoStreamingPlaylists) return undefined | 1919 | if (!this.VideoStreamingPlaylists) return undefined |
1938 | 1920 | ||
1939 | return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | 1921 | const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
1922 | playlist.Video = this | ||
1923 | |||
1924 | return playlist | ||
1940 | } | 1925 | } |
1941 | 1926 | ||
1942 | removeFile (videoFile: MVideoFile, isRedundancy = false) { | 1927 | setHLSPlaylist (playlist: MStreamingPlaylist) { |
1943 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR | 1928 | const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ] |
1944 | 1929 | ||
1945 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | 1930 | if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) { |
1931 | this.VideoStreamingPlaylists = toAdd | ||
1932 | return | ||
1933 | } | ||
1934 | |||
1935 | this.VideoStreamingPlaylists = this.VideoStreamingPlaylists | ||
1936 | .filter(s => s.type !== VideoStreamingPlaylistType.HLS) | ||
1937 | .concat(toAdd) | ||
1938 | } | ||
1939 | |||
1940 | removeFile (videoFile: MVideoFile, isRedundancy = false) { | ||
1941 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) | ||
1946 | return remove(filePath) | 1942 | return remove(filePath) |
1947 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1943 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1948 | } | 1944 | } |
1949 | 1945 | ||
1950 | removeTorrent (videoFile: MVideoFile) { | 1946 | removeTorrent (videoFile: MVideoFile) { |
1951 | const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | 1947 | const torrentPath = getTorrentFilePath(this, videoFile) |
1952 | return remove(torrentPath) | 1948 | return remove(torrentPath) |
1953 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | 1949 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) |
1954 | } | 1950 | } |
@@ -1973,38 +1969,30 @@ export class VideoModel extends Model<VideoModel> { | |||
1973 | return this.save() | 1969 | return this.save() |
1974 | } | 1970 | } |
1975 | 1971 | ||
1976 | getBaseUrls () { | 1972 | async publishIfNeededAndSave (t: Transaction) { |
1977 | let baseUrlHttp | 1973 | if (this.state !== VideoState.PUBLISHED) { |
1978 | let baseUrlWs | 1974 | this.state = VideoState.PUBLISHED |
1975 | this.publishedAt = new Date() | ||
1976 | await this.save({ transaction: t }) | ||
1979 | 1977 | ||
1980 | if (this.isOwned()) { | 1978 | return true |
1981 | baseUrlHttp = WEBSERVER.URL | ||
1982 | baseUrlWs = WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT | ||
1983 | } else { | ||
1984 | baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host | ||
1985 | baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host | ||
1986 | } | 1979 | } |
1987 | 1980 | ||
1988 | return { baseUrlHttp, baseUrlWs } | 1981 | return false |
1989 | } | 1982 | } |
1990 | 1983 | ||
1991 | generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) { | 1984 | getBaseUrls () { |
1992 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) | 1985 | if (this.isOwned()) { |
1993 | const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs) | 1986 | return { |
1994 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] | 1987 | baseUrlHttp: WEBSERVER.URL, |
1995 | 1988 | baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT | |
1996 | const redundancies = videoFile.RedundancyVideos | 1989 | } |
1997 | if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) | ||
1998 | |||
1999 | const magnetHash = { | ||
2000 | xs, | ||
2001 | announce, | ||
2002 | urlList, | ||
2003 | infoHash: videoFile.infoHash, | ||
2004 | name: this.name | ||
2005 | } | 1990 | } |
2006 | 1991 | ||
2007 | return magnetUtil.encode(magnetHash) | 1992 | return { |
1993 | baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host, | ||
1994 | baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host | ||
1995 | } | ||
2008 | } | 1996 | } |
2009 | 1997 | ||
2010 | getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { | 1998 | getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { |
@@ -2012,23 +2000,23 @@ export class VideoModel extends Model<VideoModel> { | |||
2012 | } | 2000 | } |
2013 | 2001 | ||
2014 | getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) { | 2002 | getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) { |
2015 | return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) | 2003 | return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile) |
2016 | } | 2004 | } |
2017 | 2005 | ||
2018 | getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { | 2006 | getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { |
2019 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile) | 2007 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile) |
2020 | } | 2008 | } |
2021 | 2009 | ||
2022 | getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) { | 2010 | getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) { |
2023 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 2011 | return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile) |
2024 | } | 2012 | } |
2025 | 2013 | ||
2026 | getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) { | 2014 | getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) { |
2027 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | 2015 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile) |
2028 | } | 2016 | } |
2029 | 2017 | ||
2030 | getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { | 2018 | getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { |
2031 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 2019 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile) |
2032 | } | 2020 | } |
2033 | 2021 | ||
2034 | getBandwidthBits (videoFile: MVideoFile) { | 2022 | getBandwidthBits (videoFile: MVideoFile) { |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 9435bb1e8..3c558d4ea 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -92,6 +92,9 @@ describe('Test config API validators', function () { | |||
92 | '1080p': false, | 92 | '1080p': false, |
93 | '2160p': false | 93 | '2160p': false |
94 | }, | 94 | }, |
95 | webtorrent: { | ||
96 | enabled: true | ||
97 | }, | ||
95 | hls: { | 98 | hls: { |
96 | enabled: false | 99 | enabled: false |
97 | } | 100 | } |
@@ -235,6 +238,27 @@ describe('Test config API validators', function () { | |||
235 | }) | 238 | }) |
236 | }) | 239 | }) |
237 | 240 | ||
241 | it('Should fail with a disabled webtorrent & hls transcoding', async function () { | ||
242 | const newUpdateParams = immutableAssign(updateParams, { | ||
243 | transcoding: { | ||
244 | hls: { | ||
245 | enabled: false | ||
246 | }, | ||
247 | webtorrent: { | ||
248 | enabled: false | ||
249 | } | ||
250 | } | ||
251 | }) | ||
252 | |||
253 | await makePutBodyRequest({ | ||
254 | url: server.url, | ||
255 | path, | ||
256 | fields: newUpdateParams, | ||
257 | token: server.accessToken, | ||
258 | statusCodeExpected: 400 | ||
259 | }) | ||
260 | }) | ||
261 | |||
238 | it('Should success with the correct parameters', async function () { | 262 | it('Should success with the correct parameters', async function () { |
239 | await makePutBodyRequest({ | 263 | await makePutBodyRequest({ |
240 | url: server.url, | 264 | url: server.url, |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 97cc99eea..a494858b3 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -72,6 +72,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) { | |||
72 | expect(data.transcoding.resolutions['720p']).to.be.true | 72 | expect(data.transcoding.resolutions['720p']).to.be.true |
73 | expect(data.transcoding.resolutions['1080p']).to.be.true | 73 | expect(data.transcoding.resolutions['1080p']).to.be.true |
74 | expect(data.transcoding.resolutions['2160p']).to.be.true | 74 | expect(data.transcoding.resolutions['2160p']).to.be.true |
75 | expect(data.transcoding.webtorrent.enabled).to.be.true | ||
75 | expect(data.transcoding.hls.enabled).to.be.true | 76 | expect(data.transcoding.hls.enabled).to.be.true |
76 | 77 | ||
77 | expect(data.import.videos.http.enabled).to.be.true | 78 | expect(data.import.videos.http.enabled).to.be.true |
@@ -140,6 +141,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
140 | expect(data.transcoding.resolutions['1080p']).to.be.false | 141 | expect(data.transcoding.resolutions['1080p']).to.be.false |
141 | expect(data.transcoding.resolutions['2160p']).to.be.false | 142 | expect(data.transcoding.resolutions['2160p']).to.be.false |
142 | expect(data.transcoding.hls.enabled).to.be.false | 143 | expect(data.transcoding.hls.enabled).to.be.false |
144 | expect(data.transcoding.webtorrent.enabled).to.be.true | ||
143 | 145 | ||
144 | expect(data.import.videos.http.enabled).to.be.false | 146 | expect(data.import.videos.http.enabled).to.be.false |
145 | expect(data.import.videos.torrent.enabled).to.be.false | 147 | expect(data.import.videos.torrent.enabled).to.be.false |
@@ -279,6 +281,9 @@ describe('Test config', function () { | |||
279 | '1080p': false, | 281 | '1080p': false, |
280 | '2160p': false | 282 | '2160p': false |
281 | }, | 283 | }, |
284 | webtorrent: { | ||
285 | enabled: true | ||
286 | }, | ||
282 | hls: { | 287 | hls: { |
283 | enabled: false | 288 | enabled: false |
284 | } | 289 | } |
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts index 39178bb1a..289209177 100644 --- a/server/tests/api/videos/video-hls.ts +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -10,13 +10,13 @@ import { | |||
10 | doubleFollow, | 10 | doubleFollow, |
11 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | getPlaylist, | 12 | getPlaylist, |
13 | getVideo, | 13 | getVideo, makeGetRequest, makeRawRequest, |
14 | removeVideo, | 14 | removeVideo, |
15 | ServerInfo, | 15 | ServerInfo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, updateCustomSubConfig, |
17 | updateVideo, | 17 | updateVideo, |
18 | uploadVideo, | 18 | uploadVideo, |
19 | waitJobs | 19 | waitJobs, webtorrentAdd |
20 | } from '../../../../shared/extra-utils' | 20 | } from '../../../../shared/extra-utils' |
21 | import { VideoDetails } from '../../../../shared/models/videos' | 21 | import { VideoDetails } from '../../../../shared/models/videos' |
22 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | 22 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' |
@@ -25,20 +25,45 @@ import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' | |||
25 | 25 | ||
26 | const expect = chai.expect | 26 | const expect = chai.expect |
27 | 27 | ||
28 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resolutions = [ 240, 360, 480, 720 ]) { | 28 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOnly: boolean, resolutions = [ 240, 360, 480, 720 ]) { |
29 | for (const server of servers) { | 29 | for (const server of servers) { |
30 | const res = await getVideo(server.url, videoUUID) | 30 | const resVideoDetails = await getVideo(server.url, videoUUID) |
31 | const videoDetails: VideoDetails = res.body | 31 | const videoDetails: VideoDetails = resVideoDetails.body |
32 | const baseUrl = `http://${videoDetails.account.host}` | ||
32 | 33 | ||
33 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | 34 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) |
34 | 35 | ||
35 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | 36 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
36 | expect(hlsPlaylist).to.not.be.undefined | 37 | expect(hlsPlaylist).to.not.be.undefined |
37 | 38 | ||
39 | const hlsFiles = hlsPlaylist.files | ||
40 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
41 | |||
42 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | ||
43 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
44 | |||
45 | for (const resolution of resolutions) { | ||
46 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
47 | expect(file).to.not.be.undefined | ||
48 | |||
49 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
50 | expect(file.torrentUrl).to.equal(`${baseUrl}/static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`) | ||
51 | expect(file.fileUrl).to.equal(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`) | ||
52 | expect(file.resolution.label).to.equal(resolution + 'p') | ||
53 | |||
54 | await makeRawRequest(file.torrentUrl, 200) | ||
55 | await makeRawRequest(file.fileUrl, 200) | ||
56 | |||
57 | const torrent = await webtorrentAdd(file.magnetUri, true) | ||
58 | expect(torrent.files).to.be.an('array') | ||
59 | expect(torrent.files.length).to.equal(1) | ||
60 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | ||
61 | } | ||
62 | |||
38 | { | 63 | { |
39 | const res2 = await getPlaylist(hlsPlaylist.playlistUrl) | 64 | const res = await getPlaylist(hlsPlaylist.playlistUrl) |
40 | 65 | ||
41 | const masterPlaylist = res2.text | 66 | const masterPlaylist = res.text |
42 | 67 | ||
43 | for (const resolution of resolutions) { | 68 | for (const resolution of resolutions) { |
44 | expect(masterPlaylist).to.match(new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+')) | 69 | expect(masterPlaylist).to.match(new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+')) |
@@ -48,18 +73,18 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resol | |||
48 | 73 | ||
49 | { | 74 | { |
50 | for (const resolution of resolutions) { | 75 | for (const resolution of resolutions) { |
51 | const res2 = await getPlaylist(`http://localhost:${servers[0].port}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`) | 76 | const res = await getPlaylist(`${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`) |
52 | 77 | ||
53 | const subPlaylist = res2.text | 78 | const subPlaylist = res.text |
54 | expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) | 79 | expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) |
55 | } | 80 | } |
56 | } | 81 | } |
57 | 82 | ||
58 | { | 83 | { |
59 | const baseUrl = 'http://localhost:' + servers[0].port + '/static/streaming-playlists/hls' | 84 | const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls' |
60 | 85 | ||
61 | for (const resolution of resolutions) { | 86 | for (const resolution of resolutions) { |
62 | await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist) | 87 | await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist) |
63 | } | 88 | } |
64 | } | 89 | } |
65 | } | 90 | } |
@@ -70,84 +95,118 @@ describe('Test HLS videos', function () { | |||
70 | let videoUUID = '' | 95 | let videoUUID = '' |
71 | let videoAudioUUID = '' | 96 | let videoAudioUUID = '' |
72 | 97 | ||
73 | before(async function () { | 98 | function runTestSuite (hlsOnly: boolean) { |
74 | this.timeout(120000) | 99 | it('Should upload a video and transcode it to HLS', async function () { |
100 | this.timeout(120000) | ||
75 | 101 | ||
76 | const configOverride = { | 102 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) |
77 | transcoding: { | 103 | videoUUID = res.body.video.uuid |
78 | enabled: true, | ||
79 | allow_audio_files: true, | ||
80 | hls: { | ||
81 | enabled: true | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | servers = await flushAndRunMultipleServers(2, configOverride) | ||
86 | 104 | ||
87 | // Get the access tokens | 105 | await waitJobs(servers) |
88 | await setAccessTokensToServers(servers) | ||
89 | 106 | ||
90 | // Server 1 and server 2 follow each other | 107 | await checkHlsPlaylist(servers, videoUUID, hlsOnly) |
91 | await doubleFollow(servers[0], servers[1]) | 108 | }) |
92 | }) | ||
93 | 109 | ||
94 | it('Should upload a video and transcode it to HLS', async function () { | 110 | it('Should upload an audio file and transcode it to HLS', async function () { |
95 | this.timeout(120000) | 111 | this.timeout(120000) |
96 | 112 | ||
97 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) | 113 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' }) |
98 | videoUUID = res.body.video.uuid | 114 | videoAudioUUID = res.body.video.uuid |
99 | 115 | ||
100 | await waitJobs(servers) | 116 | await waitJobs(servers) |
101 | 117 | ||
102 | await checkHlsPlaylist(servers, videoUUID) | 118 | await checkHlsPlaylist(servers, videoAudioUUID, hlsOnly, [ DEFAULT_AUDIO_RESOLUTION ]) |
103 | }) | 119 | }) |
104 | 120 | ||
105 | it('Should upload an audio file and transcode it to HLS', async function () { | 121 | it('Should update the video', async function () { |
106 | this.timeout(120000) | 122 | this.timeout(10000) |
107 | 123 | ||
108 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' }) | 124 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, { name: 'video 1 updated' }) |
109 | videoAudioUUID = res.body.video.uuid | ||
110 | 125 | ||
111 | await waitJobs(servers) | 126 | await waitJobs(servers) |
112 | 127 | ||
113 | await checkHlsPlaylist(servers, videoAudioUUID, [ DEFAULT_AUDIO_RESOLUTION ]) | 128 | await checkHlsPlaylist(servers, videoUUID, hlsOnly) |
114 | }) | 129 | }) |
115 | 130 | ||
116 | it('Should update the video', async function () { | 131 | it('Should delete videos', async function () { |
117 | this.timeout(10000) | 132 | this.timeout(10000) |
118 | 133 | ||
119 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) | 134 | await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID) |
135 | await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAudioUUID) | ||
120 | 136 | ||
121 | await waitJobs(servers) | 137 | await waitJobs(servers) |
122 | 138 | ||
123 | await checkHlsPlaylist(servers, videoUUID) | 139 | for (const server of servers) { |
124 | }) | 140 | await getVideo(server.url, videoUUID, 404) |
141 | await getVideo(server.url, videoAudioUUID, 404) | ||
142 | } | ||
143 | }) | ||
125 | 144 | ||
126 | it('Should delete videos', async function () { | 145 | it('Should have the playlists/segment deleted from the disk', async function () { |
127 | this.timeout(10000) | 146 | for (const server of servers) { |
147 | await checkDirectoryIsEmpty(server, 'videos') | ||
148 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls')) | ||
149 | } | ||
150 | }) | ||
128 | 151 | ||
129 | await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) | 152 | it('Should have an empty tmp directory', async function () { |
130 | await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID) | 153 | for (const server of servers) { |
154 | await checkTmpIsEmpty(server) | ||
155 | } | ||
156 | }) | ||
157 | } | ||
131 | 158 | ||
132 | await waitJobs(servers) | 159 | before(async function () { |
160 | this.timeout(120000) | ||
133 | 161 | ||
134 | for (const server of servers) { | 162 | const configOverride = { |
135 | await getVideo(server.url, videoUUID, 404) | 163 | transcoding: { |
136 | await getVideo(server.url, videoAudioUUID, 404) | 164 | enabled: true, |
165 | allow_audio_files: true, | ||
166 | hls: { | ||
167 | enabled: true | ||
168 | } | ||
169 | } | ||
137 | } | 170 | } |
171 | servers = await flushAndRunMultipleServers(2, configOverride) | ||
172 | |||
173 | // Get the access tokens | ||
174 | await setAccessTokensToServers(servers) | ||
175 | |||
176 | // Server 1 and server 2 follow each other | ||
177 | await doubleFollow(servers[0], servers[1]) | ||
138 | }) | 178 | }) |
139 | 179 | ||
140 | it('Should have the playlists/segment deleted from the disk', async function () { | 180 | describe('With WebTorrent & HLS enabled', function () { |
141 | for (const server of servers) { | 181 | runTestSuite(false) |
142 | await checkDirectoryIsEmpty(server, 'videos') | ||
143 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls')) | ||
144 | } | ||
145 | }) | 182 | }) |
146 | 183 | ||
147 | it('Should have an empty tmp directory', async function () { | 184 | describe('With only HLS enabled', function () { |
148 | for (const server of servers) { | 185 | |
149 | await checkTmpIsEmpty(server) | 186 | before(async function () { |
150 | } | 187 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { |
188 | transcoding: { | ||
189 | enabled: true, | ||
190 | allowAudioFiles: true, | ||
191 | resolutions: { | ||
192 | '240p': true, | ||
193 | '360p': true, | ||
194 | '480p': true, | ||
195 | '720p': true, | ||
196 | '1080p': true, | ||
197 | '2160p': true | ||
198 | }, | ||
199 | hls: { | ||
200 | enabled: true | ||
201 | }, | ||
202 | webtorrent: { | ||
203 | enabled: false | ||
204 | } | ||
205 | } | ||
206 | }) | ||
207 | }) | ||
208 | |||
209 | runTestSuite(true) | ||
151 | }) | 210 | }) |
152 | 211 | ||
153 | after(async function () { | 212 | after(async function () { |
diff --git a/server/tests/cli/create-import-video-file-job.ts b/server/tests/cli/create-import-video-file-job.ts index 0d378c1aa..aca3216bb 100644 --- a/server/tests/cli/create-import-video-file-job.ts +++ b/server/tests/cli/create-import-video-file-job.ts | |||
@@ -2,22 +2,21 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoDetails, VideoFile } from '../../../shared/models/videos' | 5 | import { VideoDetails } from '../../../shared/models/videos' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | doubleFollow, | 8 | doubleFollow, |
9 | execCLI, | 9 | execCLI, |
10 | flushAndRunMultipleServers, | 10 | flushAndRunMultipleServers, |
11 | flushTests, | ||
12 | getEnvCli, | 11 | getEnvCli, |
13 | getVideo, | 12 | getVideo, |
14 | getVideosList, | 13 | getVideosList, |
15 | killallServers, | ||
16 | ServerInfo, | 14 | ServerInfo, |
17 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
18 | uploadVideo | 16 | uploadVideo |
19 | } from '../../../shared/extra-utils' | 17 | } from '../../../shared/extra-utils' |
20 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' | 18 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' |
19 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
21 | 20 | ||
22 | const expect = chai.expect | 21 | const expect = chai.expect |
23 | 22 | ||
diff --git a/server/typings/models/account/account.ts b/server/typings/models/account/account.ts index ec78fece8..adb1f3689 100644 --- a/server/typings/models/account/account.ts +++ b/server/typings/models/account/account.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | } from './actor' | 15 | } from './actor' |
16 | import { FunctionProperties, PickWith } from '../../utils' | 16 | import { FunctionProperties, PickWith } from '../../utils' |
17 | import { MAccountBlocklistId } from './account-blocklist' | 17 | import { MAccountBlocklistId } from './account-blocklist' |
18 | import { MChannelDefault } from '@server/typings/models' | 18 | import { MChannelDefault } from '../video/video-channels' |
19 | 19 | ||
20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> | 20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> |
21 | 21 | ||
diff --git a/server/typings/models/account/actor-follow.ts b/server/typings/models/account/actor-follow.ts index 1c66eb0a0..f44157eba 100644 --- a/server/typings/models/account/actor-follow.ts +++ b/server/typings/models/account/actor-follow.ts | |||
@@ -1,17 +1,16 @@ | |||
1 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 1 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
2 | import { | 2 | import { |
3 | MActor, | 3 | MActor, |
4 | MActorAccount, | ||
5 | MActorDefaultAccountChannel, | ||
6 | MActorChannelAccountActor, | 4 | MActorChannelAccountActor, |
7 | MActorDefault, | 5 | MActorDefault, |
6 | MActorDefaultAccountChannel, | ||
8 | MActorFormattable, | 7 | MActorFormattable, |
9 | MActorHost, | 8 | MActorHost, |
10 | MActorUsername | 9 | MActorUsername |
11 | } from './actor' | 10 | } from './actor' |
12 | import { PickWith } from '../../utils' | 11 | import { PickWith } from '../../utils' |
13 | import { ActorModel } from '@server/models/activitypub/actor' | 12 | import { ActorModel } from '@server/models/activitypub/actor' |
14 | import { MChannelDefault } from '@server/typings/models' | 13 | import { MChannelDefault } from '../video/video-channels' |
15 | 14 | ||
16 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> | 15 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> |
17 | 16 | ||
diff --git a/server/typings/models/account/index.d.ts b/server/typings/models/account/index.ts index 513c09c40..513c09c40 100644 --- a/server/typings/models/account/index.d.ts +++ b/server/typings/models/account/index.ts | |||
diff --git a/server/typings/models/index.d.ts b/server/typings/models/index.ts index 78b4948ce..78b4948ce 100644 --- a/server/typings/models/index.d.ts +++ b/server/typings/models/index.ts | |||
diff --git a/server/typings/models/oauth/index.d.ts b/server/typings/models/oauth/index.ts index 36b7ea8ca..36b7ea8ca 100644 --- a/server/typings/models/oauth/index.d.ts +++ b/server/typings/models/oauth/index.ts | |||
diff --git a/server/typings/models/oauth/oauth-token.ts b/server/typings/models/oauth/oauth-token.ts index af3412925..8ef042d4e 100644 --- a/server/typings/models/oauth/oauth-token.ts +++ b/server/typings/models/oauth/oauth-token.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 1 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
2 | import { PickWith } from '@server/typings/utils' | 2 | import { PickWith } from '@server/typings/utils' |
3 | import { MUserAccountUrl } from '@server/typings/models' | 3 | import { MUserAccountUrl } from '../user/user' |
4 | 4 | ||
5 | type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M> | 5 | type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M> |
6 | 6 | ||
diff --git a/server/typings/models/server/index.d.ts b/server/typings/models/server/index.ts index c853795ad..c853795ad 100644 --- a/server/typings/models/server/index.d.ts +++ b/server/typings/models/server/index.ts | |||
diff --git a/server/typings/models/server/server-blocklist.ts b/server/typings/models/server/server-blocklist.ts index c81f604f5..c3e6230f2 100644 --- a/server/typings/models/server/server-blocklist.ts +++ b/server/typings/models/server/server-blocklist.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | 1 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' |
2 | import { PickWith } from '@server/typings/utils' | 2 | import { PickWith } from '@server/typings/utils' |
3 | import { MAccountDefault, MAccountFormattable, MServer, MServerFormattable } from '@server/typings/models' | 3 | import { MAccountDefault, MAccountFormattable } from '../account/account' |
4 | import { MServer, MServerFormattable } from './server' | ||
4 | 5 | ||
5 | type Use<K extends keyof ServerBlocklistModel, M> = PickWith<ServerBlocklistModel, K, M> | 6 | type Use<K extends keyof ServerBlocklistModel, M> = PickWith<ServerBlocklistModel, K, M> |
6 | 7 | ||
diff --git a/server/typings/models/user/index.d.ts b/server/typings/models/user/index.ts index 6657b2128..6657b2128 100644 --- a/server/typings/models/user/index.d.ts +++ b/server/typings/models/user/index.ts | |||
diff --git a/server/typings/models/user/user.ts b/server/typings/models/user/user.ts index 52d6d4a05..a2750adc7 100644 --- a/server/typings/models/user/user.ts +++ b/server/typings/models/user/user.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | } from '../account' | 11 | } from '../account' |
12 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' | 12 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' |
13 | import { AccountModel } from '@server/models/account/account' | 13 | import { AccountModel } from '@server/models/account/account' |
14 | import { MChannelFormattable } from '@server/typings/models' | 14 | import { MChannelFormattable } from '../video/video-channels' |
15 | 15 | ||
16 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> | 16 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> |
17 | 17 | ||
diff --git a/server/typings/models/video/index.d.ts b/server/typings/models/video/index.ts index bd69c8a4b..bd69c8a4b 100644 --- a/server/typings/models/video/index.d.ts +++ b/server/typings/models/video/index.ts | |||
diff --git a/server/typings/models/video/schedule-video-update.ts b/server/typings/models/video/schedule-video-update.ts index ada9af06e..e6f478cdf 100644 --- a/server/typings/models/video/schedule-video-update.ts +++ b/server/typings/models/video/schedule-video-update.ts | |||
@@ -1,9 +1,18 @@ | |||
1 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 1 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
2 | import { PickWith } from '@server/typings/utils' | ||
3 | import { MVideoAPWithoutCaption, MVideoWithBlacklistLight } from './video' | ||
4 | |||
5 | type Use<K extends keyof ScheduleVideoUpdateModel, M> = PickWith<ScheduleVideoUpdateModel, K, M> | ||
6 | |||
7 | // ############################################################################ | ||
2 | 8 | ||
3 | export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'> | 9 | export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'> |
4 | 10 | ||
5 | // ############################################################################ | 11 | // ############################################################################ |
6 | 12 | ||
13 | export type MScheduleVideoUpdateVideoAll = MScheduleVideoUpdate & | ||
14 | Use<'Video', MVideoAPWithoutCaption & MVideoWithBlacklistLight> | ||
15 | |||
7 | // Format for API or AP object | 16 | // Format for API or AP object |
8 | 17 | ||
9 | export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'> | 18 | export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'> |
diff --git a/server/typings/models/video/video-blacklist.ts b/server/typings/models/video/video-blacklist.ts index e12880454..7122a9dc0 100644 --- a/server/typings/models/video/video-blacklist.ts +++ b/server/typings/models/video/video-blacklist.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 1 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' |
2 | import { PickWith } from '@server/typings/utils' | 2 | import { PickWith } from '@server/typings/utils' |
3 | import { MVideo, MVideoFormattable } from '@server/typings/models' | 3 | import { MVideo, MVideoFormattable } from './video' |
4 | 4 | ||
5 | type Use<K extends keyof VideoBlacklistModel, M> = PickWith<VideoBlacklistModel, K, M> | 5 | type Use<K extends keyof VideoBlacklistModel, M> = PickWith<VideoBlacklistModel, K, M> |
6 | 6 | ||
diff --git a/server/typings/models/video/video-caption.ts b/server/typings/models/video/video-caption.ts index 7cb2a2ad3..ffa56f544 100644 --- a/server/typings/models/video/video-caption.ts +++ b/server/typings/models/video/video-caption.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { VideoCaptionModel } from '../../../models/video/video-caption' | 1 | import { VideoCaptionModel } from '../../../models/video/video-caption' |
2 | import { FunctionProperties, PickWith } from '@server/typings/utils' | 2 | import { FunctionProperties, PickWith } from '@server/typings/utils' |
3 | import { MVideo, MVideoUUID } from '@server/typings/models' | 3 | import { MVideo, MVideoUUID } from './video' |
4 | 4 | ||
5 | type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M> | 5 | type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M> |
6 | 6 | ||
diff --git a/server/typings/models/video/video-change-ownership.ts b/server/typings/models/video/video-change-ownership.ts index 72634cdb2..e5b5bbc1d 100644 --- a/server/typings/models/video/video-change-ownership.ts +++ b/server/typings/models/video/video-change-ownership.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership' | 1 | import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership' |
2 | import { PickWith } from '@server/typings/utils' | 2 | import { PickWith } from '@server/typings/utils' |
3 | import { MAccountDefault, MAccountFormattable, MVideo, MVideoWithFileThumbnail } from '@server/typings/models' | 3 | import { MAccountDefault, MAccountFormattable } from '../account/account' |
4 | import { MVideo, MVideoWithAllFiles } from './video' | ||
4 | 5 | ||
5 | type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwnershipModel, K, M> | 6 | type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwnershipModel, K, M> |
6 | 7 | ||
@@ -11,7 +12,7 @@ export type MVideoChangeOwnership = Omit<VideoChangeOwnershipModel, 'Initiator' | |||
11 | export type MVideoChangeOwnershipFull = MVideoChangeOwnership & | 12 | export type MVideoChangeOwnershipFull = MVideoChangeOwnership & |
12 | Use<'Initiator', MAccountDefault> & | 13 | Use<'Initiator', MAccountDefault> & |
13 | Use<'NextOwner', MAccountDefault> & | 14 | Use<'NextOwner', MAccountDefault> & |
14 | Use<'Video', MVideoWithFileThumbnail> | 15 | Use<'Video', MVideoWithAllFiles> |
15 | 16 | ||
16 | // ############################################################################ | 17 | // ############################################################################ |
17 | 18 | ||
diff --git a/server/typings/models/video/video-comment.ts b/server/typings/models/video/video-comment.ts index 4fd1c29e8..d693f9186 100644 --- a/server/typings/models/video/video-comment.ts +++ b/server/typings/models/video/video-comment.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { VideoCommentModel } from '../../../models/video/video-comment' | 1 | import { VideoCommentModel } from '../../../models/video/video-comment' |
2 | import { PickWith, PickWithOpt } from '../../utils' | 2 | import { PickWith, PickWithOpt } from '../../utils' |
3 | import { MAccountDefault, MAccountFormattable, MAccountUrl, MActorUrl } from '../account' | 3 | import { MAccountDefault, MAccountFormattable, MAccountUrl } from '../account' |
4 | import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video' | 4 | import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video' |
5 | 5 | ||
6 | type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M> | 6 | type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M> |
diff --git a/server/typings/models/video/video-file.ts b/server/typings/models/video/video-file.ts index 484351a8d..352fe3d32 100644 --- a/server/typings/models/video/video-file.ts +++ b/server/typings/models/video/video-file.ts | |||
@@ -2,18 +2,33 @@ import { VideoFileModel } from '../../../models/video/video-file' | |||
2 | import { PickWith, PickWithOpt } from '../../utils' | 2 | import { PickWith, PickWithOpt } from '../../utils' |
3 | import { MVideo, MVideoUUID } from './video' | 3 | import { MVideo, MVideoUUID } from './video' |
4 | import { MVideoRedundancyFileUrl } from './video-redundancy' | 4 | import { MVideoRedundancyFileUrl } from './video-redundancy' |
5 | import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist' | ||
5 | 6 | ||
6 | type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M> | 7 | type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M> |
7 | 8 | ||
8 | // ############################################################################ | 9 | // ############################################################################ |
9 | 10 | ||
10 | export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos'> | 11 | export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos' | 'VideoStreamingPlaylist'> |
11 | 12 | ||
12 | export type MVideoFileVideo = MVideoFile & | 13 | export type MVideoFileVideo = MVideoFile & |
13 | Use<'Video', MVideo> | 14 | Use<'Video', MVideo> |
14 | 15 | ||
16 | export type MVideoFileStreamingPlaylist = MVideoFile & | ||
17 | Use<'VideoStreamingPlaylist', MStreamingPlaylist> | ||
18 | |||
19 | export type MVideoFileStreamingPlaylistVideo = MVideoFile & | ||
20 | Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo> | ||
21 | |||
15 | export type MVideoFileVideoUUID = MVideoFile & | 22 | export type MVideoFileVideoUUID = MVideoFile & |
16 | Use<'Video', MVideoUUID> | 23 | Use<'Video', MVideoUUID> |
17 | 24 | ||
18 | export type MVideoFileRedundanciesOpt = MVideoFile & | 25 | export type MVideoFileRedundanciesOpt = MVideoFile & |
19 | PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> | 26 | PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> |
27 | |||
28 | export function isStreamingPlaylistFile (file: any): file is MVideoFileStreamingPlaylist { | ||
29 | return !!file.videoStreamingPlaylistId | ||
30 | } | ||
31 | |||
32 | export function isWebtorrentFile (file: any): file is MVideoFileVideo { | ||
33 | return !!file.videoId | ||
34 | } | ||
diff --git a/server/typings/models/video/video-import.ts b/server/typings/models/video/video-import.ts index c6a1c5b66..e119f17f9 100644 --- a/server/typings/models/video/video-import.ts +++ b/server/typings/models/video/video-import.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { VideoImportModel } from '@server/models/video/video-import' | 1 | import { VideoImportModel } from '@server/models/video/video-import' |
2 | import { PickWith, PickWithOpt } from '@server/typings/utils' | 2 | import { PickWith, PickWithOpt } from '@server/typings/utils' |
3 | import { MUser, MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from '@server/typings/models' | 3 | import { MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from './video' |
4 | import { MUser } from '../user/user' | ||
4 | 5 | ||
5 | type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M> | 6 | type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M> |
6 | 7 | ||
diff --git a/server/typings/models/video/video-playlist-element.ts b/server/typings/models/video/video-playlist-element.ts index 7b1b993ce..1aeff78d8 100644 --- a/server/typings/models/video/video-playlist-element.ts +++ b/server/typings/models/video/video-playlist-element.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | 1 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' |
2 | import { PickWith } from '@server/typings/utils' | 2 | import { PickWith } from '@server/typings/utils' |
3 | import { MVideoFormattable, MVideoPlaylistPrivacy, MVideoThumbnail, MVideoUrl } from '@server/typings/models' | 3 | import { MVideoFormattable, MVideoThumbnail, MVideoUrl } from './video' |
4 | import { MVideoPlaylistPrivacy } from './video-playlist' | ||
4 | 5 | ||
5 | type Use<K extends keyof VideoPlaylistElementModel, M> = PickWith<VideoPlaylistElementModel, K, M> | 6 | type Use<K extends keyof VideoPlaylistElementModel, M> = PickWith<VideoPlaylistElementModel, K, M> |
6 | 7 | ||
diff --git a/server/typings/models/video/video-rate.ts b/server/typings/models/video/video-rate.ts index 2ff8a625b..f6bb527fc 100644 --- a/server/typings/models/video/video-rate.ts +++ b/server/typings/models/video/video-rate.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AccountVideoRateModel } from '@server/models/account/account-video-rate' | 1 | import { AccountVideoRateModel } from '@server/models/account/account-video-rate' |
2 | import { PickWith } from '@server/typings/utils' | 2 | import { PickWith } from '@server/typings/utils' |
3 | import { MAccountAudience, MAccountUrl, MVideo, MVideoFormattable } from '..' | 3 | import { MAccountAudience, MAccountUrl } from '../account/account' |
4 | import { MVideo, MVideoFormattable } from './video' | ||
4 | 5 | ||
5 | type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateModel, K, M> | 6 | type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateModel, K, M> |
6 | 7 | ||
diff --git a/server/typings/models/video/video-redundancy.ts b/server/typings/models/video/video-redundancy.ts index f3846afd7..25bdac057 100644 --- a/server/typings/models/video/video-redundancy.ts +++ b/server/typings/models/video/video-redundancy.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 1 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
2 | import { PickWith, PickWithOpt } from '@server/typings/utils' | 2 | import { PickWith, PickWithOpt } from '@server/typings/utils' |
3 | import { MStreamingPlaylistVideo, MVideoFile, MVideoFileVideo, MVideoUrl } from '@server/typings/models' | ||
4 | import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model' | ||
5 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 3 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
6 | import { VideoFile } from '../../../../shared/models/videos' | ||
7 | import { VideoFileModel } from '@server/models/video/video-file' | 4 | import { VideoFileModel } from '@server/models/video/video-file' |
5 | import { MVideoFile, MVideoFileVideo } from './video-file' | ||
6 | import { MStreamingPlaylistVideo } from './video-streaming-playlist' | ||
7 | import { MVideoUrl } from './video' | ||
8 | 8 | ||
9 | type Use<K extends keyof VideoRedundancyModel, M> = PickWith<VideoRedundancyModel, K, M> | 9 | type Use<K extends keyof VideoRedundancyModel, M> = PickWith<VideoRedundancyModel, K, M> |
10 | 10 | ||
diff --git a/server/typings/models/video/video-streaming-playlist.ts b/server/typings/models/video/video-streaming-playlist.ts index 79696bcff..436c0c072 100644 --- a/server/typings/models/video/video-streaming-playlist.ts +++ b/server/typings/models/video/video-streaming-playlist.ts | |||
@@ -1,19 +1,33 @@ | |||
1 | import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist' | 1 | import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist' |
2 | import { PickWith, PickWithOpt } from '../../utils' | 2 | import { PickWith, PickWithOpt } from '../../utils' |
3 | import { MVideoRedundancyFileUrl } from './video-redundancy' | 3 | import { MVideoRedundancyFileUrl } from './video-redundancy' |
4 | import { MVideo, MVideoUrl } from '@server/typings/models' | 4 | import { MVideo } from './video' |
5 | import { MVideoFile } from './video-file' | ||
5 | 6 | ||
6 | type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreamingPlaylistModel, K, M> | 7 | type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreamingPlaylistModel, K, M> |
7 | 8 | ||
8 | // ############################################################################ | 9 | // ############################################################################ |
9 | 10 | ||
10 | export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos'> | 11 | export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos' | 'VideoFiles'> |
12 | |||
13 | export type MStreamingPlaylistFiles = MStreamingPlaylist & | ||
14 | Use<'VideoFiles', MVideoFile[]> | ||
11 | 15 | ||
12 | export type MStreamingPlaylistVideo = MStreamingPlaylist & | 16 | export type MStreamingPlaylistVideo = MStreamingPlaylist & |
13 | Use<'Video', MVideo> | 17 | Use<'Video', MVideo> |
14 | 18 | ||
19 | export type MStreamingPlaylistFilesVideo = MStreamingPlaylist & | ||
20 | Use<'VideoFiles', MVideoFile[]> & | ||
21 | Use<'Video', MVideo> | ||
22 | |||
15 | export type MStreamingPlaylistRedundancies = MStreamingPlaylist & | 23 | export type MStreamingPlaylistRedundancies = MStreamingPlaylist & |
24 | Use<'VideoFiles', MVideoFile[]> & | ||
16 | Use<'RedundancyVideos', MVideoRedundancyFileUrl[]> | 25 | Use<'RedundancyVideos', MVideoRedundancyFileUrl[]> |
17 | 26 | ||
18 | export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist & | 27 | export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist & |
28 | Use<'VideoFiles', MVideoFile[]> & | ||
19 | PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> | 29 | PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> |
30 | |||
31 | export function isStreamingPlaylist (value: MVideo | MStreamingPlaylistVideo): value is MStreamingPlaylistVideo { | ||
32 | return !!(value as MStreamingPlaylist).playlistUrl | ||
33 | } | ||
diff --git a/server/typings/models/video/video.ts b/server/typings/models/video/video.ts index 9a53bd337..7f69a91de 100644 --- a/server/typings/models/video/video.ts +++ b/server/typings/models/video/video.ts | |||
@@ -10,7 +10,7 @@ import { | |||
10 | } from './video-channels' | 10 | } from './video-channels' |
11 | import { MTag } from './tag' | 11 | import { MTag } from './tag' |
12 | import { MVideoCaptionLanguage } from './video-caption' | 12 | import { MVideoCaptionLanguage } from './video-caption' |
13 | import { MStreamingPlaylist, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist' | 13 | import { MStreamingPlaylistFiles, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist' |
14 | import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file' | 14 | import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file' |
15 | import { MThumbnail } from './thumbnail' | 15 | import { MThumbnail } from './thumbnail' |
16 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' | 16 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' |
@@ -40,7 +40,8 @@ export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'> | |||
40 | 40 | ||
41 | // "With" to not confuse with the VideoFile model | 41 | // "With" to not confuse with the VideoFile model |
42 | export type MVideoWithFile = MVideo & | 42 | export type MVideoWithFile = MVideo & |
43 | Use<'VideoFiles', MVideoFile[]> | 43 | Use<'VideoFiles', MVideoFile[]> & |
44 | Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> | ||
44 | 45 | ||
45 | export type MVideoThumbnail = MVideo & | 46 | export type MVideoThumbnail = MVideo & |
46 | Use<'Thumbnails', MThumbnail[]> | 47 | Use<'Thumbnails', MThumbnail[]> |
@@ -66,7 +67,7 @@ export type MVideoWithCaptions = MVideo & | |||
66 | Use<'VideoCaptions', MVideoCaptionLanguage[]> | 67 | Use<'VideoCaptions', MVideoCaptionLanguage[]> |
67 | 68 | ||
68 | export type MVideoWithStreamingPlaylist = MVideo & | 69 | export type MVideoWithStreamingPlaylist = MVideo & |
69 | Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> | 70 | Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> |
70 | 71 | ||
71 | // ############################################################################ | 72 | // ############################################################################ |
72 | 73 | ||
@@ -93,12 +94,12 @@ export type MVideoWithRights = MVideo & | |||
93 | export type MVideoWithAllFiles = MVideo & | 94 | export type MVideoWithAllFiles = MVideo & |
94 | Use<'VideoFiles', MVideoFile[]> & | 95 | Use<'VideoFiles', MVideoFile[]> & |
95 | Use<'Thumbnails', MThumbnail[]> & | 96 | Use<'Thumbnails', MThumbnail[]> & |
96 | Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> | 97 | Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> |
97 | 98 | ||
98 | export type MVideoAccountLightBlacklistAllFiles = MVideo & | 99 | export type MVideoAccountLightBlacklistAllFiles = MVideo & |
99 | Use<'VideoFiles', MVideoFile[]> & | 100 | Use<'VideoFiles', MVideoFile[]> & |
100 | Use<'Thumbnails', MThumbnail[]> & | 101 | Use<'Thumbnails', MThumbnail[]> & |
101 | Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> & | 102 | Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> & |
102 | Use<'VideoChannel', MChannelAccountLight> & | 103 | Use<'VideoChannel', MChannelAccountLight> & |
103 | Use<'VideoBlacklist', MVideoBlacklistLight> | 104 | Use<'VideoBlacklist', MVideoBlacklistLight> |
104 | 105 | ||
@@ -124,7 +125,7 @@ export type MVideoFullLight = MVideo & | |||
124 | Use<'UserVideoHistories', MUserVideoHistoryTime[]> & | 125 | Use<'UserVideoHistories', MUserVideoHistoryTime[]> & |
125 | Use<'VideoFiles', MVideoFile[]> & | 126 | Use<'VideoFiles', MVideoFile[]> & |
126 | Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> & | 127 | Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> & |
127 | Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> | 128 | Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> |
128 | 129 | ||
129 | // ############################################################################ | 130 | // ############################################################################ |
130 | 131 | ||
@@ -133,10 +134,11 @@ export type MVideoFullLight = MVideo & | |||
133 | export type MVideoAP = MVideo & | 134 | export type MVideoAP = MVideo & |
134 | Use<'Tags', MTag[]> & | 135 | Use<'Tags', MTag[]> & |
135 | Use<'VideoChannel', MChannelAccountLight> & | 136 | Use<'VideoChannel', MChannelAccountLight> & |
136 | Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> & | 137 | Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> & |
137 | Use<'VideoCaptions', MVideoCaptionLanguage[]> & | 138 | Use<'VideoCaptions', MVideoCaptionLanguage[]> & |
138 | Use<'VideoBlacklist', MVideoBlacklistUnfederated> & | 139 | Use<'VideoBlacklist', MVideoBlacklistUnfederated> & |
139 | Use<'VideoFiles', MVideoFileRedundanciesOpt[]> | 140 | Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & |
141 | Use<'Thumbnails', MThumbnail[]> | ||
140 | 142 | ||
141 | export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> | 143 | export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> |
142 | 144 | ||