Add hooks support for video download
This commit is contained in:
parent
c0ab041c2c
commit
4bc45da342
|
@ -1,7 +1,9 @@
|
|||
import { mapValues, pick } from 'lodash-es'
|
||||
import { pipe } from 'rxjs'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { AuthService, HooksService, Notifier } from '@app/core'
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
|
||||
import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
|
||||
|
||||
|
@ -26,7 +28,7 @@ export class VideoDownloadComponent {
|
|||
videoFileMetadataVideoStream: FileMetadata | undefined
|
||||
videoFileMetadataAudioStream: FileMetadata | undefined
|
||||
videoCaptions: VideoCaption[]
|
||||
activeModal: NgbActiveModal
|
||||
activeModal: NgbModalRef
|
||||
|
||||
type: DownloadType = 'video'
|
||||
|
||||
|
@ -38,7 +40,8 @@ export class VideoDownloadComponent {
|
|||
private notifier: Notifier,
|
||||
private modalService: NgbModal,
|
||||
private videoService: VideoService,
|
||||
private auth: AuthService
|
||||
private auth: AuthService,
|
||||
private hooks: HooksService
|
||||
) {
|
||||
this.bytesPipe = new BytesPipe()
|
||||
this.numbersPipe = new NumberFormatterPipe(this.localeId)
|
||||
|
@ -64,7 +67,12 @@ export class VideoDownloadComponent {
|
|||
|
||||
this.resolutionId = this.getVideoFiles()[0].resolution.id
|
||||
this.onResolutionIdChange()
|
||||
|
||||
if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
|
||||
|
||||
this.activeModal.shown.subscribe(() => {
|
||||
this.hooks.runAction('action:modal.video-download.shown', 'common')
|
||||
})
|
||||
}
|
||||
|
||||
onClose () {
|
||||
|
@ -88,6 +96,7 @@ export class VideoDownloadComponent {
|
|||
if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
|
||||
|
||||
await this.hydrateMetadataFromMetadataUrl(this.videoFile)
|
||||
if (!this.videoFile.metadata) return
|
||||
|
||||
this.videoFileMetadataFormat = this.videoFile
|
||||
? this.getMetadataFormat(this.videoFile.metadata.format)
|
||||
|
@ -201,7 +210,7 @@ export class VideoDownloadComponent {
|
|||
|
||||
private hydrateMetadataFromMetadataUrl (file: VideoFile) {
|
||||
const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
|
||||
observable.subscribe(res => file.metadata = res)
|
||||
.pipe(tap(res => file.metadata = res))
|
||||
|
||||
return observable.toPromise()
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import * as cors from 'cors'
|
||||
import * as express from 'express'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||
import { VideoStreamingPlaylistType } from '@shared/models'
|
||||
import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
|
||||
|
@ -14,19 +16,19 @@ downloadRouter.use(cors())
|
|||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
|
||||
downloadTorrent
|
||||
asyncMiddleware(downloadTorrent)
|
||||
)
|
||||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadVideoFile
|
||||
asyncMiddleware(downloadVideoFile)
|
||||
)
|
||||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadHLSVideoFile
|
||||
asyncMiddleware(downloadHLSVideoFile)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
|
|||
const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
|
||||
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
|
||||
|
||||
const allowedResult = await Hooks.wrapFun(
|
||||
isTorrentDownloadAllowed,
|
||||
allowParameters,
|
||||
'filter:api.download.torrent.allowed.result'
|
||||
)
|
||||
|
||||
if (!checkAllowResult(res, allowParameters, allowedResult)) return
|
||||
|
||||
return res.download(result.path, result.downloadName)
|
||||
}
|
||||
|
||||
function downloadVideoFile (req: express.Request, res: express.Response) {
|
||||
async function downloadVideoFile (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
|
||||
const videoFile = getVideoFile(req, video.VideoFiles)
|
||||
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
||||
|
||||
const allowParameters = { video, videoFile }
|
||||
|
||||
const allowedResult = await Hooks.wrapFun(
|
||||
isVideoDownloadAllowed,
|
||||
allowParameters,
|
||||
'filter:api.download.video.allowed.result'
|
||||
)
|
||||
|
||||
if (!checkAllowResult(res, allowParameters, allowedResult)) return
|
||||
|
||||
return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
|
||||
}
|
||||
|
||||
function downloadHLSVideoFile (req: express.Request, res: express.Response) {
|
||||
async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
const playlist = getHLSPlaylist(video)
|
||||
if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end
|
||||
const streamingPlaylist = getHLSPlaylist(video)
|
||||
if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
|
||||
|
||||
const videoFile = getVideoFile(req, playlist.VideoFiles)
|
||||
const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
|
||||
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
||||
|
||||
const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
|
||||
return res.download(getVideoFilePath(playlist, videoFile), filename)
|
||||
const allowParameters = { video, streamingPlaylist, videoFile }
|
||||
|
||||
const allowedResult = await Hooks.wrapFun(
|
||||
isVideoDownloadAllowed,
|
||||
allowParameters,
|
||||
'filter:api.download.video.allowed.result'
|
||||
)
|
||||
|
||||
if (!checkAllowResult(res, allowParameters, allowedResult)) return
|
||||
|
||||
const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
|
||||
return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
|
||||
}
|
||||
|
||||
function getVideoFile (req: express.Request, files: MVideoFile[]) {
|
||||
|
@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
|
|||
|
||||
return Object.assign(playlist, { Video: video })
|
||||
}
|
||||
|
||||
type AllowedResult = {
|
||||
allowed: boolean
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
function isTorrentDownloadAllowed (_object: {
|
||||
torrentPath: string
|
||||
}): AllowedResult {
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
function isVideoDownloadAllowed (_object: {
|
||||
video: MVideo
|
||||
videoFile: MVideoFile
|
||||
streamingPlaylist?: MStreamingPlaylist
|
||||
}): AllowedResult {
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
|
||||
if (!result || result.allowed !== true) {
|
||||
logger.info('Download is not allowed.', { result, allowParameters })
|
||||
res.status(HttpStatusCode.FORBIDDEN_403)
|
||||
.json({ error: result.errorMessage || 'Refused download' })
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config'
|
|||
import { FILES_CACHE } from '../../initializers/constants'
|
||||
import { VideoModel } from '../../models/video/video'
|
||||
import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
|
||||
import { MVideo, MVideoFile } from '@server/types/models'
|
||||
|
||||
class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
|
||||
|
||||
|
@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
|
|||
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
|
||||
if (!file) return undefined
|
||||
|
||||
if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) }
|
||||
if (file.getVideo().isOwned()) {
|
||||
const downloadName = this.buildDownloadName(file.getVideo(), file)
|
||||
|
||||
return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
|
||||
}
|
||||
|
||||
return this.loadRemoteFile(filename)
|
||||
}
|
||||
|
@ -43,10 +48,14 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
|
|||
|
||||
await doRequestAndSaveToFile(remoteUrl, destPath)
|
||||
|
||||
const downloadName = `${video.name}-${file.resolution}p.torrent`
|
||||
const downloadName = this.buildDownloadName(video, file)
|
||||
|
||||
return { isOwned: false, path: destPath, downloadName }
|
||||
}
|
||||
|
||||
private buildDownloadName (video: MVideo, file: MVideoFile) {
|
||||
return `${video.name}-${file.resolution}p.torrent`
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -184,6 +184,32 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
|
|||
return result
|
||||
}
|
||||
})
|
||||
|
||||
registerHook({
|
||||
target: 'filter:api.download.torrent.allowed.result',
|
||||
handler: (result, params) => {
|
||||
if (params && params.downloadName.includes('bad torrent')) {
|
||||
return { allowed: false, errorMessage: 'Liu Bei' }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
registerHook({
|
||||
target: 'filter:api.download.video.allowed.result',
|
||||
handler: (result, params) => {
|
||||
if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) {
|
||||
return { allowed: false, errorMessage: 'Cao Cao' }
|
||||
}
|
||||
|
||||
if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) {
|
||||
return { allowed: false, errorMessage: 'Sun Jian' }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function unregister () {
|
||||
|
|
|
@ -20,12 +20,14 @@ import {
|
|||
getVideoThreadComments,
|
||||
getVideoWithToken,
|
||||
installPlugin,
|
||||
makeRawRequest,
|
||||
registerUser,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
updateCustomSubConfig,
|
||||
updateVideo,
|
||||
uploadVideo,
|
||||
uploadVideoAndGetId,
|
||||
waitJobs
|
||||
} from '../../../shared/extra-utils'
|
||||
import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers'
|
||||
|
@ -355,6 +357,67 @@ describe('Test plugin filter hooks', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Download hooks', function () {
|
||||
const downloadVideos: VideoDetails[] = []
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
|
||||
transcoding: {
|
||||
webtorrent: {
|
||||
enabled: true
|
||||
},
|
||||
hls: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const uuids: string[] = []
|
||||
|
||||
for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
|
||||
const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
|
||||
uuids.push(uuid)
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const uuid of uuids) {
|
||||
const res = await getVideo(servers[0].url, uuid)
|
||||
downloadVideos.push(res.body)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should run filter:api.download.torrent.allowed.result', async function () {
|
||||
const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403)
|
||||
expect(res.body.error).to.equal('Liu Bei')
|
||||
|
||||
await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200)
|
||||
await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200)
|
||||
})
|
||||
|
||||
it('Should run filter:api.download.video.allowed.result', async function () {
|
||||
{
|
||||
const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403)
|
||||
expect(res.body.error).to.equal('Cao Cao')
|
||||
|
||||
await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200)
|
||||
await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
|
||||
}
|
||||
|
||||
{
|
||||
const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403)
|
||||
expect(res.body.error).to.equal('Sun Jian')
|
||||
|
||||
await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
|
||||
|
||||
await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
|
||||
await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -85,8 +85,12 @@ export const clientActionHookObject = {
|
|||
// Fired when the registration page is being initialized
|
||||
'action:signup.register.init': true,
|
||||
|
||||
// Fired when the modal to download a video/caption is shown
|
||||
'action:modal.video-download.shown': true,
|
||||
|
||||
// ####### Embed hooks #######
|
||||
// In embed scope, peertube helpers are not available
|
||||
// /!\ In embed scope, peertube helpers are not available
|
||||
// ###########################
|
||||
|
||||
// Fired when the embed loaded the player
|
||||
'action:embed.player.loaded': true
|
||||
|
|
|
@ -50,7 +50,11 @@ export const serverFilterHookObject = {
|
|||
'filter:video.auto-blacklist.result': true,
|
||||
|
||||
// Filter result used to check if a user can register on the instance
|
||||
'filter:api.user.signup.allowed.result': true
|
||||
'filter:api.user.signup.allowed.result': true,
|
||||
|
||||
// Filter result used to check if video/torrent download is allowed
|
||||
'filter:api.download.video.allowed.result': true,
|
||||
'filter:api.download.torrent.allowed.result': true
|
||||
}
|
||||
|
||||
export type ServerFilterHookName = keyof typeof serverFilterHookObject
|
||||
|
|
Loading…
Reference in New Issue