aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/index.ts7
-rw-r--r--server/controllers/download.ts78
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/lazy-static.ts23
-rw-r--r--server/controllers/static.ts88
-rw-r--r--server/helpers/activitypub.ts12
-rw-r--r--server/helpers/webtorrent.ts57
-rw-r--r--server/initializers/checker-before-init.ts2
-rw-r--r--server/initializers/config.ts3
-rw-r--r--server/initializers/constants.ts13
-rw-r--r--server/lib/activitypub/videos.ts54
-rw-r--r--server/lib/files-cache/abstract-video-static-file-cache.ts2
-rw-r--r--server/lib/files-cache/videos-torrent-cache.ts54
-rw-r--r--server/lib/hls.ts4
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts14
-rw-r--r--server/lib/job-queue/handlers/video-import.ts8
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts2
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts2
-rw-r--r--server/lib/live-manager.ts6
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts12
-rw-r--r--server/lib/video-paths.ts75
-rw-r--r--server/lib/video-transcoding.ts33
-rw-r--r--server/models/video/thumbnail.ts4
-rw-r--r--server/models/video/video-caption.ts6
-rw-r--r--server/models/video/video-file.ts181
-rw-r--r--server/models/video/video-format-utils.ts64
-rw-r--r--server/models/video/video-query-builder.ts8
-rw-r--r--server/models/video/video-streaming-playlist.ts58
-rw-r--r--server/models/video/video.ts60
-rw-r--r--server/tests/api/videos/video-hls.ts2
-rw-r--r--server/tests/cli/create-import-video-file-job.ts16
-rw-r--r--server/types/models/video/video-channels.ts5
-rw-r--r--server/types/models/video/video.ts23
33 files changed, 593 insertions, 384 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 9504c40a4..dcd6194ae 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -7,7 +7,7 @@ import { changeVideoChannelShare } from '@server/lib/activitypub/share'
7import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 7import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
8import { LiveManager } from '@server/lib/live-manager' 8import { LiveManager } from '@server/lib/live-manager'
9import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' 9import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
10import { getVideoFilePath } from '@server/lib/video-paths' 10import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
11import { getServerActor } from '@server/models/application/application' 11import { getServerActor } from '@server/models/application/application'
12import { MVideoFullLight } from '@server/types/models' 12import { MVideoFullLight } from '@server/types/models'
13import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' 13import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared'
@@ -189,6 +189,7 @@ async function addVideo (req: express.Request, res: express.Response) {
189 videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware 189 videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware
190 190
191 const video = new VideoModel(videoData) as MVideoFullLight 191 const video = new VideoModel(videoData) as MVideoFullLight
192 video.VideoChannel = res.locals.videoChannel
192 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 193 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
193 194
194 const videoFile = new VideoFileModel({ 195 const videoFile = new VideoFileModel({
@@ -205,6 +206,8 @@ async function addVideo (req: express.Request, res: express.Response) {
205 videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution 206 videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
206 } 207 }
207 208
209 videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
210
208 // Move physical file 211 // Move physical file
209 const destination = getVideoFilePath(video, videoFile) 212 const destination = getVideoFilePath(video, videoFile)
210 await move(videoPhysicalFile.path, destination) 213 await move(videoPhysicalFile.path, destination)
@@ -219,7 +222,7 @@ async function addVideo (req: express.Request, res: express.Response) {
219 }) 222 })
220 223
221 // Create the torrent file 224 // Create the torrent file
222 await createTorrentAndSetInfoHash(video, videoFile) 225 await createTorrentAndSetInfoHash(video, video, videoFile)
223 226
224 const { videoCreated } = await sequelizeTypescript.transaction(async t => { 227 const { videoCreated } = await sequelizeTypescript.transaction(async t => {
225 const sequelizeOptions = { transaction: t } 228 const sequelizeOptions = { transaction: t }
diff --git a/server/controllers/download.ts b/server/controllers/download.ts
new file mode 100644
index 000000000..27caa1518
--- /dev/null
+++ b/server/controllers/download.ts
@@ -0,0 +1,78 @@
1import * as cors from 'cors'
2import * as express from 'express'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
4import { getVideoFilePath } from '@server/lib/video-paths'
5import { MVideoFile, MVideoFullLight } from '@server/types/models'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7import { VideoStreamingPlaylistType } from '@shared/models'
8import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
9import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
10
11const downloadRouter = express.Router()
12
13downloadRouter.use(cors())
14
15downloadRouter.use(
16 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
17 downloadTorrent
18)
19
20downloadRouter.use(
21 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
22 asyncMiddleware(videosDownloadValidator),
23 downloadVideoFile
24)
25
26downloadRouter.use(
27 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
28 asyncMiddleware(videosDownloadValidator),
29 downloadHLSVideoFile
30)
31
32// ---------------------------------------------------------------------------
33
34export {
35 downloadRouter
36}
37
38// ---------------------------------------------------------------------------
39
40async function downloadTorrent (req: express.Request, res: express.Response) {
41 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
42 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
43
44 return res.download(result.path, result.downloadName)
45}
46
47function downloadVideoFile (req: express.Request, res: express.Response) {
48 const video = res.locals.videoAll
49
50 const videoFile = getVideoFile(req, video.VideoFiles)
51 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
52
53 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
54}
55
56function downloadHLSVideoFile (req: express.Request, res: express.Response) {
57 const video = res.locals.videoAll
58 const playlist = getHLSPlaylist(video)
59 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end
60
61 const videoFile = getVideoFile(req, playlist.VideoFiles)
62 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
63
64 const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
65 return res.download(getVideoFilePath(playlist, videoFile), filename)
66}
67
68function getVideoFile (req: express.Request, files: MVideoFile[]) {
69 const resolution = parseInt(req.params.resolution, 10)
70 return files.find(f => f.resolution === resolution)
71}
72
73function getHLSPlaylist (video: MVideoFullLight) {
74 const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
75 if (!playlist) return undefined
76
77 return Object.assign(playlist, { Video: video })
78}
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 5a199ae9c..fa27ecec2 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -1,6 +1,7 @@
1export * from './activitypub' 1export * from './activitypub'
2export * from './api' 2export * from './api'
3export * from './client' 3export * from './client'
4export * from './download'
4export * from './feeds' 5export * from './feeds'
5export * from './services' 6export * from './services'
6export * from './static' 7export * from './static'
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 656dea223..c2f5c7b56 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -1,12 +1,13 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger'
3import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar'
4import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
5import { asyncMiddleware } from '../middlewares' 9import { asyncMiddleware } from '../middlewares'
6import { AvatarModel } from '../models/avatar/avatar' 10import { AvatarModel } from '../models/avatar/avatar'
7import { logger } from '../helpers/logger'
8import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar'
9import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
10 11
11const lazyStaticRouter = express.Router() 12const lazyStaticRouter = express.Router()
12 13
@@ -27,6 +28,11 @@ lazyStaticRouter.use(
27 asyncMiddleware(getVideoCaption) 28 asyncMiddleware(getVideoCaption)
28) 29)
29 30
31lazyStaticRouter.use(
32 LAZY_STATIC_PATHS.TORRENTS + ':filename',
33 asyncMiddleware(getTorrent)
34)
35
30// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
31 37
32export { 38export {
@@ -67,19 +73,26 @@ async function getAvatar (req: express.Request, res: express.Response) {
67 const path = avatar.getPath() 73 const path = avatar.getPath()
68 74
69 avatarPathUnsafeCache.set(filename, path) 75 avatarPathUnsafeCache.set(filename, path)
70 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) 76 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
71} 77}
72 78
73async function getPreview (req: express.Request, res: express.Response) { 79async function getPreview (req: express.Request, res: express.Response) {
74 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) 80 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
75 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 81 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
76 82
77 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) 83 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
78} 84}
79 85
80async function getVideoCaption (req: express.Request, res: express.Response) { 86async function getVideoCaption (req: express.Request, res: express.Response) {
81 const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) 87 const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename)
82 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 88 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
83 89
90 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
91}
92
93async function getTorrent (req: express.Request, res: express.Response) {
94 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
95 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
96
84 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) 97 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
85} 98}
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 2064857eb..7cc7f2c62 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -3,10 +3,7 @@ import * as express from 'express'
3import { join } from 'path' 3import { join } from 'path'
4import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config' 4import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
5import { serveIndexHTML } from '@server/lib/client-html' 5import { serveIndexHTML } from '@server/lib/client-html'
6import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
7import { MVideoFile, MVideoFullLight } from '@server/types/models'
8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
9import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
10import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' 7import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
11import { root } from '../helpers/core-utils' 8import { root } from '../helpers/core-utils'
12import { CONFIG, isEmailEnabled } from '../initializers/config' 9import { CONFIG, isEmailEnabled } from '../initializers/config'
@@ -16,14 +13,13 @@ import {
16 HLS_STREAMING_PLAYLIST_DIRECTORY, 13 HLS_STREAMING_PLAYLIST_DIRECTORY,
17 PEERTUBE_VERSION, 14 PEERTUBE_VERSION,
18 ROUTE_CACHE_LIFETIME, 15 ROUTE_CACHE_LIFETIME,
19 STATIC_DOWNLOAD_PATHS,
20 STATIC_MAX_AGE, 16 STATIC_MAX_AGE,
21 STATIC_PATHS, 17 STATIC_PATHS,
22 WEBSERVER 18 WEBSERVER
23} from '../initializers/constants' 19} from '../initializers/constants'
24import { getThemeOrDefault } from '../lib/plugins/theme-utils' 20import { getThemeOrDefault } from '../lib/plugins/theme-utils'
25import { getEnabledResolutions } from '../lib/video-transcoding' 21import { getEnabledResolutions } from '../lib/video-transcoding'
26import { asyncMiddleware, videosDownloadValidator } from '../middlewares' 22import { asyncMiddleware } from '../middlewares'
27import { cacheRoute } from '../middlewares/cache' 23import { cacheRoute } from '../middlewares/cache'
28import { UserModel } from '../models/account/user' 24import { UserModel } from '../models/account/user'
29import { VideoModel } from '../models/video/video' 25import { VideoModel } from '../models/video/video'
@@ -37,47 +33,23 @@ staticRouter.use(cors())
37 Cors is very important to let other servers access torrent and video files 33 Cors is very important to let other servers access torrent and video files
38*/ 34*/
39 35
36// FIXME: deprecated in 3.2, use lazy-statics instead
40const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR 37const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR
41staticRouter.use( 38staticRouter.use(
42 STATIC_PATHS.TORRENTS, 39 STATIC_PATHS.TORRENTS,
43 cors(),
44 express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file 40 express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file
45) 41)
46staticRouter.use(
47 STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent',
48 asyncMiddleware(videosDownloadValidator),
49 downloadTorrent
50)
51staticRouter.use(
52 STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
53 asyncMiddleware(videosDownloadValidator),
54 downloadHLSVideoFileTorrent
55)
56 42
57// Videos path for webseeding 43// Videos path for webseed
58staticRouter.use( 44staticRouter.use(
59 STATIC_PATHS.WEBSEED, 45 STATIC_PATHS.WEBSEED,
60 cors(),
61 express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video 46 express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
62) 47)
63staticRouter.use( 48staticRouter.use(
64 STATIC_PATHS.REDUNDANCY, 49 STATIC_PATHS.REDUNDANCY,
65 cors(),
66 express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video 50 express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
67) 51)
68 52
69staticRouter.use(
70 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
71 asyncMiddleware(videosDownloadValidator),
72 downloadVideoFile
73)
74
75staticRouter.use(
76 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
77 asyncMiddleware(videosDownloadValidator),
78 downloadHLSVideoFile
79)
80
81// HLS 53// HLS
82staticRouter.use( 54staticRouter.use(
83 STATIC_PATHS.STREAMING_PLAYLISTS.HLS, 55 STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
@@ -327,60 +299,6 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
327 return res.send(json).end() 299 return res.send(json).end()
328} 300}
329 301
330function downloadTorrent (req: express.Request, res: express.Response) {
331 const video = res.locals.videoAll
332
333 const videoFile = getVideoFile(req, video.VideoFiles)
334 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
335
336 return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
337}
338
339function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) {
340 const video = res.locals.videoAll
341
342 const playlist = getHLSPlaylist(video)
343 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end
344
345 const videoFile = getVideoFile(req, playlist.VideoFiles)
346 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
347
348 return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
349}
350
351function downloadVideoFile (req: express.Request, res: express.Response) {
352 const video = res.locals.videoAll
353
354 const videoFile = getVideoFile(req, video.VideoFiles)
355 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
356
357 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
358}
359
360function downloadHLSVideoFile (req: express.Request, res: express.Response) {
361 const video = res.locals.videoAll
362 const playlist = getHLSPlaylist(video)
363 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end
364
365 const videoFile = getVideoFile(req, playlist.VideoFiles)
366 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
367
368 const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
369 return res.download(getVideoFilePath(playlist, videoFile), filename)
370}
371
372function getVideoFile (req: express.Request, files: MVideoFile[]) {
373 const resolution = parseInt(req.params.resolution, 10)
374 return files.find(f => f.resolution === resolution)
375}
376
377function getHLSPlaylist (video: MVideoFullLight) {
378 const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
379 if (!playlist) return undefined
380
381 return Object.assign(playlist, { Video: video })
382}
383
384function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { 302function getCup (req: express.Request, res: express.Response, next: express.NextFunction) {
385 res.status(HttpStatusCode.I_AM_A_TEAPOT_418) 303 res.status(HttpStatusCode.I_AM_A_TEAPOT_418)
386 res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1') 304 res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1')
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 1188d6cf9..02a9d4026 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -1,13 +1,13 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { URL } from 'url'
2import validator from 'validator' 3import validator from 'validator'
4import { ContextType } from '@shared/models/activitypub/context'
3import { ResultList } from '../../shared/models' 5import { ResultList } from '../../shared/models'
4import { Activity } from '../../shared/models/activitypub' 6import { Activity } from '../../shared/models/activitypub'
5import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' 7import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
6import { signJsonLDObject } from './peertube-crypto' 8import { MActor, MVideoWithHost } from '../types/models'
7import { pageToStartAndCount } from './core-utils' 9import { pageToStartAndCount } from './core-utils'
8import { URL } from 'url' 10import { signJsonLDObject } from './peertube-crypto'
9import { MActor, MVideoAccountLight } from '../types/models'
10import { ContextType } from '@shared/models/activitypub/context'
11 11
12function getContextData (type: ContextType) { 12function getContextData (type: ContextType) {
13 const context: any[] = [ 13 const context: any[] = [
@@ -201,8 +201,8 @@ function checkUrlsSameHost (url1: string, url2: string) {
201 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase() 201 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
202} 202}
203 203
204function buildRemoteVideoBaseUrl (video: MVideoAccountLight, path: string) { 204function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string) {
205 const host = video.VideoChannel.Account.Actor.Server.host 205 const host = video.VideoChannel.Actor.Server.host
206 206
207 return REMOTE_SCHEME.HTTP + '://' + host + path 207 return REMOTE_SCHEME.HTTP + '://' + host + path
208} 208}
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index 9c5df2083..73418aa0a 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -1,20 +1,19 @@
1import { logger } from './logger'
2import { generateVideoImportTmpPath } from './utils'
3import * as WebTorrent from 'webtorrent'
4import { createWriteStream, ensureDir, remove, writeFile } from 'fs-extra'
5import { CONFIG } from '../initializers/config'
6import { dirname, join } from 'path'
7import * as createTorrent from 'create-torrent' 1import * as createTorrent from 'create-torrent'
8import { promisify2 } from './core-utils' 2import { createWriteStream, ensureDir, remove, writeFile } from 'fs-extra'
9import { MVideo } from '@server/types/models/video/video'
10import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/types/models/video/video-file'
11import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/types/models/video/video-streaming-playlist'
12import { WEBSERVER } from '@server/initializers/constants'
13import * as parseTorrent from 'parse-torrent'
14import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent'
5import { dirname, join } from 'path'
6import * as WebTorrent from 'webtorrent'
15import { isArray } from '@server/helpers/custom-validators/misc' 7import { isArray } from '@server/helpers/custom-validators/misc'
16import { getTorrentFileName, getVideoFilePath } from '@server/lib/video-paths' 8import { WEBSERVER } from '@server/initializers/constants'
17import { extractVideo } from '@server/helpers/video' 9import { generateTorrentFileName, getVideoFilePath } from '@server/lib/video-paths'
10import { MVideo, MVideoWithHost } from '@server/types/models/video/video'
11import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/types/models/video/video-file'
12import { MStreamingPlaylistVideo } from '@server/types/models/video/video-streaming-playlist'
13import { CONFIG } from '../initializers/config'
14import { promisify2 } from './core-utils'
15import { logger } from './logger'
16import { generateVideoImportTmpPath } from './utils'
18 17
19const createTorrentPromise = promisify2<string, any, any>(createTorrent) 18const createTorrentPromise = promisify2<string, any, any>(createTorrent)
20 19
@@ -78,10 +77,12 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
78 }) 77 })
79} 78}
80 79
81async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { 80// FIXME: refactor/merge videoOrPlaylist and video arguments
82 const video = extractVideo(videoOrPlaylist) 81async function createTorrentAndSetInfoHash (
83 const { baseUrlHttp } = video.getBaseUrls() 82 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
84 83 video: MVideoWithHost,
84 videoFile: MVideoFile
85) {
85 const options = { 86 const options = {
86 // Keep the extname, it's used by the client to stream the file inside a web browser 87 // Keep the extname, it's used by the client to stream the file inside a web browser
87 name: `${video.name} ${videoFile.resolution}p${videoFile.extname}`, 88 name: `${video.name} ${videoFile.resolution}p${videoFile.extname}`,
@@ -90,33 +91,33 @@ async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreaming
90 [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ], 91 [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
91 [ WEBSERVER.URL + '/tracker/announce' ] 92 [ WEBSERVER.URL + '/tracker/announce' ]
92 ], 93 ],
93 urlList: [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ] 94 urlList: [ videoFile.getFileUrl(video) ]
94 } 95 }
95 96
96 const torrent = await createTorrentPromise(getVideoFilePath(videoOrPlaylist, videoFile), options) 97 const torrent = await createTorrentPromise(getVideoFilePath(videoOrPlaylist, videoFile), options)
97 98
98 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile)) 99 const torrentFilename = generateTorrentFileName(videoOrPlaylist, videoFile.resolution)
99 logger.info('Creating torrent %s.', filePath) 100 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, torrentFilename)
101 logger.info('Creating torrent %s.', torrentPath)
100 102
101 await writeFile(filePath, torrent) 103 await writeFile(torrentPath, torrent)
102 104
103 const parsedTorrent = parseTorrent(torrent) 105 const parsedTorrent = parseTorrent(torrent)
104 videoFile.infoHash = parsedTorrent.infoHash 106 videoFile.infoHash = parsedTorrent.infoHash
107 videoFile.torrentFilename = torrentFilename
105} 108}
106 109
110// FIXME: merge/refactor videoOrPlaylist and video arguments
107function generateMagnetUri ( 111function generateMagnetUri (
108 videoOrPlaylist: MVideo | MStreamingPlaylistVideo, 112 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
113 video: MVideoWithHost,
109 videoFile: MVideoFileRedundanciesOpt, 114 videoFile: MVideoFileRedundanciesOpt,
110 baseUrlHttp: string, 115 baseUrlHttp: string,
111 baseUrlWs: string 116 baseUrlWs: string
112) { 117) {
113 const video = isStreamingPlaylist(videoOrPlaylist) 118 const xs = videoFile.getTorrentUrl()
114 ? videoOrPlaylist.Video
115 : videoOrPlaylist
116
117 const xs = videoOrPlaylist.getTorrentUrl(videoFile, baseUrlHttp)
118 const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs) 119 const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs)
119 let urlList = [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ] 120 let urlList = [ videoFile.getFileUrl(video) ]
120 121
121 const redundancies = videoFile.RedundancyVideos 122 const redundancies = videoFile.RedundancyVideos
122 if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) 123 if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index a186afbdd..2578de5ed 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -17,7 +17,7 @@ function checkMissedConfig () {
17 'log.level', 17 'log.level',
18 'user.video_quota', 'user.video_quota_daily', 18 'user.video_quota', 'user.video_quota_daily',
19 'csp.enabled', 'csp.report_only', 'csp.report_uri', 19 'csp.enabled', 'csp.report_only', 'csp.report_uri',
20 'cache.previews.size', 'admin.email', 'contact_form.enabled', 20 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled',
21 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', 21 'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
22 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 22 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
23 'redundancy.videos.strategies', 'redundancy.videos.check_interval', 23 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 930fd784e..21ca78584 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -266,6 +266,9 @@ const CONFIG = {
266 }, 266 },
267 VIDEO_CAPTIONS: { 267 VIDEO_CAPTIONS: {
268 get SIZE () { return config.get<number>('cache.captions.size') } 268 get SIZE () { return config.get<number>('cache.captions.size') }
269 },
270 TORRENTS: {
271 get SIZE () { return config.get<number>('cache.torrents.size') }
269 } 272 }
270 }, 273 },
271 INSTANCE: { 274 INSTANCE: {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index be5db8fe8..6b0984186 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -551,16 +551,13 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
551 551
552// Express static paths (router) 552// Express static paths (router)
553const STATIC_PATHS = { 553const STATIC_PATHS = {
554 PREVIEWS: '/static/previews/',
555 THUMBNAILS: '/static/thumbnails/', 554 THUMBNAILS: '/static/thumbnails/',
556 TORRENTS: '/static/torrents/', 555 TORRENTS: '/static/torrents/',
557 WEBSEED: '/static/webseed/', 556 WEBSEED: '/static/webseed/',
558 REDUNDANCY: '/static/redundancy/', 557 REDUNDANCY: '/static/redundancy/',
559 STREAMING_PLAYLISTS: { 558 STREAMING_PLAYLISTS: {
560 HLS: '/static/streaming-playlists/hls' 559 HLS: '/static/streaming-playlists/hls'
561 }, 560 }
562 AVATARS: '/static/avatars/',
563 VIDEO_CAPTIONS: '/static/video-captions/'
564} 561}
565const STATIC_DOWNLOAD_PATHS = { 562const STATIC_DOWNLOAD_PATHS = {
566 TORRENTS: '/download/torrents/', 563 TORRENTS: '/download/torrents/',
@@ -570,12 +567,14 @@ const STATIC_DOWNLOAD_PATHS = {
570const LAZY_STATIC_PATHS = { 567const LAZY_STATIC_PATHS = {
571 AVATARS: '/lazy-static/avatars/', 568 AVATARS: '/lazy-static/avatars/',
572 PREVIEWS: '/lazy-static/previews/', 569 PREVIEWS: '/lazy-static/previews/',
573 VIDEO_CAPTIONS: '/lazy-static/video-captions/' 570 VIDEO_CAPTIONS: '/lazy-static/video-captions/',
571 TORRENTS: '/lazy-static/torrents/'
574} 572}
575 573
576// Cache control 574// Cache control
577const STATIC_MAX_AGE = { 575const STATIC_MAX_AGE = {
578 SERVER: '2h', 576 SERVER: '2h',
577 LAZY_SERVER: '2d',
579 CLIENT: '30d' 578 CLIENT: '30d'
580} 579}
581 580
@@ -609,6 +608,10 @@ const FILES_CACHE = {
609 VIDEO_CAPTIONS: { 608 VIDEO_CAPTIONS: {
610 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'), 609 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'),
611 MAX_AGE: 1000 * 3600 * 3 // 3 hours 610 MAX_AGE: 1000 * 3600 * 3 // 3 hours
611 },
612 TORRENTS: {
613 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'torrents'),
614 MAX_AGE: 1000 * 3600 * 3 // 3 hours
612 } 615 }
613} 616}
614 617
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 66981f43f..a5f6537eb 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,7 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { join } from 'path' 4import { basename, join } from 'path'
5import * as request from 'request' 5import * as request from 'request'
6import * as sequelize from 'sequelize' 6import * as sequelize from 'sequelize'
7import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
@@ -30,11 +30,11 @@ import { doRequest } from '../../helpers/requests'
30import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 30import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
31import { 31import {
32 ACTIVITY_PUB, 32 ACTIVITY_PUB,
33 LAZY_STATIC_PATHS,
33 MIMETYPES, 34 MIMETYPES,
34 P2P_MEDIA_LOADER_PEER_VERSION, 35 P2P_MEDIA_LOADER_PEER_VERSION,
35 PREVIEWS_SIZE, 36 PREVIEWS_SIZE,
36 REMOTE_SCHEME, 37 REMOTE_SCHEME,
37 STATIC_PATHS,
38 THUMBNAILS_SIZE 38 THUMBNAILS_SIZE
39} from '../../initializers/constants' 39} from '../../initializers/constants'
40import { sequelizeTypescript } from '../../initializers/database' 40import { sequelizeTypescript } from '../../initializers/database'
@@ -51,6 +51,8 @@ import {
51 MChannelDefault, 51 MChannelDefault,
52 MChannelId, 52 MChannelId,
53 MStreamingPlaylist, 53 MStreamingPlaylist,
54 MStreamingPlaylistFilesVideo,
55 MStreamingPlaylistVideo,
54 MVideo, 56 MVideo,
55 MVideoAccountLight, 57 MVideoAccountLight,
56 MVideoAccountLightBlacklistAllFiles, 58 MVideoAccountLightBlacklistAllFiles,
@@ -61,7 +63,8 @@ import {
61 MVideoFullLight, 63 MVideoFullLight,
62 MVideoId, 64 MVideoId,
63 MVideoImmutable, 65 MVideoImmutable,
64 MVideoThumbnail 66 MVideoThumbnail,
67 MVideoWithHost
65} from '../../types/models' 68} from '../../types/models'
66import { MThumbnail } from '../../types/models/video/thumbnail' 69import { MThumbnail } from '../../types/models/video/thumbnail'
67import { FilteredModelAttributes } from '../../types/sequelize' 70import { FilteredModelAttributes } from '../../types/sequelize'
@@ -72,6 +75,7 @@ import { PeerTubeSocket } from '../peertube-socket'
72import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail' 75import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
73import { setVideoTags } from '../video' 76import { setVideoTags } from '../video'
74import { autoBlacklistVideoIfNeeded } from '../video-blacklist' 77import { autoBlacklistVideoIfNeeded } from '../video-blacklist'
78import { generateTorrentFileName } from '../video-paths'
75import { getOrCreateActorAndServerAndModel } from './actor' 79import { getOrCreateActorAndServerAndModel } from './actor'
76import { crawlCollectionPage } from './crawl' 80import { crawlCollectionPage } from './crawl'
77import { sendCreateVideo, sendUpdateVideo } from './send' 81import { sendCreateVideo, sendUpdateVideo } from './send'
@@ -405,7 +409,8 @@ async function updateVideoFromAP (options: {
405 409
406 for (const playlistAttributes of streamingPlaylistAttributes) { 410 for (const playlistAttributes of streamingPlaylistAttributes) {
407 const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t }) 411 const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t })
408 .then(([ streamingPlaylist ]) => streamingPlaylist) 412 .then(([ streamingPlaylist ]) => streamingPlaylist as MStreamingPlaylistFilesVideo)
413 streamingPlaylistModel.Video = videoUpdated
409 414
410 const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject) 415 const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject)
411 .map(a => new VideoFileModel(a)) 416 .map(a => new VideoFileModel(a))
@@ -637,13 +642,14 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
637 videoCreated.VideoStreamingPlaylists = [] 642 videoCreated.VideoStreamingPlaylists = []
638 643
639 for (const playlistAttributes of streamingPlaylistsAttributes) { 644 for (const playlistAttributes of streamingPlaylistsAttributes) {
640 const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) 645 const playlist = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) as MStreamingPlaylistFilesVideo
646 playlist.Video = videoCreated
641 647
642 const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject) 648 const playlistFiles = videoFileActivityUrlToDBAttributes(playlist, playlistAttributes.tagAPObject)
643 const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t })) 649 const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
644 playlistModel.VideoFiles = await Promise.all(videoFilePromises) 650 playlist.VideoFiles = await Promise.all(videoFilePromises)
645 651
646 videoCreated.VideoStreamingPlaylists.push(playlistModel) 652 videoCreated.VideoStreamingPlaylists.push(playlist)
647 } 653 }
648 654
649 // Process tags 655 // Process tags
@@ -766,7 +772,7 @@ function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObjec
766} 772}
767 773
768function videoFileActivityUrlToDBAttributes ( 774function videoFileActivityUrlToDBAttributes (
769 videoOrPlaylist: MVideo | MStreamingPlaylist, 775 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
770 urls: (ActivityTagObject | ActivityUrlObject)[] 776 urls: (ActivityTagObject | ActivityUrlObject)[]
771) { 777) {
772 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] 778 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
@@ -786,6 +792,10 @@ function videoFileActivityUrlToDBAttributes (
786 throw new Error('Cannot parse magnet URI ' + magnet.href) 792 throw new Error('Cannot parse magnet URI ' + magnet.href)
787 } 793 }
788 794
795 const torrentUrl = Array.isArray(parsed.xs)
796 ? parsed.xs[0]
797 : parsed.xs
798
789 // Fetch associated metadata url, if any 799 // Fetch associated metadata url, if any
790 const metadata = urls.filter(isAPVideoFileMetadataObject) 800 const metadata = urls.filter(isAPVideoFileMetadataObject)
791 .find(u => { 801 .find(u => {
@@ -794,18 +804,30 @@ function videoFileActivityUrlToDBAttributes (
794 u.rel.includes(fileUrl.mediaType) 804 u.rel.includes(fileUrl.mediaType)
795 }) 805 })
796 806
797 const mediaType = fileUrl.mediaType 807 const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
808 const resolution = fileUrl.height
809 const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id
810 const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
811
798 const attribute = { 812 const attribute = {
799 extname: getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, mediaType), 813 extname,
800 infoHash: parsed.infoHash, 814 infoHash: parsed.infoHash,
801 resolution: fileUrl.height, 815 resolution,
802 size: fileUrl.size, 816 size: fileUrl.size,
803 fps: fileUrl.fps || -1, 817 fps: fileUrl.fps || -1,
804 metadataUrl: metadata?.href, 818 metadataUrl: metadata?.href,
805 819
820 // Use the name of the remote file because we don't proxify video file requests
821 filename: basename(fileUrl.href),
822 fileUrl: fileUrl.href,
823
824 torrentUrl,
825 // Use our own torrent name since we proxify torrent requests
826 torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution),
827
806 // This is a video file owned by a video or by a streaming playlist 828 // This is a video file owned by a video or by a streaming playlist
807 videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id, 829 videoId,
808 videoStreamingPlaylistId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null 830 videoStreamingPlaylistId
809 } 831 }
810 832
811 attributes.push(attribute) 833 attributes.push(attribute)
@@ -862,8 +884,8 @@ function getPreviewFromIcons (videoObject: VideoObject) {
862 return maxBy(validIcons, 'width') 884 return maxBy(validIcons, 'width')
863} 885}
864 886
865function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoAccountLight) { 887function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
866 return previewIcon 888 return previewIcon
867 ? previewIcon.url 889 ? previewIcon.url
868 : buildRemoteVideoBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 890 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
869} 891}
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/abstract-video-static-file-cache.ts
index c06355446..af66689a0 100644
--- a/server/lib/files-cache/abstract-video-static-file-cache.ts
+++ b/server/lib/files-cache/abstract-video-static-file-cache.ts
@@ -2,7 +2,7 @@ import { remove } from 'fs-extra'
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3import * as memoizee from 'memoizee' 3import * as memoizee from 'memoizee'
4 4
5type GetFilePathResult = { isOwned: boolean, path: string } | undefined 5type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined
6 6
7export abstract class AbstractVideoStaticFileCache <T> { 7export abstract class AbstractVideoStaticFileCache <T> {
8 8
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts
new file mode 100644
index 000000000..ca0e1770d
--- /dev/null
+++ b/server/lib/files-cache/videos-torrent-cache.ts
@@ -0,0 +1,54 @@
1import { join } from 'path'
2import { doRequestAndSaveToFile } from '@server/helpers/requests'
3import { VideoFileModel } from '@server/models/video/video-file'
4import { CONFIG } from '../../initializers/config'
5import { FILES_CACHE } from '../../initializers/constants'
6import { VideoModel } from '../../models/video/video'
7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
8
9class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
10
11 private static instance: VideosTorrentCache
12
13 private constructor () {
14 super()
15 }
16
17 static get Instance () {
18 return this.instance || (this.instance = new this())
19 }
20
21 async getFilePathImpl (filename: string) {
22 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
23 if (!file) return undefined
24
25 if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) }
26
27 return this.loadRemoteFile(filename)
28 }
29
30 // Key is the torrent filename
31 protected async loadRemoteFile (key: string) {
32 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key)
33 if (!file) return undefined
34
35 if (file.getVideo().isOwned()) throw new Error('Cannot load remote file of owned video.')
36
37 // Used to fetch the path
38 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(file.getVideo().id)
39 if (!video) return undefined
40
41 const remoteUrl = file.getRemoteTorrentUrl(video)
42 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename)
43
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
45
46 const downloadName = `${video.name}-${file.resolution}p.torrent`
47
48 return { isOwned: false, path: destPath, downloadName }
49 }
50}
51
52export {
53 VideosTorrentCache
54}
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index ef489097a..04187668c 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -12,7 +12,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from
12import { sequelizeTypescript } from '../initializers/database' 12import { sequelizeTypescript } from '../initializers/database'
13import { VideoFileModel } from '../models/video/video-file' 13import { VideoFileModel } from '../models/video/video-file'
14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
15import { getVideoFilename, getVideoFilePath } from './video-paths' 15import { getVideoFilePath } from './video-paths'
16 16
17async function updateStreamingPlaylistsInfohashesIfNeeded () { 17async function updateStreamingPlaylistsInfohashesIfNeeded () {
18 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() 18 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
@@ -93,7 +93,7 @@ async function updateSha256VODSegments (video: MVideoWithFile) {
93 } 93 }
94 await close(fd) 94 await close(fd)
95 95
96 const videoFilename = getVideoFilename(hlsPlaylist, file) 96 const videoFilename = file.filename
97 json[videoFilename] = rangeHashes 97 json[videoFilename] = rangeHashes
98 } 98 }
99 99
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index cd95aa075..86c9b5c29 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -2,9 +2,9 @@ import * as Bull from 'bull'
2import { copy, stat } from 'fs-extra' 2import { copy, stat } from 'fs-extra'
3import { extname } from 'path' 3import { extname } from 'path'
4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
5import { getVideoFilePath } from '@server/lib/video-paths' 5import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
6import { UserModel } from '@server/models/account/user' 6import { UserModel } from '@server/models/account/user'
7import { MVideoFile, MVideoWithFile } from '@server/types/models' 7import { MVideoFile, MVideoFullLight } from '@server/types/models'
8import { VideoFileImportPayload } from '@shared/models' 8import { VideoFileImportPayload } from '@shared/models'
9import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 9import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
10import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
@@ -50,14 +50,16 @@ export {
50 50
51// --------------------------------------------------------------------------- 51// ---------------------------------------------------------------------------
52 52
53async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) { 53async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
54 const { videoFileResolution } = await getVideoFileResolution(inputFilePath) 54 const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
55 const { size } = await stat(inputFilePath) 55 const { size } = await stat(inputFilePath)
56 const fps = await getVideoFileFPS(inputFilePath) 56 const fps = await getVideoFileFPS(inputFilePath)
57 57
58 const fileExt = extname(inputFilePath)
58 let updatedVideoFile = new VideoFileModel({ 59 let updatedVideoFile = new VideoFileModel({
59 resolution: videoFileResolution, 60 resolution: videoFileResolution,
60 extname: extname(inputFilePath), 61 extname: fileExt,
62 filename: generateVideoFilename(video, false, videoFileResolution, fileExt),
61 size, 63 size,
62 fps, 64 fps,
63 videoId: video.id 65 videoId: video.id
@@ -68,7 +70,7 @@ async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
68 if (currentVideoFile) { 70 if (currentVideoFile) {
69 // Remove old file and old torrent 71 // Remove old file and old torrent
70 await video.removeFile(currentVideoFile) 72 await video.removeFile(currentVideoFile)
71 await video.removeTorrent(currentVideoFile) 73 await currentVideoFile.removeTorrent()
72 // Remove the old video file from the array 74 // Remove the old video file from the array
73 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) 75 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
74 76
@@ -83,7 +85,7 @@ async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
83 const outputPath = getVideoFilePath(video, updatedVideoFile) 85 const outputPath = getVideoFilePath(video, updatedVideoFile)
84 await copy(inputFilePath, outputPath) 86 await copy(inputFilePath, outputPath)
85 87
86 await createTorrentAndSetInfoHash(video, updatedVideoFile) 88 await createTorrentAndSetInfoHash(video, video, updatedVideoFile)
87 89
88 await updatedVideoFile.save() 90 await updatedVideoFile.save()
89 91
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 0d00c1b9d..8fa024105 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -6,7 +6,7 @@ import { isPostImportVideoAccepted } from '@server/lib/moderation'
6import { Hooks } from '@server/lib/plugins/hooks' 6import { Hooks } from '@server/lib/plugins/hooks'
7import { isAbleToUploadVideo } from '@server/lib/user' 7import { isAbleToUploadVideo } from '@server/lib/user'
8import { addOptimizeOrMergeAudioJob } from '@server/lib/video' 8import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
9import { getVideoFilePath } from '@server/lib/video-paths' 9import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
10import { ThumbnailModel } from '@server/models/video/thumbnail' 10import { ThumbnailModel } from '@server/models/video/thumbnail'
11import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import' 11import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import'
12import { 12import {
@@ -116,10 +116,12 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
116 const duration = await getDurationFromVideoFile(tempVideoPath) 116 const duration = await getDurationFromVideoFile(tempVideoPath)
117 117
118 // Prepare video file object for creation in database 118 // Prepare video file object for creation in database
119 const fileExt = extname(tempVideoPath)
119 const videoFileData = { 120 const videoFileData = {
120 extname: extname(tempVideoPath), 121 extname: fileExt,
121 resolution: videoFileResolution, 122 resolution: videoFileResolution,
122 size: stats.size, 123 size: stats.size,
124 filename: generateVideoFilename(videoImport.Video, false, videoFileResolution, fileExt),
123 fps, 125 fps,
124 videoId: videoImport.videoId 126 videoId: videoImport.videoId
125 } 127 }
@@ -183,7 +185,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
183 } 185 }
184 186
185 // Create torrent 187 // Create torrent
186 await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile) 188 await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoImportWithFiles.Video, videoFile)
187 189
188 const videoFileSave = videoFile.toJSON() 190 const videoFileSave = videoFile.toJSON()
189 191
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index 6d50635bb..d57202ca5 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -85,7 +85,7 @@ async function saveLive (video: MVideo, live: MVideoLive) {
85 await video.save() 85 await video.save()
86 86
87 // Remove old HLS playlist video files 87 // Remove old HLS playlist video files
88 const videoWithFiles = await VideoModel.loadWithFiles(video.id) 88 const videoWithFiles = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
89 89
90 const hlsPlaylist = videoWithFiles.getHLSPlaylist() 90 const hlsPlaylist = videoWithFiles.getHLSPlaylist()
91 await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) 91 await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index e248b645e..8573d4d12 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -128,7 +128,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
128 // Remove webtorrent files if not enabled 128 // Remove webtorrent files if not enabled
129 for (const file of video.VideoFiles) { 129 for (const file of video.VideoFiles) {
130 await video.removeFile(file) 130 await video.removeFile(file)
131 await video.removeTorrent(file) 131 await file.removeTorrent()
132 await file.destroy() 132 await file.destroy()
133 } 133 }
134 134
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index 9f17b8820..b549c189f 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -16,7 +16,7 @@ import { VideoModel } from '@server/models/video/video'
16import { VideoFileModel } from '@server/models/video/video-file' 16import { VideoFileModel } from '@server/models/video/video-file'
17import { VideoLiveModel } from '@server/models/video/video-live' 17import { VideoLiveModel } from '@server/models/video/video-live'
18import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 18import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
19import { MStreamingPlaylist, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models' 19import { MStreamingPlaylist, MStreamingPlaylistVideo, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models'
20import { VideoState, VideoStreamingPlaylistType } from '@shared/models' 20import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
21import { federateVideoIfNeeded } from './activitypub/videos' 21import { federateVideoIfNeeded } from './activitypub/videos'
22import { buildSha256Segment } from './hls' 22import { buildSha256Segment } from './hls'
@@ -277,7 +277,7 @@ class LiveManager {
277 return this.runMuxing({ 277 return this.runMuxing({
278 sessionId, 278 sessionId,
279 videoLive, 279 videoLive,
280 playlist: videoStreamingPlaylist, 280 playlist: Object.assign(videoStreamingPlaylist, { Video: video }),
281 rtmpUrl, 281 rtmpUrl,
282 fps, 282 fps,
283 allResolutions 283 allResolutions
@@ -287,7 +287,7 @@ class LiveManager {
287 private async runMuxing (options: { 287 private async runMuxing (options: {
288 sessionId: string 288 sessionId: string
289 videoLive: MVideoLiveVideo 289 videoLive: MVideoLiveVideo
290 playlist: MStreamingPlaylist 290 playlist: MStreamingPlaylistVideo
291 rtmpUrl: string 291 rtmpUrl: string
292 fps: number 292 fps: number
293 allResolutions: number[] 293 allResolutions: number[]
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 93e76626c..60008e695 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -18,14 +18,14 @@ import { VideosRedundancyStrategy } from '../../../shared/models/redundancy'
18import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
19import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' 19import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent'
20import { CONFIG } from '../../initializers/config' 20import { CONFIG } from '../../initializers/config'
21import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' 21import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers/constants'
22import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 22import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
23import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' 23import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
24import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' 24import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
25import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos' 25import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos'
26import { downloadPlaylistSegments } from '../hls' 26import { downloadPlaylistSegments } from '../hls'
27import { removeVideoRedundancy } from '../redundancy' 27import { removeVideoRedundancy } from '../redundancy'
28import { getVideoFilename } from '../video-paths' 28import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-paths'
29import { AbstractScheduler } from './abstract-scheduler' 29import { AbstractScheduler } from './abstract-scheduler'
30 30
31type CandidateToDuplicate = { 31type CandidateToDuplicate = {
@@ -222,17 +222,17 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
222 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy) 222 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
223 223
224 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 224 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
225 const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) 225 const magnetUri = generateMagnetUri(video, video, file, baseUrlHttp, baseUrlWs)
226 226
227 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) 227 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
228 228
229 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, getVideoFilename(video, file)) 229 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, file.filename)
230 await move(tmpPath, destPath, { overwrite: true }) 230 await move(tmpPath, destPath, { overwrite: true })
231 231
232 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ 232 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
233 expiresOn, 233 expiresOn,
234 url: getLocalVideoCacheFileActivityPubUrl(file), 234 url: getLocalVideoCacheFileActivityPubUrl(file),
235 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), 235 fileUrl: generateWebTorrentRedundancyUrl(file),
236 strategy, 236 strategy,
237 videoFileId: file.id, 237 videoFileId: file.id,
238 actorId: serverActor.id 238 actorId: serverActor.id
@@ -271,7 +271,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
271 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ 271 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
272 expiresOn, 272 expiresOn,
273 url: getLocalVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), 273 url: getLocalVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
274 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), 274 fileUrl: generateHLSRedundancyUrl(video, playlistArg),
275 strategy, 275 strategy,
276 videoStreamingPlaylistId: playlist.id, 276 videoStreamingPlaylistId: playlist.id,
277 actorId: serverActor.id 277 actorId: serverActor.id
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts
index 53fc8e81d..0385e89cc 100644
--- a/server/lib/video-paths.ts
+++ b/server/lib/video-paths.ts
@@ -1,19 +1,23 @@
1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
2import { join } from 'path' 1import { join } from 'path'
3import { CONFIG } from '@server/initializers/config'
4import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
5import { extractVideo } from '@server/helpers/video' 2import { extractVideo } from '@server/helpers/video'
3import { CONFIG } from '@server/initializers/config'
4import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants'
5import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
6 6
7// ################## Video file name ################## 7// ################## Video file name ##################
8 8
9function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { 9function generateVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, isHls: boolean, resolution: number, extname: string) {
10 const video = extractVideo(videoOrPlaylist) 10 const video = extractVideo(videoOrPlaylist)
11 11
12 if (videoFile.isHLS()) { 12 // FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.2
13 return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution) 13 // const uuid = uuidv4()
14 const uuid = video.uuid
15
16 if (isHls) {
17 return generateVideoStreamingPlaylistName(uuid, resolution)
14 } 18 }
15 19
16 return generateWebTorrentVideoName(video.uuid, videoFile.resolution, videoFile.extname) 20 return generateWebTorrentVideoName(uuid, resolution, extname)
17} 21}
18 22
19function generateVideoStreamingPlaylistName (uuid: string, resolution: number) { 23function generateVideoStreamingPlaylistName (uuid: string, resolution: number) {
@@ -28,36 +32,64 @@ function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, vi
28 if (videoFile.isHLS()) { 32 if (videoFile.isHLS()) {
29 const video = extractVideo(videoOrPlaylist) 33 const video = extractVideo(videoOrPlaylist)
30 34
31 return join(getHLSDirectory(video), getVideoFilename(videoOrPlaylist, videoFile)) 35 return join(getHLSDirectory(video), videoFile.filename)
32 } 36 }
33 37
34 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR 38 const baseDir = isRedundancy
35 return join(baseDir, getVideoFilename(videoOrPlaylist, videoFile)) 39 ? CONFIG.STORAGE.REDUNDANCY_DIR
40 : CONFIG.STORAGE.VIDEOS_DIR
41
42 return join(baseDir, videoFile.filename)
43}
44
45// ################## Redundancy ##################
46
47function generateHLSRedundancyUrl (video: MVideo, playlist: MStreamingPlaylist) {
48 // Base URL used by our HLS player
49 return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + playlist.getStringType() + '/' + video.uuid
50}
51
52function generateWebTorrentRedundancyUrl (file: MVideoFile) {
53 return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + file.filename
36} 54}
37 55
38// ################## Streaming playlist ################## 56// ################## Streaming playlist ##################
39 57
40function getHLSDirectory (video: MVideoUUID, isRedundancy = false) { 58function getHLSDirectory (video: MVideoUUID, isRedundancy = false) {
41 const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_STREAMING_PLAYLIST_DIRECTORY 59 const baseDir = isRedundancy
60 ? HLS_REDUNDANCY_DIRECTORY
61 : HLS_STREAMING_PLAYLIST_DIRECTORY
42 62
43 return join(baseDir, video.uuid) 63 return join(baseDir, video.uuid)
44} 64}
45 65
46// ################## Torrents ################## 66// ################## Torrents ##################
47 67
48function getTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { 68function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) {
49 const video = extractVideo(videoOrPlaylist) 69 const video = extractVideo(videoOrPlaylist)
50 const extension = '.torrent' 70 const extension = '.torrent'
51 71
72 // FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.2
73 // const uuid = uuidv4()
74 const uuid = video.uuid
75
52 if (isStreamingPlaylist(videoOrPlaylist)) { 76 if (isStreamingPlaylist(videoOrPlaylist)) {
53 return `${video.uuid}-${videoFile.resolution}-${videoOrPlaylist.getStringType()}${extension}` 77 return `${uuid}-${resolution}-${videoOrPlaylist.getStringType()}${extension}`
54 } 78 }
55 79
56 return video.uuid + '-' + videoFile.resolution + extension 80 return uuid + '-' + resolution + extension
81}
82
83function getTorrentFilePath (videoFile: MVideoFile) {
84 return join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename)
57} 85}
58 86
59function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { 87// ################## Meta data ##################
60 return join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile)) 88
89function getLocalVideoFileMetadataUrl (video: MVideoUUID, videoFile: MVideoFile) {
90 const path = '/api/v1/videos/'
91
92 return WEBSERVER.URL + path + video.uuid + '/metadata/' + videoFile.id
61} 93}
62 94
63// --------------------------------------------------------------------------- 95// ---------------------------------------------------------------------------
@@ -65,11 +97,16 @@ function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
65export { 97export {
66 generateVideoStreamingPlaylistName, 98 generateVideoStreamingPlaylistName,
67 generateWebTorrentVideoName, 99 generateWebTorrentVideoName,
68 getVideoFilename, 100 generateVideoFilename,
69 getVideoFilePath, 101 getVideoFilePath,
70 102
71 getTorrentFileName, 103 generateTorrentFileName,
72 getTorrentFilePath, 104 getTorrentFilePath,
73 105
74 getHLSDirectory 106 getHLSDirectory,
107
108 getLocalVideoFileMetadataUrl,
109
110 generateWebTorrentRedundancyUrl,
111 generateHLSRedundancyUrl
75} 112}
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index a58c9dd20..b366e2e44 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -2,7 +2,7 @@ import { Job } from 'bull'
2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' 2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
3import { basename, extname as extnameUtil, join } from 'path' 3import { basename, extname as extnameUtil, join } from 'path'
4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
5import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' 5import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
6import { VideoResolution } from '../../shared/models/videos' 6import { VideoResolution } from '../../shared/models/videos'
7import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' 7import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
8import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' 8import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
@@ -13,7 +13,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSER
13import { VideoFileModel } from '../models/video/video-file' 13import { VideoFileModel } from '../models/video/video-file'
14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
15import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls' 15import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
16import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' 16import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from './video-paths'
17import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' 17import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
18 18
19/** 19/**
@@ -24,7 +24,7 @@ import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
24 */ 24 */
25 25
26// Optimize the original video file and replace it. The resolution is not changed. 26// Optimize the original video file and replace it. The resolution is not changed.
27async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: MVideoFile, job?: Job) { 27async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
28 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 28 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
29 const newExtname = '.mp4' 29 const newExtname = '.mp4'
30 30
@@ -55,8 +55,9 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
55 try { 55 try {
56 await remove(videoInputPath) 56 await remove(videoInputPath)
57 57
58 // Important to do this before getVideoFilename() to take in account the new file extension 58 // Important to do this before getVideoFilename() to take in account the new filename
59 inputVideoFile.extname = newExtname 59 inputVideoFile.extname = newExtname
60 inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname)
60 61
61 const videoOutputPath = getVideoFilePath(video, inputVideoFile) 62 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
62 63
@@ -72,7 +73,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
72} 73}
73 74
74// Transcode the original video file to a lower resolution. 75// Transcode the original video file to a lower resolution.
75async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean, job: Job) { 76async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
76 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 77 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
77 const extname = '.mp4' 78 const extname = '.mp4'
78 79
@@ -82,11 +83,13 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
82 const newVideoFile = new VideoFileModel({ 83 const newVideoFile = new VideoFileModel({
83 resolution, 84 resolution,
84 extname, 85 extname,
86 filename: generateVideoFilename(video, false, resolution, extname),
85 size: 0, 87 size: 0,
86 videoId: video.id 88 videoId: video.id
87 }) 89 })
90
88 const videoOutputPath = getVideoFilePath(video, newVideoFile) 91 const videoOutputPath = getVideoFilePath(video, newVideoFile)
89 const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile)) 92 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
90 93
91 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO 94 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
92 ? { 95 ? {
@@ -122,7 +125,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
122} 125}
123 126
124// Merge an image with an audio file to create a video 127// Merge an image with an audio file to create a video
125async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution, job: Job) { 128async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
126 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 129 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
127 const newExtname = '.mp4' 130 const newExtname = '.mp4'
128 131
@@ -175,7 +178,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
175 178
176// Concat TS segments from a live video to a fragmented mp4 HLS playlist 179// Concat TS segments from a live video to a fragmented mp4 HLS playlist
177async function generateHlsPlaylistResolutionFromTS (options: { 180async function generateHlsPlaylistResolutionFromTS (options: {
178 video: MVideoWithFile 181 video: MVideoFullLight
179 concatenatedTsFilePath: string 182 concatenatedTsFilePath: string
180 resolution: VideoResolution 183 resolution: VideoResolution
181 isPortraitMode: boolean 184 isPortraitMode: boolean
@@ -193,7 +196,7 @@ async function generateHlsPlaylistResolutionFromTS (options: {
193 196
194// Generate an HLS playlist from an input file, and update the master playlist 197// Generate an HLS playlist from an input file, and update the master playlist
195function generateHlsPlaylistResolution (options: { 198function generateHlsPlaylistResolution (options: {
196 video: MVideoWithFile 199 video: MVideoFullLight
197 videoInputPath: string 200 videoInputPath: string
198 resolution: VideoResolution 201 resolution: VideoResolution
199 copyCodecs: boolean 202 copyCodecs: boolean
@@ -235,7 +238,7 @@ export {
235// --------------------------------------------------------------------------- 238// ---------------------------------------------------------------------------
236 239
237async function onWebTorrentVideoFileTranscoding ( 240async function onWebTorrentVideoFileTranscoding (
238 video: MVideoWithFile, 241 video: MVideoFullLight,
239 videoFile: MVideoFile, 242 videoFile: MVideoFile,
240 transcodingPath: string, 243 transcodingPath: string,
241 outputPath: string 244 outputPath: string
@@ -250,7 +253,7 @@ async function onWebTorrentVideoFileTranscoding (
250 videoFile.fps = fps 253 videoFile.fps = fps
251 videoFile.metadata = metadata 254 videoFile.metadata = metadata
252 255
253 await createTorrentAndSetInfoHash(video, videoFile) 256 await createTorrentAndSetInfoHash(video, video, videoFile)
254 257
255 await VideoFileModel.customUpsert(videoFile, 'video', undefined) 258 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
256 video.VideoFiles = await video.$get('VideoFiles') 259 video.VideoFiles = await video.$get('VideoFiles')
@@ -260,7 +263,7 @@ async function onWebTorrentVideoFileTranscoding (
260 263
261async function generateHlsPlaylistCommon (options: { 264async function generateHlsPlaylistCommon (options: {
262 type: 'hls' | 'hls-from-ts' 265 type: 'hls' | 'hls-from-ts'
263 video: MVideoWithFile 266 video: MVideoFullLight
264 inputPath: string 267 inputPath: string
265 resolution: VideoResolution 268 resolution: VideoResolution
266 copyCodecs?: boolean 269 copyCodecs?: boolean
@@ -318,10 +321,12 @@ async function generateHlsPlaylistCommon (options: {
318 videoStreamingPlaylist.Video = video 321 videoStreamingPlaylist.Video = video
319 322
320 // Build the new playlist file 323 // Build the new playlist file
324 const extname = extnameUtil(videoFilename)
321 const newVideoFile = new VideoFileModel({ 325 const newVideoFile = new VideoFileModel({
322 resolution, 326 resolution,
323 extname: extnameUtil(videoFilename), 327 extname,
324 size: 0, 328 size: 0,
329 filename: generateVideoFilename(video, true, resolution, extname),
325 fps: -1, 330 fps: -1,
326 videoStreamingPlaylistId: videoStreamingPlaylist.id 331 videoStreamingPlaylistId: videoStreamingPlaylist.id
327 }) 332 })
@@ -344,7 +349,7 @@ async function generateHlsPlaylistCommon (options: {
344 newVideoFile.fps = await getVideoFileFPS(videoFilePath) 349 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
345 newVideoFile.metadata = await getMetadataFromFile(videoFilePath) 350 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
346 351
347 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) 352 await createTorrentAndSetInfoHash(videoStreamingPlaylist, video, newVideoFile)
348 353
349 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) 354 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
350 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') 355 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 4185ec5f2..9533c8d19 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -17,7 +17,7 @@ import {
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
19import { afterCommitIfTransaction } from '@server/helpers/database-utils' 19import { afterCommitIfTransaction } from '@server/helpers/database-utils'
20import { MThumbnail, MThumbnailVideo, MVideoAccountLight } from '@server/types/models' 20import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
23import { CONFIG } from '../../initializers/config' 23import { CONFIG } from '../../initializers/config'
@@ -164,7 +164,7 @@ export class ThumbnailModel extends Model {
164 return join(directory, filename) 164 return join(directory, filename)
165 } 165 }
166 166
167 getFileUrl (video: MVideoAccountLight) { 167 getFileUrl (video: MVideoWithHost) {
168 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename 168 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
169 169
170 if (video.isOwned()) return WEBSERVER.URL + staticPath 170 if (video.isOwned()) return WEBSERVER.URL + staticPath
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index a1553ea15..71b067335 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -15,8 +15,9 @@ import {
15 Table, 15 Table,
16 UpdatedAt 16 UpdatedAt
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { v4 as uuidv4 } from 'uuid'
18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 19import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
19import { MVideoAccountLight, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' 20import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models'
20import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 21import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
21import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 22import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
22import { logger } from '../../helpers/logger' 23import { logger } from '../../helpers/logger'
@@ -24,7 +25,6 @@ import { CONFIG } from '../../initializers/config'
24import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' 25import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
25import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' 26import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
26import { VideoModel } from './video' 27import { VideoModel } from './video'
27import { v4 as uuidv4 } from 'uuid'
28 28
29export enum ScopeNames { 29export enum ScopeNames {
30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -204,7 +204,7 @@ export class VideoCaptionModel extends Model {
204 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) 204 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
205 } 205 }
206 206
207 getFileUrl (video: MVideoAccountLight) { 207 getFileUrl (video: MVideoWithHost) {
208 if (!this.Video) this.Video = video as VideoModel 208 if (!this.Video) this.Video = video as VideoModel
209 209
210 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() 210 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 48b337c68..57807cbfd 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -1,3 +1,7 @@
1import { remove } from 'fs-extra'
2import * as memoizee from 'memoizee'
3import { join } from 'path'
4import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
1import { 5import {
2 AllowNull, 6 AllowNull,
3 BelongsTo, 7 BelongsTo,
@@ -5,15 +9,22 @@ import {
5 CreatedAt, 9 CreatedAt,
6 DataType, 10 DataType,
7 Default, 11 Default,
12 DefaultScope,
8 ForeignKey, 13 ForeignKey,
9 HasMany, 14 HasMany,
10 Is, 15 Is,
11 Model, 16 Model,
12 Table,
13 UpdatedAt,
14 Scopes, 17 Scopes,
15 DefaultScope 18 Table,
19 UpdatedAt
16} from 'sequelize-typescript' 20} from 'sequelize-typescript'
21import { Where } from 'sequelize/types/lib/utils'
22import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { logger } from '@server/helpers/logger'
25import { extractVideo } from '@server/helpers/video'
26import { getTorrentFilePath } from '@server/lib/video-paths'
27import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
17import { 28import {
18 isVideoFileExtnameValid, 29 isVideoFileExtnameValid,
19 isVideoFileInfoHashValid, 30 isVideoFileInfoHashValid,
@@ -21,20 +32,25 @@ import {
21 isVideoFileSizeValid, 32 isVideoFileSizeValid,
22 isVideoFPSResolutionValid 33 isVideoFPSResolutionValid
23} from '../../helpers/custom-validators/videos' 34} from '../../helpers/custom-validators/videos'
35import {
36 LAZY_STATIC_PATHS,
37 MEMOIZE_LENGTH,
38 MEMOIZE_TTL,
39 MIMETYPES,
40 STATIC_DOWNLOAD_PATHS,
41 STATIC_PATHS,
42 WEBSERVER
43} from '../../initializers/constants'
44import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
45import { VideoRedundancyModel } from '../redundancy/video-redundancy'
24import { parseAggregateResult, throwIfNotValid } from '../utils' 46import { parseAggregateResult, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 47import { VideoModel } from './video'
26import { VideoRedundancyModel } from '../redundancy/video-redundancy'
27import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 48import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
28import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
29import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
30import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
31import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
32import * as memoizee from 'memoizee'
33import validator from 'validator'
34 49
35export enum ScopeNames { 50export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO', 51 WITH_VIDEO = 'WITH_VIDEO',
37 WITH_METADATA = 'WITH_METADATA' 52 WITH_METADATA = 'WITH_METADATA',
53 WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST'
38} 54}
39 55
40@DefaultScope(() => ({ 56@DefaultScope(() => ({
@@ -51,6 +67,28 @@ export enum ScopeNames {
51 } 67 }
52 ] 68 ]
53 }, 69 },
70 [ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (options: { whereVideo?: Where } = {}) => {
71 return {
72 include: [
73 {
74 model: VideoModel.unscoped(),
75 required: false,
76 where: options.whereVideo
77 },
78 {
79 model: VideoStreamingPlaylistModel.unscoped(),
80 required: false,
81 include: [
82 {
83 model: VideoModel.unscoped(),
84 required: true,
85 where: options.whereVideo
86 }
87 ]
88 }
89 ]
90 }
91 },
54 [ScopeNames.WITH_METADATA]: { 92 [ScopeNames.WITH_METADATA]: {
55 attributes: { 93 attributes: {
56 include: [ 'metadata' ] 94 include: [ 'metadata' ]
@@ -82,6 +120,16 @@ export enum ScopeNames {
82 }, 120 },
83 121
84 { 122 {
123 fields: [ 'torrentFilename' ],
124 unique: true
125 },
126
127 {
128 fields: [ 'filename' ],
129 unique: true
130 },
131
132 {
85 fields: [ 'videoId', 'resolution', 'fps' ], 133 fields: [ 'videoId', 'resolution', 'fps' ],
86 unique: true, 134 unique: true,
87 where: { 135 where: {
@@ -142,6 +190,24 @@ export class VideoFileModel extends Model {
142 @Column 190 @Column
143 metadataUrl: string 191 metadataUrl: string
144 192
193 @AllowNull(true)
194 @Column
195 fileUrl: string
196
197 // Could be null for live files
198 @AllowNull(true)
199 @Column
200 filename: string
201
202 @AllowNull(true)
203 @Column
204 torrentUrl: string
205
206 // Could be null for live files
207 @AllowNull(true)
208 @Column
209 torrentFilename: string
210
145 @ForeignKey(() => VideoModel) 211 @ForeignKey(() => VideoModel)
146 @Column 212 @Column
147 videoId: number 213 videoId: number
@@ -199,6 +265,16 @@ export class VideoFileModel extends Model {
199 return !!videoFile 265 return !!videoFile
200 } 266 }
201 267
268 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
269 const query = {
270 where: {
271 torrentFilename: filename
272 }
273 }
274
275 return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
276 }
277
202 static loadWithMetadata (id: number) { 278 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) 279 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
204 } 280 }
@@ -215,28 +291,11 @@ export class VideoFileModel extends Model {
215 const options = { 291 const options = {
216 where: { 292 where: {
217 id 293 id
218 }, 294 }
219 include: [
220 {
221 model: VideoModel.unscoped(),
222 required: false,
223 where: whereVideo
224 },
225 {
226 model: VideoStreamingPlaylistModel.unscoped(),
227 required: false,
228 include: [
229 {
230 model: VideoModel.unscoped(),
231 required: true,
232 where: whereVideo
233 }
234 ]
235 }
236 ]
237 } 295 }
238 296
239 return VideoFileModel.findOne(options) 297 return VideoFileModel.scope({ method: [ ScopeNames.WITH_VIDEO_OR_PLAYLIST, whereVideo ] })
298 .findOne(options)
240 .then(file => { 299 .then(file => {
241 // We used `required: false` so check we have at least a video or a streaming playlist 300 // We used `required: false` so check we have at least a video or a streaming playlist
242 if (!file.Video && !file.VideoStreamingPlaylist) return null 301 if (!file.Video && !file.VideoStreamingPlaylist) return null
@@ -348,6 +407,10 @@ export class VideoFileModel extends Model {
348 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist 407 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
349 } 408 }
350 409
410 getVideo (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo {
411 return extractVideo(this.getVideoOrStreamingPlaylist())
412 }
413
351 isAudio () { 414 isAudio () {
352 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] 415 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
353 } 416 }
@@ -360,6 +423,62 @@ export class VideoFileModel extends Model {
360 return !!this.videoStreamingPlaylistId 423 return !!this.videoStreamingPlaylistId
361 } 424 }
362 425
426 getFileUrl (video: MVideoWithHost) {
427 if (!this.Video) this.Video = video as VideoModel
428
429 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
430 if (this.fileUrl) return this.fileUrl
431
432 // Fallback if we don't have a file URL
433 return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
434 }
435
436 getFileStaticPath (video: MVideo) {
437 if (this.isHLS()) return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename)
438
439 return join(STATIC_PATHS.WEBSEED, this.filename)
440 }
441
442 getFileDownloadUrl (video: MVideoWithHost) {
443 const basePath = this.isHLS()
444 ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS
445 : STATIC_DOWNLOAD_PATHS.VIDEOS
446 const path = join(basePath, this.filename)
447
448 if (video.isOwned()) return WEBSERVER.URL + path
449
450 // FIXME: don't guess remote URL
451 return buildRemoteVideoBaseUrl(video, path)
452 }
453
454 getRemoteTorrentUrl (video: MVideoWithHost) {
455 if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
456
457 if (this.torrentUrl) return this.torrentUrl
458
459 // Fallback if we don't have a torrent URL
460 return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
461 }
462
463 // We proxify torrent requests so use a local URL
464 getTorrentUrl () {
465 return WEBSERVER.URL + this.getTorrentStaticPath()
466 }
467
468 getTorrentStaticPath () {
469 return join(LAZY_STATIC_PATHS.TORRENTS, this.torrentFilename)
470 }
471
472 getTorrentDownloadUrl () {
473 return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.TORRENTS, this.torrentFilename)
474 }
475
476 removeTorrent () {
477 const torrentPath = getTorrentFilePath(this)
478 return remove(torrentPath)
479 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
480 }
481
363 hasSameUniqueKeysThan (other: MVideoFile) { 482 hasSameUniqueKeysThan (other: MVideoFile) {
364 return this.fps === other.fps && 483 return this.fps === other.fps &&
365 this.resolution === other.resolution && 484 this.resolution === other.resolution &&
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 77b8bcfe3..adf460734 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -1,16 +1,17 @@
1import { Video, VideoDetails } from '../../../shared/models/videos' 1import { generateMagnetUri } from '@server/helpers/webtorrent'
2import { VideoModel } from './video' 2import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths'
3import { VideoFile } from '@shared/models/videos/video-file.model'
3import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 4import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
5import { Video, VideoDetails } from '../../../shared/models/videos'
6import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
7import { isArray } from '../../helpers/custom-validators/misc'
4import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 8import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
5import { VideoCaptionModel } from './video-caption'
6import { 9import {
7 getLocalVideoCommentsActivityPubUrl, 10 getLocalVideoCommentsActivityPubUrl,
8 getLocalVideoDislikesActivityPubUrl, 11 getLocalVideoDislikesActivityPubUrl,
9 getLocalVideoLikesActivityPubUrl, 12 getLocalVideoLikesActivityPubUrl,
10 getLocalVideoSharesActivityPubUrl 13 getLocalVideoSharesActivityPubUrl
11} from '../../lib/activitypub/url' 14} from '../../lib/activitypub/url'
12import { isArray } from '../../helpers/custom-validators/misc'
13import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
14import { 15import {
15 MStreamingPlaylistRedundanciesOpt, 16 MStreamingPlaylistRedundanciesOpt,
16 MStreamingPlaylistVideo, 17 MStreamingPlaylistVideo,
@@ -18,12 +19,12 @@ import {
18 MVideoAP, 19 MVideoAP,
19 MVideoFile, 20 MVideoFile,
20 MVideoFormattable, 21 MVideoFormattable,
21 MVideoFormattableDetails 22 MVideoFormattableDetails,
23 MVideoWithHost
22} from '../../types/models' 24} from '../../types/models'
23import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file' 25import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file'
24import { VideoFile } from '@shared/models/videos/video-file.model' 26import { VideoModel } from './video'
25import { generateMagnetUri } from '@server/helpers/webtorrent' 27import { VideoCaptionModel } from './video-caption'
26import { extractVideo } from '@server/helpers/video'
27 28
28export type VideoFormattingJSONOptions = { 29export type VideoFormattingJSONOptions = {
29 completeDescription?: boolean 30 completeDescription?: boolean
@@ -153,12 +154,15 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
153 } 154 }
154 155
155 // Format and sort video files 156 // Format and sort video files
156 detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles) 157 detailsJson.files = videoFilesModelToFormattedJSON(video, video, baseUrlHttp, baseUrlWs, video.VideoFiles)
157 158
158 return Object.assign(formattedJson, detailsJson) 159 return Object.assign(formattedJson, detailsJson)
159} 160}
160 161
161function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] { 162function streamingPlaylistsModelToFormattedJSON (
163 video: MVideoFormattableDetails,
164 playlists: MStreamingPlaylistRedundanciesOpt[]
165): VideoStreamingPlaylist[] {
162 if (isArray(playlists) === false) return [] 166 if (isArray(playlists) === false) return []
163 167
164 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 168 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
@@ -171,7 +175,7 @@ function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStre
171 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) 175 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
172 : [] 176 : []
173 177
174 const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles) 178 const files = videoFilesModelToFormattedJSON(playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
175 179
176 return { 180 return {
177 id: playlist.id, 181 id: playlist.id,
@@ -190,14 +194,14 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
190 return -1 194 return -1
191} 195}
192 196
197// FIXME: refactor/merge model and video arguments
193function videoFilesModelToFormattedJSON ( 198function videoFilesModelToFormattedJSON (
194 model: MVideo | MStreamingPlaylistVideo, 199 model: MVideo | MStreamingPlaylistVideo,
200 video: MVideoFormattableDetails,
195 baseUrlHttp: string, 201 baseUrlHttp: string,
196 baseUrlWs: string, 202 baseUrlWs: string,
197 videoFiles: MVideoFileRedundanciesOpt[] 203 videoFiles: MVideoFileRedundanciesOpt[]
198): VideoFile[] { 204): VideoFile[] {
199 const video = extractVideo(model)
200
201 return [ ...videoFiles ] 205 return [ ...videoFiles ]
202 .filter(f => !f.isLive()) 206 .filter(f => !f.isLive())
203 .sort(sortByResolutionDesc) 207 .sort(sortByResolutionDesc)
@@ -207,21 +211,29 @@ function videoFilesModelToFormattedJSON (
207 id: videoFile.resolution, 211 id: videoFile.resolution,
208 label: videoFile.resolution + 'p' 212 label: videoFile.resolution + 'p'
209 }, 213 },
210 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), 214
215 // FIXME: deprecated in 3.2
216 magnetUri: generateMagnetUri(model, video, videoFile, baseUrlHttp, baseUrlWs),
217
211 size: videoFile.size, 218 size: videoFile.size,
212 fps: videoFile.fps, 219 fps: videoFile.fps,
213 torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp), 220
214 torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), 221 torrentUrl: videoFile.getTorrentUrl(),
215 fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), 222 torrentDownloadUrl: videoFile.getTorrentDownloadUrl(),
216 fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp), 223
217 metadataUrl: video.getVideoFileMetadataUrl(videoFile, baseUrlHttp) 224 fileUrl: videoFile.getFileUrl(video),
225 fileDownloadUrl: videoFile.getFileDownloadUrl(video),
226
227 metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile)
218 } as VideoFile 228 } as VideoFile
219 }) 229 })
220} 230}
221 231
232// FIXME: refactor/merge model and video arguments
222function addVideoFilesInAPAcc ( 233function addVideoFilesInAPAcc (
223 acc: ActivityUrlObject[] | ActivityTagObject[], 234 acc: ActivityUrlObject[] | ActivityTagObject[],
224 model: MVideoAP | MStreamingPlaylistVideo, 235 model: MVideoAP | MStreamingPlaylistVideo,
236 video: MVideoWithHost,
225 baseUrlHttp: string, 237 baseUrlHttp: string,
226 baseUrlWs: string, 238 baseUrlWs: string,
227 files: MVideoFile[] 239 files: MVideoFile[]
@@ -234,7 +246,7 @@ function addVideoFilesInAPAcc (
234 acc.push({ 246 acc.push({
235 type: 'Link', 247 type: 'Link',
236 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, 248 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any,
237 href: model.getVideoFileUrl(file, baseUrlHttp), 249 href: file.getFileUrl(video),
238 height: file.resolution, 250 height: file.resolution,
239 size: file.size, 251 size: file.size,
240 fps: file.fps 252 fps: file.fps
@@ -244,7 +256,7 @@ function addVideoFilesInAPAcc (
244 type: 'Link', 256 type: 'Link',
245 rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], 257 rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ],
246 mediaType: 'application/json' as 'application/json', 258 mediaType: 'application/json' as 'application/json',
247 href: extractVideo(model).getVideoFileMetadataUrl(file, baseUrlHttp), 259 href: getLocalVideoFileMetadataUrl(video, file),
248 height: file.resolution, 260 height: file.resolution,
249 fps: file.fps 261 fps: file.fps
250 }) 262 })
@@ -252,14 +264,14 @@ function addVideoFilesInAPAcc (
252 acc.push({ 264 acc.push({
253 type: 'Link', 265 type: 'Link',
254 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', 266 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
255 href: model.getTorrentUrl(file, baseUrlHttp), 267 href: file.getTorrentUrl(),
256 height: file.resolution 268 height: file.resolution
257 }) 269 })
258 270
259 acc.push({ 271 acc.push({
260 type: 'Link', 272 type: 'Link',
261 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', 273 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
262 href: generateMagnetUri(model, file, baseUrlHttp, baseUrlWs), 274 href: generateMagnetUri(model, video, file, baseUrlHttp, baseUrlWs),
263 height: file.resolution 275 height: file.resolution
264 }) 276 })
265 } 277 }
@@ -307,7 +319,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
307 } 319 }
308 ] 320 ]
309 321
310 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) 322 addVideoFilesInAPAcc(url, video, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
311 323
312 for (const playlist of (video.VideoStreamingPlaylists || [])) { 324 for (const playlist of (video.VideoStreamingPlaylists || [])) {
313 const tag = playlist.p2pMediaLoaderInfohashes 325 const tag = playlist.p2pMediaLoaderInfohashes
@@ -320,7 +332,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
320 }) 332 })
321 333
322 const playlistWithVideo = Object.assign(playlist, { Video: video }) 334 const playlistWithVideo = Object.assign(playlist, { Video: video })
323 addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || []) 335 addVideoFilesInAPAcc(tag, playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
324 336
325 url.push({ 337 url.push({
326 type: 'Link', 338 type: 'Link',
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 822d0c89b..af1878e7a 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -516,6 +516,10 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
516 '"VideoFiles"."resolution"': '"VideoFiles.resolution"', 516 '"VideoFiles"."resolution"': '"VideoFiles.resolution"',
517 '"VideoFiles"."size"': '"VideoFiles.size"', 517 '"VideoFiles"."size"': '"VideoFiles.size"',
518 '"VideoFiles"."extname"': '"VideoFiles.extname"', 518 '"VideoFiles"."extname"': '"VideoFiles.extname"',
519 '"VideoFiles"."filename"': '"VideoFiles.filename"',
520 '"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"',
521 '"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"',
522 '"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"',
519 '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"', 523 '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"',
520 '"VideoFiles"."fps"': '"VideoFiles.fps"', 524 '"VideoFiles"."fps"': '"VideoFiles.fps"',
521 '"VideoFiles"."videoId"': '"VideoFiles.videoId"', 525 '"VideoFiles"."videoId"': '"VideoFiles.videoId"',
@@ -529,6 +533,10 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
529 '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"', 533 '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"',
530 '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"', 534 '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"',
531 '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"', 535 '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"',
536 '"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"',
537 '"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"',
538 '"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"',
539 '"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"',
532 '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"', 540 '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"',
533 '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"', 541 '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"',
534 '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"', 542 '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"',
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index 148768c21..c9375b433 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -1,28 +1,18 @@
1import * as memoizee from 'memoizee'
2import { join } from 'path'
3import { Op, QueryTypes } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 5import { VideoFileModel } from '@server/models/video/video-file'
3import { throwIfNotValid } from '../utils' 6import { MStreamingPlaylist } from '@server/types/models'
4import { VideoModel } from './video'
5import { VideoRedundancyModel } from '../redundancy/video-redundancy'
6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 7import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import {
9 CONSTRAINTS_FIELDS,
10 MEMOIZE_LENGTH,
11 MEMOIZE_TTL,
12 P2P_MEDIA_LOADER_PEER_VERSION,
13 STATIC_DOWNLOAD_PATHS,
14 STATIC_PATHS
15} from '../../initializers/constants'
16import { join } from 'path'
17import { sha1 } from '../../helpers/core-utils' 8import { sha1 } from '../../helpers/core-utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { isArrayOf } from '../../helpers/custom-validators/misc' 10import { isArrayOf } from '../../helpers/custom-validators/misc'
19import { Op, QueryTypes } from 'sequelize' 11import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
20import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideoFile } from '@server/types/models' 12import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
21import { VideoFileModel } from '@server/models/video/video-file' 13import { VideoRedundancyModel } from '../redundancy/video-redundancy'
22import { getTorrentFileName, getTorrentFilePath, getVideoFilename } from '@server/lib/video-paths' 14import { throwIfNotValid } from '../utils'
23import * as memoizee from 'memoizee' 15import { VideoModel } from './video'
24import { remove } from 'fs-extra'
25import { logger } from '@server/helpers/logger'
26 16
27@Table({ 17@Table({
28 tableName: 'videoStreamingPlaylist', 18 tableName: 'videoStreamingPlaylist',
@@ -196,26 +186,6 @@ export class VideoStreamingPlaylistModel extends Model {
196 return 'unknown' 186 return 'unknown'
197 } 187 }
198 188
199 getVideoRedundancyUrl (baseUrlHttp: string) {
200 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
201 }
202
203 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
204 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
205 }
206
207 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
208 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
209 }
210
211 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
212 return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
213 }
214
215 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
216 return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
217 }
218
219 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { 189 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
220 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 190 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
221 } 191 }
@@ -224,10 +194,4 @@ export class VideoStreamingPlaylistModel extends Model {
224 return this.type === other.type && 194 return this.type === other.type &&
225 this.videoId === other.videoId 195 this.videoId === other.videoId
226 } 196 }
227
228 removeTorrent (this: MStreamingPlaylistVideo, videoFile: MVideoFile) {
229 const torrentPath = getTorrentFilePath(this, videoFile)
230 return remove(torrentPath)
231 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
232 }
233} 197}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3321deed3..2e6b6aeec 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -24,10 +24,11 @@ import {
24 Table, 24 Table,
25 UpdatedAt 25 UpdatedAt
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { v4 as uuidv4 } from 'uuid'
27import { buildNSFWFilter } from '@server/helpers/express-utils' 28import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
29import { LiveManager } from '@server/lib/live-manager' 30import { LiveManager } from '@server/lib/live-manager'
30import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 31import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
31import { getServerActor } from '@server/models/application/application' 32import { getServerActor } from '@server/models/application/application'
32import { ModelCache } from '@server/models/model-cache' 33import { ModelCache } from '@server/models/model-cache'
33import { VideoFile } from '@shared/models/videos/video-file.model' 34import { VideoFile } from '@shared/models/videos/video-file.model'
@@ -60,7 +61,6 @@ import {
60 CONSTRAINTS_FIELDS, 61 CONSTRAINTS_FIELDS,
61 LAZY_STATIC_PATHS, 62 LAZY_STATIC_PATHS,
62 REMOTE_SCHEME, 63 REMOTE_SCHEME,
63 STATIC_DOWNLOAD_PATHS,
64 STATIC_PATHS, 64 STATIC_PATHS,
65 VIDEO_CATEGORIES, 65 VIDEO_CATEGORIES,
66 VIDEO_LANGUAGES, 66 VIDEO_LANGUAGES,
@@ -78,6 +78,7 @@ import {
78 MStreamingPlaylistFilesVideo, 78 MStreamingPlaylistFilesVideo,
79 MUserAccountId, 79 MUserAccountId,
80 MUserId, 80 MUserId,
81 MVideo,
81 MVideoAccountLight, 82 MVideoAccountLight,
82 MVideoAccountLightBlacklistAllFiles, 83 MVideoAccountLightBlacklistAllFiles,
83 MVideoAP, 84 MVideoAP,
@@ -130,7 +131,6 @@ import { VideoShareModel } from './video-share'
130import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 131import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
131import { VideoTagModel } from './video-tag' 132import { VideoTagModel } from './video-tag'
132import { VideoViewModel } from './video-view' 133import { VideoViewModel } from './video-view'
133import { v4 as uuidv4 } from 'uuid'
134 134
135export enum ScopeNames { 135export enum ScopeNames {
136 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 136 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -790,7 +790,7 @@ export class VideoModel extends Model {
790 // Remove physical files and torrents 790 // Remove physical files and torrents
791 instance.VideoFiles.forEach(file => { 791 instance.VideoFiles.forEach(file => {
792 tasks.push(instance.removeFile(file)) 792 tasks.push(instance.removeFile(file))
793 tasks.push(instance.removeTorrent(file)) 793 tasks.push(file.removeTorrent())
794 }) 794 })
795 795
796 // Remove playlists file 796 // Remove playlists file
@@ -853,18 +853,14 @@ export class VideoModel extends Model {
853 return undefined 853 return undefined
854 } 854 }
855 855
856 static listLocal (): Promise<MVideoWithAllFiles[]> { 856 static listLocal (): Promise<MVideo[]> {
857 const query = { 857 const query = {
858 where: { 858 where: {
859 remote: false 859 remote: false
860 } 860 }
861 } 861 }
862 862
863 return VideoModel.scope([ 863 return VideoModel.findAll(query)
864 ScopeNames.WITH_WEBTORRENT_FILES,
865 ScopeNames.WITH_STREAMING_PLAYLISTS,
866 ScopeNames.WITH_THUMBNAILS
867 ]).findAll(query)
868 } 864 }
869 865
870 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 866 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -1623,6 +1619,10 @@ export class VideoModel extends Model {
1623 'resolution', 1619 'resolution',
1624 'size', 1620 'size',
1625 'extname', 1621 'extname',
1622 'filename',
1623 'fileUrl',
1624 'torrentFilename',
1625 'torrentUrl',
1626 'infoHash', 1626 'infoHash',
1627 'fps', 1627 'fps',
1628 'videoId', 1628 'videoId',
@@ -1891,14 +1891,14 @@ export class VideoModel extends Model {
1891 let files: VideoFile[] = [] 1891 let files: VideoFile[] = []
1892 1892
1893 if (Array.isArray(this.VideoFiles)) { 1893 if (Array.isArray(this.VideoFiles)) {
1894 const result = videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles) 1894 const result = videoFilesModelToFormattedJSON(this, this, baseUrlHttp, baseUrlWs, this.VideoFiles)
1895 files = files.concat(result) 1895 files = files.concat(result)
1896 } 1896 }
1897 1897
1898 for (const p of (this.VideoStreamingPlaylists || [])) { 1898 for (const p of (this.VideoStreamingPlaylists || [])) {
1899 p.Video = this 1899 p.Video = this
1900 1900
1901 const result = videoFilesModelToFormattedJSON(p, baseUrlHttp, baseUrlWs, p.VideoFiles) 1901 const result = videoFilesModelToFormattedJSON(p, this, baseUrlHttp, baseUrlWs, p.VideoFiles)
1902 files = files.concat(result) 1902 files = files.concat(result)
1903 } 1903 }
1904 1904
@@ -1956,12 +1956,6 @@ export class VideoModel extends Model {
1956 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1956 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1957 } 1957 }
1958 1958
1959 removeTorrent (videoFile: MVideoFile) {
1960 const torrentPath = getTorrentFilePath(this, videoFile)
1961 return remove(torrentPath)
1962 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1963 }
1964
1965 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { 1959 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
1966 const directoryPath = getHLSDirectory(this, isRedundancy) 1960 const directoryPath = getHLSDirectory(this, isRedundancy)
1967 1961
@@ -1977,7 +1971,7 @@ export class VideoModel extends Model {
1977 1971
1978 // Remove physical files and torrents 1972 // Remove physical files and torrents
1979 await Promise.all( 1973 await Promise.all(
1980 streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file)) 1974 streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
1981 ) 1975 )
1982 } 1976 }
1983 } 1977 }
@@ -2054,34 +2048,6 @@ export class VideoModel extends Model {
2054 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 2048 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
2055 } 2049 }
2056 2050
2057 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2058 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
2059 }
2060
2061 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2062 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
2063 }
2064
2065 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2066 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
2067 }
2068
2069 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2070 const path = '/api/v1/videos/'
2071
2072 return this.isOwned()
2073 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
2074 : videoFile.metadataUrl
2075 }
2076
2077 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2078 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
2079 }
2080
2081 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2082 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
2083 }
2084
2085 getBandwidthBits (videoFile: MVideoFile) { 2051 getBandwidthBits (videoFile: MVideoFile) {
2086 return Math.ceil((videoFile.size * 8) / this.duration) 2052 return Math.ceil((videoFile.size * 8) / this.duration)
2087 } 2053 }
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts
index db551dd9e..03ac3f321 100644
--- a/server/tests/api/videos/video-hls.ts
+++ b/server/tests/api/videos/video-hls.ts
@@ -52,7 +52,7 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
52 expect(file).to.not.be.undefined 52 expect(file).to.not.be.undefined
53 53
54 expect(file.magnetUri).to.have.lengthOf.above(2) 54 expect(file.magnetUri).to.have.lengthOf.above(2)
55 expect(file.torrentUrl).to.equal(`${baseUrl}/static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`) 55 expect(file.torrentUrl).to.equal(`http://${server.host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`)
56 expect(file.fileUrl).to.equal( 56 expect(file.fileUrl).to.equal(
57 `${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4` 57 `${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`
58 ) 58 )
diff --git a/server/tests/cli/create-import-video-file-job.ts b/server/tests/cli/create-import-video-file-job.ts
index dac049fe4..7eaf2c19e 100644
--- a/server/tests/cli/create-import-video-file-job.ts
+++ b/server/tests/cli/create-import-video-file-job.ts
@@ -2,7 +2,7 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { VideoDetails } from '../../../shared/models/videos' 5import { VideoFile } from '@shared/models/videos/video-file.model'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 doubleFollow, 8 doubleFollow,
@@ -16,7 +16,7 @@ import {
16 uploadVideo 16 uploadVideo
17} from '../../../shared/extra-utils' 17} from '../../../shared/extra-utils'
18import { waitJobs } from '../../../shared/extra-utils/server/jobs' 18import { waitJobs } from '../../../shared/extra-utils/server/jobs'
19import { VideoFile } from '@shared/models/videos/video-file.model' 19import { VideoDetails } from '../../../shared/models/videos'
20 20
21const expect = chai.expect 21const expect = chai.expect
22 22
@@ -62,7 +62,6 @@ describe('Test create import video jobs', function () {
62 62
63 await waitJobs(servers) 63 await waitJobs(servers)
64 64
65 let magnetUri: string
66 for (const server of servers) { 65 for (const server of servers) {
67 const { data: videos } = (await getVideosList(server.url)).body 66 const { data: videos } = (await getVideosList(server.url)).body
68 expect(videos).to.have.lengthOf(2) 67 expect(videos).to.have.lengthOf(2)
@@ -74,9 +73,6 @@ describe('Test create import video jobs', function () {
74 const [ originalVideo, transcodedVideo ] = videoDetail.files 73 const [ originalVideo, transcodedVideo ] = videoDetail.files
75 assertVideoProperties(originalVideo, 720, 'webm', 218910) 74 assertVideoProperties(originalVideo, 720, 'webm', 218910)
76 assertVideoProperties(transcodedVideo, 480, 'webm', 69217) 75 assertVideoProperties(transcodedVideo, 480, 'webm', 69217)
77
78 if (!magnetUri) magnetUri = transcodedVideo.magnetUri
79 else expect(transcodedVideo.magnetUri).to.equal(magnetUri)
80 } 76 }
81 }) 77 })
82 78
@@ -86,7 +82,6 @@ describe('Test create import video jobs', function () {
86 82
87 await waitJobs(servers) 83 await waitJobs(servers)
88 84
89 let magnetUri: string
90 for (const server of servers) { 85 for (const server of servers) {
91 const { data: videos } = (await getVideosList(server.url)).body 86 const { data: videos } = (await getVideosList(server.url)).body
92 expect(videos).to.have.lengthOf(2) 87 expect(videos).to.have.lengthOf(2)
@@ -100,9 +95,6 @@ describe('Test create import video jobs', function () {
100 assertVideoProperties(transcodedVideo420, 480, 'mp4') 95 assertVideoProperties(transcodedVideo420, 480, 'mp4')
101 assertVideoProperties(transcodedVideo320, 360, 'mp4') 96 assertVideoProperties(transcodedVideo320, 360, 'mp4')
102 assertVideoProperties(transcodedVideo240, 240, 'mp4') 97 assertVideoProperties(transcodedVideo240, 240, 'mp4')
103
104 if (!magnetUri) magnetUri = originalVideo.magnetUri
105 else expect(originalVideo.magnetUri).to.equal(magnetUri)
106 } 98 }
107 }) 99 })
108 100
@@ -112,7 +104,6 @@ describe('Test create import video jobs', function () {
112 104
113 await waitJobs(servers) 105 await waitJobs(servers)
114 106
115 let magnetUri: string
116 for (const server of servers) { 107 for (const server of servers) {
117 const { data: videos } = (await getVideosList(server.url)).body 108 const { data: videos } = (await getVideosList(server.url)).body
118 expect(videos).to.have.lengthOf(2) 109 expect(videos).to.have.lengthOf(2)
@@ -124,9 +115,6 @@ describe('Test create import video jobs', function () {
124 const [ video720, video480 ] = videoDetail.files 115 const [ video720, video480 ] = videoDetail.files
125 assertVideoProperties(video720, 720, 'webm', 942961) 116 assertVideoProperties(video720, 720, 'webm', 942961)
126 assertVideoProperties(video480, 480, 'webm', 69217) 117 assertVideoProperties(video480, 480, 'webm', 69217)
127
128 if (!magnetUri) magnetUri = video720.magnetUri
129 else expect(video720.magnetUri).to.equal(magnetUri)
130 } 118 }
131 }) 119 })
132 120
diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts
index 2e05d8753..77790daa4 100644
--- a/server/types/models/video/video-channels.ts
+++ b/server/types/models/video/video-channels.ts
@@ -17,6 +17,7 @@ import {
17 MActorDefault, 17 MActorDefault,
18 MActorDefaultLight, 18 MActorDefaultLight,
19 MActorFormattable, 19 MActorFormattable,
20 MActorHost,
20 MActorLight, 21 MActorLight,
21 MActorSummary, 22 MActorSummary,
22 MActorSummaryFormattable, MActorUrl 23 MActorSummaryFormattable, MActorUrl
@@ -71,6 +72,10 @@ export type MChannelAccountLight =
71 Use<'Actor', MActorDefaultLight> & 72 Use<'Actor', MActorDefaultLight> &
72 Use<'Account', MAccountLight> 73 Use<'Account', MAccountLight>
73 74
75export type MChannelHost =
76 MChannelId &
77 Use<'Actor', MActorHost>
78
74// ############################################################################ 79// ############################################################################
75 80
76// Account associations 81// Account associations
diff --git a/server/types/models/video/video.ts b/server/types/models/video/video.ts
index ae23cc30f..92dcbaf59 100644
--- a/server/types/models/video/video.ts
+++ b/server/types/models/video/video.ts
@@ -1,27 +1,28 @@
1import { VideoModel } from '../../../models/video/video'
2import { PickWith, PickWithOpt } from '@shared/core-utils' 1import { PickWith, PickWithOpt } from '@shared/core-utils'
2import { VideoModel } from '../../../models/video/video'
3import { MUserVideoHistoryTime } from '../user/user-video-history'
4import { MScheduleVideoUpdate } from './schedule-video-update'
5import { MTag } from './tag'
6import { MThumbnail } from './thumbnail'
7import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
8import { MVideoCaptionLanguage, MVideoCaptionLanguageUrl } from './video-caption'
3import { 9import {
4 MChannelAccountDefault, 10 MChannelAccountDefault,
5 MChannelAccountLight, 11 MChannelAccountLight,
6 MChannelAccountSummaryFormattable, 12 MChannelAccountSummaryFormattable,
7 MChannelActor, 13 MChannelActor,
8 MChannelFormattable, 14 MChannelFormattable,
15 MChannelHost,
9 MChannelUserId 16 MChannelUserId
10} from './video-channels' 17} from './video-channels'
11import { MTag } from './tag' 18import { MVideoFile, MVideoFileRedundanciesAll, MVideoFileRedundanciesOpt } from './video-file'
12import { MVideoCaptionLanguage, MVideoCaptionLanguageUrl } from './video-caption' 19import { MVideoLive } from './video-live'
13import { 20import {
14 MStreamingPlaylistFiles, 21 MStreamingPlaylistFiles,
15 MStreamingPlaylistRedundancies, 22 MStreamingPlaylistRedundancies,
16 MStreamingPlaylistRedundanciesAll, 23 MStreamingPlaylistRedundanciesAll,
17 MStreamingPlaylistRedundanciesOpt 24 MStreamingPlaylistRedundanciesOpt
18} from './video-streaming-playlist' 25} from './video-streaming-playlist'
19import { MVideoFile, MVideoFileRedundanciesAll, MVideoFileRedundanciesOpt } from './video-file'
20import { MThumbnail } from './thumbnail'
21import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
22import { MScheduleVideoUpdate } from './schedule-video-update'
23import { MUserVideoHistoryTime } from '../user/user-video-history'
24import { MVideoLive } from './video-live'
25 26
26type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M> 27type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
27 28
@@ -143,6 +144,10 @@ export type MVideoWithChannelActor =
143 MVideo & 144 MVideo &
144 Use<'VideoChannel', MChannelActor> 145 Use<'VideoChannel', MChannelActor>
145 146
147export type MVideoWithHost =
148 MVideo &
149 Use<'VideoChannel', MChannelHost>
150
146export type MVideoFullLight = 151export type MVideoFullLight =
147 MVideo & 152 MVideo &
148 Use<'Thumbnails', MThumbnail[]> & 153 Use<'Thumbnails', MThumbnail[]> &