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