1 import cors from 'cors'
2 import express from 'express'
3 import { logger } from '@server/helpers/logger'
4 import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
5 import { Hooks } from '@server/lib/plugins/hooks'
6 import { VideoPathManager } from '@server/lib/video-path-manager'
7 import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
8 import { addQueryParams, forceNumber } from '@shared/core-utils'
9 import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models'
10 import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
11 import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares'
13 const downloadRouter = express.Router()
15 downloadRouter.use(cors())
18 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
19 asyncMiddleware(downloadTorrent)
23 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
25 asyncMiddleware(videosDownloadValidator),
26 asyncMiddleware(downloadVideoFile)
30 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
32 asyncMiddleware(videosDownloadValidator),
33 asyncMiddleware(downloadHLSVideoFile)
36 // ---------------------------------------------------------------------------
42 // ---------------------------------------------------------------------------
44 async function downloadTorrent (req: express.Request, res: express.Response) {
45 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
48 status: HttpStatusCode.NOT_FOUND_404,
49 message: 'Torrent file not found'
53 const allowParameters = {
56 torrentPath: result.path,
57 downloadName: result.downloadName
60 const allowedResult = await Hooks.wrapFun(
61 isTorrentDownloadAllowed,
63 'filter:api.download.torrent.allowed.result'
66 if (!checkAllowResult(res, allowParameters, allowedResult)) return
68 return res.download(result.path, result.downloadName)
71 async function downloadVideoFile (req: express.Request, res: express.Response) {
72 const video = res.locals.videoAll
74 const videoFile = getVideoFile(req, video.VideoFiles)
77 status: HttpStatusCode.NOT_FOUND_404,
78 message: 'Video file not found'
82 const allowParameters = {
89 const allowedResult = await Hooks.wrapFun(
90 isVideoDownloadAllowed,
92 'filter:api.download.video.allowed.result'
95 if (!checkAllowResult(res, allowParameters, allowedResult)) return
97 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
98 return redirectToObjectStorage({ req, res, video, file: videoFile })
101 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => {
102 // Express uses basename on filename parameter
103 const videoName = video.name.replace(/[/\\]/g, '_')
104 const filename = `${videoName}-${videoFile.resolution}p${videoFile.extname}`
106 return res.download(path, filename)
110 async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
111 const video = res.locals.videoAll
112 const streamingPlaylist = getHLSPlaylist(video)
113 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
115 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
118 status: HttpStatusCode.NOT_FOUND_404,
119 message: 'Video file not found'
123 const allowParameters = {
131 const allowedResult = await Hooks.wrapFun(
132 isVideoDownloadAllowed,
134 'filter:api.download.video.allowed.result'
137 if (!checkAllowResult(res, allowParameters, allowedResult)) return
139 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
140 return redirectToObjectStorage({ req, res, video, file: videoFile })
143 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => {
144 const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
146 return res.download(path, filename)
150 function getVideoFile (req: express.Request, files: MVideoFile[]) {
151 const resolution = forceNumber(req.params.resolution)
152 return files.find(f => f.resolution === resolution)
155 function getHLSPlaylist (video: MVideoFullLight) {
156 const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
157 if (!playlist) return undefined
159 return Object.assign(playlist, { Video: video })
162 type AllowedResult = {
164 errorMessage?: string
167 function isTorrentDownloadAllowed (_object: {
170 return { allowed: true }
173 function isVideoDownloadAllowed (_object: {
175 videoFile: MVideoFile
176 streamingPlaylist?: MStreamingPlaylist
178 return { allowed: true }
181 function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
182 if (!result || result.allowed !== true) {
183 logger.info('Download is not allowed.', { result, allowParameters })
186 status: HttpStatusCode.FORBIDDEN_403,
187 message: result?.errorMessage || 'Refused download'
195 function redirectToObjectStorage (options: {
197 res: express.Response
201 const { req, res, video, file } = options
203 const baseUrl = file.getObjectStorageUrl(video)
205 const url = video.hasPrivateStaticPath() && req.query.videoFileToken
206 ? addQueryParams(baseUrl, { videoFileToken: req.query.videoFileToken })
209 return res.redirect(url)