Dissociate video file names and video uuid
This commit is contained in:
parent
684cdacbbd
commit
90a8bd305d
|
@ -197,6 +197,8 @@ cache:
|
|||
size: 500 # Max number of previews you want to cache
|
||||
captions:
|
||||
size: 500 # Max number of video captions/subtitles you want to cache
|
||||
torrents:
|
||||
size: 500 # Max number of video torrents you want to cache
|
||||
|
||||
admin:
|
||||
# Used to generate the root user at first startup
|
||||
|
|
|
@ -208,6 +208,8 @@ cache:
|
|||
size: 500 # Max number of previews you want to cache
|
||||
captions:
|
||||
size: 500 # Max number of video captions/subtitles you want to cache
|
||||
torrents:
|
||||
size: 500 # Max number of video torrents you want to cache
|
||||
|
||||
admin:
|
||||
# Used to generate the root user at first startup
|
||||
|
|
|
@ -34,7 +34,9 @@ async function run () {
|
|||
|
||||
const localVideos = await VideoModel.listLocal()
|
||||
|
||||
for (const video of localVideos) {
|
||||
for (const localVideo of localVideos) {
|
||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(localVideo.id)
|
||||
|
||||
currentVideoId = video.id
|
||||
|
||||
for (const file of video.VideoFiles) {
|
||||
|
@ -70,7 +72,7 @@ async function run () {
|
|||
|
||||
console.log('Failed to optimize %s, restoring original', basename(currentFile))
|
||||
await move(backupFile, currentFile, { overwrite: true })
|
||||
await createTorrentAndSetInfoHash(video, file)
|
||||
await createTorrentAndSetInfoHash(video, video, file)
|
||||
await file.save()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,8 +116,10 @@ async function run () {
|
|||
|
||||
console.log('Updating video and torrent files.')
|
||||
|
||||
const videos = await VideoModel.listLocal()
|
||||
for (const video of videos) {
|
||||
const localVideos = await VideoModel.listLocal()
|
||||
for (const localVideo of localVideos) {
|
||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(localVideo.id)
|
||||
|
||||
console.log('Updating video ' + video.uuid)
|
||||
|
||||
video.url = getLocalVideoActivityPubUrl(video)
|
||||
|
@ -125,7 +127,7 @@ async function run () {
|
|||
|
||||
for (const file of video.VideoFiles) {
|
||||
console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid)
|
||||
await createTorrentAndSetInfoHash(video, file)
|
||||
await createTorrentAndSetInfoHash(video, video, file)
|
||||
}
|
||||
|
||||
for (const playlist of video.VideoStreamingPlaylists) {
|
||||
|
|
|
@ -103,7 +103,8 @@ import {
|
|||
webfingerRouter,
|
||||
trackerRouter,
|
||||
createWebsocketTrackerServer,
|
||||
botsRouter
|
||||
botsRouter,
|
||||
downloadRouter
|
||||
} from './server/controllers'
|
||||
import { advertiseDoNotTrack } from './server/middlewares/dnt'
|
||||
import { Redis } from './server/lib/redis'
|
||||
|
@ -123,6 +124,7 @@ import { Hooks } from './server/lib/plugins/hooks'
|
|||
import { PluginManager } from './server/lib/plugins/plugin-manager'
|
||||
import { LiveManager } from './server/lib/live-manager'
|
||||
import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
|
||||
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
|
||||
|
||||
// ----------- Command line -----------
|
||||
|
||||
|
@ -202,6 +204,7 @@ app.use('/', botsRouter)
|
|||
|
||||
// Static files
|
||||
app.use('/', staticRouter)
|
||||
app.use('/', downloadRouter)
|
||||
app.use('/', lazyStaticRouter)
|
||||
|
||||
// Client files, last valid routes!
|
||||
|
@ -258,6 +261,7 @@ async function startApplication () {
|
|||
// Caches initializations
|
||||
VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE)
|
||||
VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE)
|
||||
VideosTorrentCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE)
|
||||
|
||||
// Enable Schedulers
|
||||
ActorFollowScheduler.Instance.enable()
|
||||
|
|
|
@ -7,7 +7,7 @@ import { changeVideoChannelShare } from '@server/lib/activitypub/share'
|
|||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
||||
import { LiveManager } from '@server/lib/live-manager'
|
||||
import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
|
||||
import { getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { MVideoFullLight } from '@server/types/models'
|
||||
import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared'
|
||||
|
@ -189,6 +189,7 @@ async function addVideo (req: express.Request, res: express.Response) {
|
|||
videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware
|
||||
|
||||
const video = new VideoModel(videoData) as MVideoFullLight
|
||||
video.VideoChannel = res.locals.videoChannel
|
||||
video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
|
||||
|
||||
const videoFile = new VideoFileModel({
|
||||
|
@ -205,6 +206,8 @@ async function addVideo (req: express.Request, res: express.Response) {
|
|||
videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
|
||||
}
|
||||
|
||||
videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
|
||||
|
||||
// Move physical file
|
||||
const destination = getVideoFilePath(video, videoFile)
|
||||
await move(videoPhysicalFile.path, destination)
|
||||
|
@ -219,7 +222,7 @@ async function addVideo (req: express.Request, res: express.Response) {
|
|||
})
|
||||
|
||||
// Create the torrent file
|
||||
await createTorrentAndSetInfoHash(video, videoFile)
|
||||
await createTorrentAndSetInfoHash(video, video, videoFile)
|
||||
|
||||
const { videoCreated } = await sequelizeTypescript.transaction(async t => {
|
||||
const sequelizeOptions = { transaction: t }
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import * as cors from 'cors'
|
||||
import * as express from 'express'
|
||||
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
|
||||
import { getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { 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'
|
||||
import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
|
||||
|
||||
const downloadRouter = express.Router()
|
||||
|
||||
downloadRouter.use(cors())
|
||||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
|
||||
downloadTorrent
|
||||
)
|
||||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadVideoFile
|
||||
)
|
||||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadHLSVideoFile
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
downloadRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
|
||||
return res.download(result.path, result.downloadName)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
|
||||
}
|
||||
|
||||
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 videoFile = getVideoFile(req, playlist.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)
|
||||
}
|
||||
|
||||
function getVideoFile (req: express.Request, files: MVideoFile[]) {
|
||||
const resolution = parseInt(req.params.resolution, 10)
|
||||
return files.find(f => f.resolution === resolution)
|
||||
}
|
||||
|
||||
function getHLSPlaylist (video: MVideoFullLight) {
|
||||
const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||
if (!playlist) return undefined
|
||||
|
||||
return Object.assign(playlist, { Video: video })
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
export * from './activitypub'
|
||||
export * from './api'
|
||||
export * from './client'
|
||||
export * from './download'
|
||||
export * from './feeds'
|
||||
export * from './services'
|
||||
export * from './static'
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import * as cors from 'cors'
|
||||
import * as express from 'express'
|
||||
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
|
||||
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
|
||||
import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar'
|
||||
import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
|
||||
import { asyncMiddleware } from '../middlewares'
|
||||
import { AvatarModel } from '../models/avatar/avatar'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar'
|
||||
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
|
||||
|
||||
const lazyStaticRouter = express.Router()
|
||||
|
||||
|
@ -27,6 +28,11 @@ lazyStaticRouter.use(
|
|||
asyncMiddleware(getVideoCaption)
|
||||
)
|
||||
|
||||
lazyStaticRouter.use(
|
||||
LAZY_STATIC_PATHS.TORRENTS + ':filename',
|
||||
asyncMiddleware(getTorrent)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -67,19 +73,26 @@ async function getAvatar (req: express.Request, res: express.Response) {
|
|||
const path = avatar.getPath()
|
||||
|
||||
avatarPathUnsafeCache.set(filename, path)
|
||||
return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
|
||||
return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
|
||||
}
|
||||
|
||||
async function getPreview (req: express.Request, res: express.Response) {
|
||||
const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
|
||||
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
|
||||
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
|
||||
}
|
||||
|
||||
async function getVideoCaption (req: express.Request, res: express.Response) {
|
||||
const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename)
|
||||
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
|
||||
}
|
||||
|
||||
async function getTorrent (req: express.Request, res: express.Response) {
|
||||
const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
|
||||
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
|
||||
}
|
||||
|
|
|
@ -3,10 +3,7 @@ import * as express from 'express'
|
|||
import { join } from 'path'
|
||||
import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
|
||||
import { serveIndexHTML } from '@server/lib/client-html'
|
||||
import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||
import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
|
||||
import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
|
||||
import { root } from '../helpers/core-utils'
|
||||
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
||||
|
@ -16,14 +13,13 @@ import {
|
|||
HLS_STREAMING_PLAYLIST_DIRECTORY,
|
||||
PEERTUBE_VERSION,
|
||||
ROUTE_CACHE_LIFETIME,
|
||||
STATIC_DOWNLOAD_PATHS,
|
||||
STATIC_MAX_AGE,
|
||||
STATIC_PATHS,
|
||||
WEBSERVER
|
||||
} from '../initializers/constants'
|
||||
import { getThemeOrDefault } from '../lib/plugins/theme-utils'
|
||||
import { getEnabledResolutions } from '../lib/video-transcoding'
|
||||
import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
|
||||
import { asyncMiddleware } from '../middlewares'
|
||||
import { cacheRoute } from '../middlewares/cache'
|
||||
import { UserModel } from '../models/account/user'
|
||||
import { VideoModel } from '../models/video/video'
|
||||
|
@ -37,47 +33,23 @@ staticRouter.use(cors())
|
|||
Cors is very important to let other servers access torrent and video files
|
||||
*/
|
||||
|
||||
// FIXME: deprecated in 3.2, use lazy-statics instead
|
||||
const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR
|
||||
staticRouter.use(
|
||||
STATIC_PATHS.TORRENTS,
|
||||
cors(),
|
||||
express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file
|
||||
)
|
||||
staticRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadTorrent
|
||||
)
|
||||
staticRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadHLSVideoFileTorrent
|
||||
)
|
||||
|
||||
// Videos path for webseeding
|
||||
// Videos path for webseed
|
||||
staticRouter.use(
|
||||
STATIC_PATHS.WEBSEED,
|
||||
cors(),
|
||||
express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
|
||||
)
|
||||
staticRouter.use(
|
||||
STATIC_PATHS.REDUNDANCY,
|
||||
cors(),
|
||||
express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
|
||||
)
|
||||
|
||||
staticRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadVideoFile
|
||||
)
|
||||
|
||||
staticRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
downloadHLSVideoFile
|
||||
)
|
||||
|
||||
// HLS
|
||||
staticRouter.use(
|
||||
STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
|
||||
|
@ -327,60 +299,6 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
|
|||
return res.send(json).end()
|
||||
}
|
||||
|
||||
function downloadTorrent (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()
|
||||
|
||||
return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
|
||||
}
|
||||
|
||||
function downloadHLSVideoFileTorrent (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 videoFile = getVideoFile(req, playlist.VideoFiles)
|
||||
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
||||
|
||||
return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
|
||||
}
|
||||
|
||||
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 videoFile = getVideoFile(req, playlist.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)
|
||||
}
|
||||
|
||||
function getVideoFile (req: express.Request, files: MVideoFile[]) {
|
||||
const resolution = parseInt(req.params.resolution, 10)
|
||||
return files.find(f => f.resolution === resolution)
|
||||
}
|
||||
|
||||
function getHLSPlaylist (video: MVideoFullLight) {
|
||||
const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||
if (!playlist) return undefined
|
||||
|
||||
return Object.assign(playlist, { Video: video })
|
||||
}
|
||||
|
||||
function getCup (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
res.status(HttpStatusCode.I_AM_A_TEAPOT_418)
|
||||
res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1')
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import { URL } from 'url'
|
||||
import validator from 'validator'
|
||||
import { ContextType } from '@shared/models/activitypub/context'
|
||||
import { ResultList } from '../../shared/models'
|
||||
import { Activity } from '../../shared/models/activitypub'
|
||||
import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
|
||||
import { signJsonLDObject } from './peertube-crypto'
|
||||
import { MActor, MVideoWithHost } from '../types/models'
|
||||
import { pageToStartAndCount } from './core-utils'
|
||||
import { URL } from 'url'
|
||||
import { MActor, MVideoAccountLight } from '../types/models'
|
||||
import { ContextType } from '@shared/models/activitypub/context'
|
||||
import { signJsonLDObject } from './peertube-crypto'
|
||||
|
||||
function getContextData (type: ContextType) {
|
||||
const context: any[] = [
|
||||
|
@ -201,8 +201,8 @@ function checkUrlsSameHost (url1: string, url2: string) {
|
|||
return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
|
||||
}
|
||||
|
||||
function buildRemoteVideoBaseUrl (video: MVideoAccountLight, path: string) {
|
||||
const host = video.VideoChannel.Account.Actor.Server.host
|
||||
function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string) {
|
||||
const host = video.VideoChannel.Actor.Server.host
|
||||
|
||||
return REMOTE_SCHEME.HTTP + '://' + host + path
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import * as createTorrent from 'create-torrent'
|
||||
import { createWriteStream, ensureDir, remove, writeFile } from 'fs-extra'
|
||||
import * as magnetUtil from 'magnet-uri'
|
||||
import * as parseTorrent from 'parse-torrent'
|
||||
import { dirname, join } from 'path'
|
||||
import * as WebTorrent from 'webtorrent'
|
||||
import { isArray } from '@server/helpers/custom-validators/misc'
|
||||
import { WEBSERVER } from '@server/initializers/constants'
|
||||
import { generateTorrentFileName, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { MVideo, MVideoWithHost } from '@server/types/models/video/video'
|
||||
import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/types/models/video/video-file'
|
||||
import { MStreamingPlaylistVideo } from '@server/types/models/video/video-streaming-playlist'
|
||||
import { CONFIG } from '../initializers/config'
|
||||
import { promisify2 } from './core-utils'
|
||||
import { logger } from './logger'
|
||||
import { generateVideoImportTmpPath } from './utils'
|
||||
import * as WebTorrent from 'webtorrent'
|
||||
import { createWriteStream, ensureDir, remove, writeFile } from 'fs-extra'
|
||||
import { CONFIG } from '../initializers/config'
|
||||
import { dirname, join } from 'path'
|
||||
import * as createTorrent from 'create-torrent'
|
||||
import { promisify2 } from './core-utils'
|
||||
import { MVideo } from '@server/types/models/video/video'
|
||||
import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/types/models/video/video-file'
|
||||
import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/types/models/video/video-streaming-playlist'
|
||||
import { WEBSERVER } from '@server/initializers/constants'
|
||||
import * as parseTorrent from 'parse-torrent'
|
||||
import * as magnetUtil from 'magnet-uri'
|
||||
import { isArray } from '@server/helpers/custom-validators/misc'
|
||||
import { getTorrentFileName, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { extractVideo } from '@server/helpers/video'
|
||||
|
||||
const createTorrentPromise = promisify2<string, any, any>(createTorrent)
|
||||
|
||||
|
@ -78,10 +77,12 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
|
|||
})
|
||||
}
|
||||
|
||||
async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
|
||||
const video = extractVideo(videoOrPlaylist)
|
||||
const { baseUrlHttp } = video.getBaseUrls()
|
||||
|
||||
// FIXME: refactor/merge videoOrPlaylist and video arguments
|
||||
async function createTorrentAndSetInfoHash (
|
||||
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
||||
video: MVideoWithHost,
|
||||
videoFile: MVideoFile
|
||||
) {
|
||||
const options = {
|
||||
// Keep the extname, it's used by the client to stream the file inside a web browser
|
||||
name: `${video.name} ${videoFile.resolution}p${videoFile.extname}`,
|
||||
|
@ -90,33 +91,33 @@ async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreaming
|
|||
[ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
|
||||
[ WEBSERVER.URL + '/tracker/announce' ]
|
||||
],
|
||||
urlList: [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ]
|
||||
urlList: [ videoFile.getFileUrl(video) ]
|
||||
}
|
||||
|
||||
const torrent = await createTorrentPromise(getVideoFilePath(videoOrPlaylist, videoFile), options)
|
||||
|
||||
const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile))
|
||||
logger.info('Creating torrent %s.', filePath)
|
||||
const torrentFilename = generateTorrentFileName(videoOrPlaylist, videoFile.resolution)
|
||||
const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, torrentFilename)
|
||||
logger.info('Creating torrent %s.', torrentPath)
|
||||
|
||||
await writeFile(filePath, torrent)
|
||||
await writeFile(torrentPath, torrent)
|
||||
|
||||
const parsedTorrent = parseTorrent(torrent)
|
||||
videoFile.infoHash = parsedTorrent.infoHash
|
||||
videoFile.torrentFilename = torrentFilename
|
||||
}
|
||||
|
||||
// FIXME: merge/refactor videoOrPlaylist and video arguments
|
||||
function generateMagnetUri (
|
||||
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
||||
video: MVideoWithHost,
|
||||
videoFile: MVideoFileRedundanciesOpt,
|
||||
baseUrlHttp: string,
|
||||
baseUrlWs: string
|
||||
) {
|
||||
const video = isStreamingPlaylist(videoOrPlaylist)
|
||||
? videoOrPlaylist.Video
|
||||
: videoOrPlaylist
|
||||
|
||||
const xs = videoOrPlaylist.getTorrentUrl(videoFile, baseUrlHttp)
|
||||
const xs = videoFile.getTorrentUrl()
|
||||
const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs)
|
||||
let urlList = [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ]
|
||||
let urlList = [ videoFile.getFileUrl(video) ]
|
||||
|
||||
const redundancies = videoFile.RedundancyVideos
|
||||
if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
|
||||
|
|
|
@ -17,7 +17,7 @@ function checkMissedConfig () {
|
|||
'log.level',
|
||||
'user.video_quota', 'user.video_quota_daily',
|
||||
'csp.enabled', 'csp.report_only', 'csp.report_uri',
|
||||
'cache.previews.size', 'admin.email', 'contact_form.enabled',
|
||||
'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled',
|
||||
'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
|
||||
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
||||
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
|
||||
|
|
|
@ -266,6 +266,9 @@ const CONFIG = {
|
|||
},
|
||||
VIDEO_CAPTIONS: {
|
||||
get SIZE () { return config.get<number>('cache.captions.size') }
|
||||
},
|
||||
TORRENTS: {
|
||||
get SIZE () { return config.get<number>('cache.torrents.size') }
|
||||
}
|
||||
},
|
||||
INSTANCE: {
|
||||
|
|
|
@ -551,16 +551,13 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
|||
|
||||
// Express static paths (router)
|
||||
const STATIC_PATHS = {
|
||||
PREVIEWS: '/static/previews/',
|
||||
THUMBNAILS: '/static/thumbnails/',
|
||||
TORRENTS: '/static/torrents/',
|
||||
WEBSEED: '/static/webseed/',
|
||||
REDUNDANCY: '/static/redundancy/',
|
||||
STREAMING_PLAYLISTS: {
|
||||
HLS: '/static/streaming-playlists/hls'
|
||||
},
|
||||
AVATARS: '/static/avatars/',
|
||||
VIDEO_CAPTIONS: '/static/video-captions/'
|
||||
}
|
||||
}
|
||||
const STATIC_DOWNLOAD_PATHS = {
|
||||
TORRENTS: '/download/torrents/',
|
||||
|
@ -570,12 +567,14 @@ const STATIC_DOWNLOAD_PATHS = {
|
|||
const LAZY_STATIC_PATHS = {
|
||||
AVATARS: '/lazy-static/avatars/',
|
||||
PREVIEWS: '/lazy-static/previews/',
|
||||
VIDEO_CAPTIONS: '/lazy-static/video-captions/'
|
||||
VIDEO_CAPTIONS: '/lazy-static/video-captions/',
|
||||
TORRENTS: '/lazy-static/torrents/'
|
||||
}
|
||||
|
||||
// Cache control
|
||||
const STATIC_MAX_AGE = {
|
||||
SERVER: '2h',
|
||||
LAZY_SERVER: '2d',
|
||||
CLIENT: '30d'
|
||||
}
|
||||
|
||||
|
@ -609,6 +608,10 @@ const FILES_CACHE = {
|
|||
VIDEO_CAPTIONS: {
|
||||
DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'),
|
||||
MAX_AGE: 1000 * 3600 * 3 // 3 hours
|
||||
},
|
||||
TORRENTS: {
|
||||
DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'torrents'),
|
||||
MAX_AGE: 1000 * 3600 * 3 // 3 hours
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import { maxBy, minBy } from 'lodash'
|
||||
import * as magnetUtil from 'magnet-uri'
|
||||
import { join } from 'path'
|
||||
import { basename, join } from 'path'
|
||||
import * as request from 'request'
|
||||
import * as sequelize from 'sequelize'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
|
@ -30,11 +30,11 @@ import { doRequest } from '../../helpers/requests'
|
|||
import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
|
||||
import {
|
||||
ACTIVITY_PUB,
|
||||
LAZY_STATIC_PATHS,
|
||||
MIMETYPES,
|
||||
P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
PREVIEWS_SIZE,
|
||||
REMOTE_SCHEME,
|
||||
STATIC_PATHS,
|
||||
THUMBNAILS_SIZE
|
||||
} from '../../initializers/constants'
|
||||
import { sequelizeTypescript } from '../../initializers/database'
|
||||
|
@ -51,6 +51,8 @@ import {
|
|||
MChannelDefault,
|
||||
MChannelId,
|
||||
MStreamingPlaylist,
|
||||
MStreamingPlaylistFilesVideo,
|
||||
MStreamingPlaylistVideo,
|
||||
MVideo,
|
||||
MVideoAccountLight,
|
||||
MVideoAccountLightBlacklistAllFiles,
|
||||
|
@ -61,7 +63,8 @@ import {
|
|||
MVideoFullLight,
|
||||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnail
|
||||
MVideoThumbnail,
|
||||
MVideoWithHost
|
||||
} from '../../types/models'
|
||||
import { MThumbnail } from '../../types/models/video/thumbnail'
|
||||
import { FilteredModelAttributes } from '../../types/sequelize'
|
||||
|
@ -72,6 +75,7 @@ import { PeerTubeSocket } from '../peertube-socket'
|
|||
import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
|
||||
import { setVideoTags } from '../video'
|
||||
import { autoBlacklistVideoIfNeeded } from '../video-blacklist'
|
||||
import { generateTorrentFileName } from '../video-paths'
|
||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { crawlCollectionPage } from './crawl'
|
||||
import { sendCreateVideo, sendUpdateVideo } from './send'
|
||||
|
@ -405,7 +409,8 @@ async function updateVideoFromAP (options: {
|
|||
|
||||
for (const playlistAttributes of streamingPlaylistAttributes) {
|
||||
const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t })
|
||||
.then(([ streamingPlaylist ]) => streamingPlaylist)
|
||||
.then(([ streamingPlaylist ]) => streamingPlaylist as MStreamingPlaylistFilesVideo)
|
||||
streamingPlaylistModel.Video = videoUpdated
|
||||
|
||||
const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject)
|
||||
.map(a => new VideoFileModel(a))
|
||||
|
@ -637,13 +642,14 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
|
|||
videoCreated.VideoStreamingPlaylists = []
|
||||
|
||||
for (const playlistAttributes of streamingPlaylistsAttributes) {
|
||||
const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t })
|
||||
const playlist = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) as MStreamingPlaylistFilesVideo
|
||||
playlist.Video = videoCreated
|
||||
|
||||
const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject)
|
||||
const playlistFiles = videoFileActivityUrlToDBAttributes(playlist, playlistAttributes.tagAPObject)
|
||||
const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
|
||||
playlistModel.VideoFiles = await Promise.all(videoFilePromises)
|
||||
playlist.VideoFiles = await Promise.all(videoFilePromises)
|
||||
|
||||
videoCreated.VideoStreamingPlaylists.push(playlistModel)
|
||||
videoCreated.VideoStreamingPlaylists.push(playlist)
|
||||
}
|
||||
|
||||
// Process tags
|
||||
|
@ -766,7 +772,7 @@ function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObjec
|
|||
}
|
||||
|
||||
function videoFileActivityUrlToDBAttributes (
|
||||
videoOrPlaylist: MVideo | MStreamingPlaylist,
|
||||
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
||||
urls: (ActivityTagObject | ActivityUrlObject)[]
|
||||
) {
|
||||
const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
|
||||
|
@ -786,6 +792,10 @@ function videoFileActivityUrlToDBAttributes (
|
|||
throw new Error('Cannot parse magnet URI ' + magnet.href)
|
||||
}
|
||||
|
||||
const torrentUrl = Array.isArray(parsed.xs)
|
||||
? parsed.xs[0]
|
||||
: parsed.xs
|
||||
|
||||
// Fetch associated metadata url, if any
|
||||
const metadata = urls.filter(isAPVideoFileMetadataObject)
|
||||
.find(u => {
|
||||
|
@ -794,18 +804,30 @@ function videoFileActivityUrlToDBAttributes (
|
|||
u.rel.includes(fileUrl.mediaType)
|
||||
})
|
||||
|
||||
const mediaType = fileUrl.mediaType
|
||||
const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
|
||||
const resolution = fileUrl.height
|
||||
const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id
|
||||
const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
|
||||
|
||||
const attribute = {
|
||||
extname: getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, mediaType),
|
||||
extname,
|
||||
infoHash: parsed.infoHash,
|
||||
resolution: fileUrl.height,
|
||||
resolution,
|
||||
size: fileUrl.size,
|
||||
fps: fileUrl.fps || -1,
|
||||
metadataUrl: metadata?.href,
|
||||
|
||||
// Use the name of the remote file because we don't proxify video file requests
|
||||
filename: basename(fileUrl.href),
|
||||
fileUrl: fileUrl.href,
|
||||
|
||||
torrentUrl,
|
||||
// Use our own torrent name since we proxify torrent requests
|
||||
torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution),
|
||||
|
||||
// This is a video file owned by a video or by a streaming playlist
|
||||
videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id,
|
||||
videoStreamingPlaylistId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
|
||||
videoId,
|
||||
videoStreamingPlaylistId
|
||||
}
|
||||
|
||||
attributes.push(attribute)
|
||||
|
@ -862,8 +884,8 @@ function getPreviewFromIcons (videoObject: VideoObject) {
|
|||
return maxBy(validIcons, 'width')
|
||||
}
|
||||
|
||||
function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoAccountLight) {
|
||||
function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
|
||||
return previewIcon
|
||||
? previewIcon.url
|
||||
: buildRemoteVideoBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
|
||||
: buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { remove } from 'fs-extra'
|
|||
import { logger } from '../../helpers/logger'
|
||||
import * as memoizee from 'memoizee'
|
||||
|
||||
type GetFilePathResult = { isOwned: boolean, path: string } | undefined
|
||||
type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined
|
||||
|
||||
export abstract class AbstractVideoStaticFileCache <T> {
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { join } from 'path'
|
||||
import { doRequestAndSaveToFile } from '@server/helpers/requests'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
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'
|
||||
|
||||
class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
|
||||
|
||||
private static instance: VideosTorrentCache
|
||||
|
||||
private constructor () {
|
||||
super()
|
||||
}
|
||||
|
||||
static get Instance () {
|
||||
return this.instance || (this.instance = new this())
|
||||
}
|
||||
|
||||
async getFilePathImpl (filename: 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) }
|
||||
|
||||
return this.loadRemoteFile(filename)
|
||||
}
|
||||
|
||||
// Key is the torrent filename
|
||||
protected async loadRemoteFile (key: string) {
|
||||
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key)
|
||||
if (!file) return undefined
|
||||
|
||||
if (file.getVideo().isOwned()) throw new Error('Cannot load remote file of owned video.')
|
||||
|
||||
// Used to fetch the path
|
||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(file.getVideo().id)
|
||||
if (!video) return undefined
|
||||
|
||||
const remoteUrl = file.getRemoteTorrentUrl(video)
|
||||
const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename)
|
||||
|
||||
await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
|
||||
|
||||
const downloadName = `${video.name}-${file.resolution}p.torrent`
|
||||
|
||||
return { isOwned: false, path: destPath, downloadName }
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
VideosTorrentCache
|
||||
}
|
|
@ -12,7 +12,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from
|
|||
import { sequelizeTypescript } from '../initializers/database'
|
||||
import { VideoFileModel } from '../models/video/video-file'
|
||||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||
import { getVideoFilename, getVideoFilePath } from './video-paths'
|
||||
import { getVideoFilePath } from './video-paths'
|
||||
|
||||
async function updateStreamingPlaylistsInfohashesIfNeeded () {
|
||||
const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
|
||||
|
@ -93,7 +93,7 @@ async function updateSha256VODSegments (video: MVideoWithFile) {
|
|||
}
|
||||
await close(fd)
|
||||
|
||||
const videoFilename = getVideoFilename(hlsPlaylist, file)
|
||||
const videoFilename = file.filename
|
||||
json[videoFilename] = rangeHashes
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import * as Bull from 'bull'
|
|||
import { copy, stat } from 'fs-extra'
|
||||
import { extname } from 'path'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||
import { getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { UserModel } from '@server/models/account/user'
|
||||
import { MVideoFile, MVideoWithFile } from '@server/types/models'
|
||||
import { MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { VideoFileImportPayload } from '@shared/models'
|
||||
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
|
@ -50,14 +50,16 @@ export {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
|
||||
async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
|
||||
const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
|
||||
const { size } = await stat(inputFilePath)
|
||||
const fps = await getVideoFileFPS(inputFilePath)
|
||||
|
||||
const fileExt = extname(inputFilePath)
|
||||
let updatedVideoFile = new VideoFileModel({
|
||||
resolution: videoFileResolution,
|
||||
extname: extname(inputFilePath),
|
||||
extname: fileExt,
|
||||
filename: generateVideoFilename(video, false, videoFileResolution, fileExt),
|
||||
size,
|
||||
fps,
|
||||
videoId: video.id
|
||||
|
@ -68,7 +70,7 @@ async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
|
|||
if (currentVideoFile) {
|
||||
// Remove old file and old torrent
|
||||
await video.removeFile(currentVideoFile)
|
||||
await video.removeTorrent(currentVideoFile)
|
||||
await currentVideoFile.removeTorrent()
|
||||
// Remove the old video file from the array
|
||||
video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
|
||||
|
||||
|
@ -83,7 +85,7 @@ async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
|
|||
const outputPath = getVideoFilePath(video, updatedVideoFile)
|
||||
await copy(inputFilePath, outputPath)
|
||||
|
||||
await createTorrentAndSetInfoHash(video, updatedVideoFile)
|
||||
await createTorrentAndSetInfoHash(video, video, updatedVideoFile)
|
||||
|
||||
await updatedVideoFile.save()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { isPostImportVideoAccepted } from '@server/lib/moderation'
|
|||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { isAbleToUploadVideo } from '@server/lib/user'
|
||||
import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
|
||||
import { getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { ThumbnailModel } from '@server/models/video/thumbnail'
|
||||
import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import'
|
||||
import {
|
||||
|
@ -116,10 +116,12 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
|||
const duration = await getDurationFromVideoFile(tempVideoPath)
|
||||
|
||||
// Prepare video file object for creation in database
|
||||
const fileExt = extname(tempVideoPath)
|
||||
const videoFileData = {
|
||||
extname: extname(tempVideoPath),
|
||||
extname: fileExt,
|
||||
resolution: videoFileResolution,
|
||||
size: stats.size,
|
||||
filename: generateVideoFilename(videoImport.Video, false, videoFileResolution, fileExt),
|
||||
fps,
|
||||
videoId: videoImport.videoId
|
||||
}
|
||||
|
@ -183,7 +185,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
|||
}
|
||||
|
||||
// Create torrent
|
||||
await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile)
|
||||
await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoImportWithFiles.Video, videoFile)
|
||||
|
||||
const videoFileSave = videoFile.toJSON()
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ async function saveLive (video: MVideo, live: MVideoLive) {
|
|||
await video.save()
|
||||
|
||||
// Remove old HLS playlist video files
|
||||
const videoWithFiles = await VideoModel.loadWithFiles(video.id)
|
||||
const videoWithFiles = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
|
||||
|
||||
const hlsPlaylist = videoWithFiles.getHLSPlaylist()
|
||||
await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
|
||||
|
|
|
@ -128,7 +128,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
|
|||
// Remove webtorrent files if not enabled
|
||||
for (const file of video.VideoFiles) {
|
||||
await video.removeFile(file)
|
||||
await video.removeTorrent(file)
|
||||
await file.removeTorrent()
|
||||
await file.destroy()
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { VideoModel } from '@server/models/video/video'
|
|||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
|
||||
import { MStreamingPlaylist, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistVideo, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models'
|
||||
import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
|
||||
import { federateVideoIfNeeded } from './activitypub/videos'
|
||||
import { buildSha256Segment } from './hls'
|
||||
|
@ -277,7 +277,7 @@ class LiveManager {
|
|||
return this.runMuxing({
|
||||
sessionId,
|
||||
videoLive,
|
||||
playlist: videoStreamingPlaylist,
|
||||
playlist: Object.assign(videoStreamingPlaylist, { Video: video }),
|
||||
rtmpUrl,
|
||||
fps,
|
||||
allResolutions
|
||||
|
@ -287,7 +287,7 @@ class LiveManager {
|
|||
private async runMuxing (options: {
|
||||
sessionId: string
|
||||
videoLive: MVideoLiveVideo
|
||||
playlist: MStreamingPlaylist
|
||||
playlist: MStreamingPlaylistVideo
|
||||
rtmpUrl: string
|
||||
fps: number
|
||||
allResolutions: number[]
|
||||
|
|
|
@ -18,14 +18,14 @@ import { VideosRedundancyStrategy } from '../../../shared/models/redundancy'
|
|||
import { logger } from '../../helpers/logger'
|
||||
import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants'
|
||||
import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers/constants'
|
||||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
|
||||
import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
|
||||
import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos'
|
||||
import { downloadPlaylistSegments } from '../hls'
|
||||
import { removeVideoRedundancy } from '../redundancy'
|
||||
import { getVideoFilename } from '../video-paths'
|
||||
import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-paths'
|
||||
import { AbstractScheduler } from './abstract-scheduler'
|
||||
|
||||
type CandidateToDuplicate = {
|
||||
|
@ -222,17 +222,17 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
|
|||
logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
|
||||
|
||||
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
|
||||
const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs)
|
||||
const magnetUri = generateMagnetUri(video, video, file, baseUrlHttp, baseUrlWs)
|
||||
|
||||
const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
|
||||
|
||||
const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, getVideoFilename(video, file))
|
||||
const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, file.filename)
|
||||
await move(tmpPath, destPath, { overwrite: true })
|
||||
|
||||
const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
|
||||
expiresOn,
|
||||
url: getLocalVideoCacheFileActivityPubUrl(file),
|
||||
fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL),
|
||||
fileUrl: generateWebTorrentRedundancyUrl(file),
|
||||
strategy,
|
||||
videoFileId: file.id,
|
||||
actorId: serverActor.id
|
||||
|
@ -271,7 +271,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
|
|||
const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
|
||||
expiresOn,
|
||||
url: getLocalVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
|
||||
fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL),
|
||||
fileUrl: generateHLSRedundancyUrl(video, playlistArg),
|
||||
strategy,
|
||||
videoStreamingPlaylistId: playlist.id,
|
||||
actorId: serverActor.id
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
|
||||
import { join } from 'path'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
|
||||
import { extractVideo } from '@server/helpers/video'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants'
|
||||
import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
|
||||
|
||||
// ################## Video file name ##################
|
||||
|
||||
function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
|
||||
function generateVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, isHls: boolean, resolution: number, extname: string) {
|
||||
const video = extractVideo(videoOrPlaylist)
|
||||
|
||||
if (videoFile.isHLS()) {
|
||||
return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution)
|
||||
// FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.2
|
||||
// const uuid = uuidv4()
|
||||
const uuid = video.uuid
|
||||
|
||||
if (isHls) {
|
||||
return generateVideoStreamingPlaylistName(uuid, resolution)
|
||||
}
|
||||
|
||||
return generateWebTorrentVideoName(video.uuid, videoFile.resolution, videoFile.extname)
|
||||
return generateWebTorrentVideoName(uuid, resolution, extname)
|
||||
}
|
||||
|
||||
function generateVideoStreamingPlaylistName (uuid: string, resolution: number) {
|
||||
|
@ -28,36 +32,64 @@ function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, vi
|
|||
if (videoFile.isHLS()) {
|
||||
const video = extractVideo(videoOrPlaylist)
|
||||
|
||||
return join(getHLSDirectory(video), getVideoFilename(videoOrPlaylist, videoFile))
|
||||
return join(getHLSDirectory(video), videoFile.filename)
|
||||
}
|
||||
|
||||
const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
|
||||
return join(baseDir, getVideoFilename(videoOrPlaylist, videoFile))
|
||||
const baseDir = isRedundancy
|
||||
? CONFIG.STORAGE.REDUNDANCY_DIR
|
||||
: CONFIG.STORAGE.VIDEOS_DIR
|
||||
|
||||
return join(baseDir, videoFile.filename)
|
||||
}
|
||||
|
||||
// ################## Redundancy ##################
|
||||
|
||||
function generateHLSRedundancyUrl (video: MVideo, playlist: MStreamingPlaylist) {
|
||||
// Base URL used by our HLS player
|
||||
return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + playlist.getStringType() + '/' + video.uuid
|
||||
}
|
||||
|
||||
function generateWebTorrentRedundancyUrl (file: MVideoFile) {
|
||||
return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + file.filename
|
||||
}
|
||||
|
||||
// ################## Streaming playlist ##################
|
||||
|
||||
function getHLSDirectory (video: MVideoUUID, isRedundancy = false) {
|
||||
const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_STREAMING_PLAYLIST_DIRECTORY
|
||||
const baseDir = isRedundancy
|
||||
? HLS_REDUNDANCY_DIRECTORY
|
||||
: HLS_STREAMING_PLAYLIST_DIRECTORY
|
||||
|
||||
return join(baseDir, video.uuid)
|
||||
}
|
||||
|
||||
// ################## Torrents ##################
|
||||
|
||||
function getTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
|
||||
function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) {
|
||||
const video = extractVideo(videoOrPlaylist)
|
||||
const extension = '.torrent'
|
||||
|
||||
// FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.2
|
||||
// const uuid = uuidv4()
|
||||
const uuid = video.uuid
|
||||
|
||||
if (isStreamingPlaylist(videoOrPlaylist)) {
|
||||
return `${video.uuid}-${videoFile.resolution}-${videoOrPlaylist.getStringType()}${extension}`
|
||||
return `${uuid}-${resolution}-${videoOrPlaylist.getStringType()}${extension}`
|
||||
}
|
||||
|
||||
return video.uuid + '-' + videoFile.resolution + extension
|
||||
return uuid + '-' + resolution + extension
|
||||
}
|
||||
|
||||
function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
|
||||
return join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile))
|
||||
function getTorrentFilePath (videoFile: MVideoFile) {
|
||||
return join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename)
|
||||
}
|
||||
|
||||
// ################## Meta data ##################
|
||||
|
||||
function getLocalVideoFileMetadataUrl (video: MVideoUUID, videoFile: MVideoFile) {
|
||||
const path = '/api/v1/videos/'
|
||||
|
||||
return WEBSERVER.URL + path + video.uuid + '/metadata/' + videoFile.id
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -65,11 +97,16 @@ function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
|||
export {
|
||||
generateVideoStreamingPlaylistName,
|
||||
generateWebTorrentVideoName,
|
||||
getVideoFilename,
|
||||
generateVideoFilename,
|
||||
getVideoFilePath,
|
||||
|
||||
getTorrentFileName,
|
||||
generateTorrentFileName,
|
||||
getTorrentFilePath,
|
||||
|
||||
getHLSDirectory
|
||||
getHLSDirectory,
|
||||
|
||||
getLocalVideoFileMetadataUrl,
|
||||
|
||||
generateWebTorrentRedundancyUrl,
|
||||
generateHLSRedundancyUrl
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Job } from 'bull'
|
|||
import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
|
||||
import { basename, extname as extnameUtil, join } from 'path'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||
import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
|
||||
import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { VideoResolution } from '../../shared/models/videos'
|
||||
import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
|
||||
import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
|
||||
|
@ -13,7 +13,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSER
|
|||
import { VideoFileModel } from '../models/video/video-file'
|
||||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||
import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
|
||||
import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
|
||||
import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from './video-paths'
|
||||
import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
|
||||
|
||||
/**
|
||||
|
@ -24,7 +24,7 @@ import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
|
|||
*/
|
||||
|
||||
// Optimize the original video file and replace it. The resolution is not changed.
|
||||
async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: MVideoFile, job?: Job) {
|
||||
async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
|
@ -55,8 +55,9 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
|
|||
try {
|
||||
await remove(videoInputPath)
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
||||
// Important to do this before getVideoFilename() to take in account the new filename
|
||||
inputVideoFile.extname = newExtname
|
||||
inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname)
|
||||
|
||||
const videoOutputPath = getVideoFilePath(video, inputVideoFile)
|
||||
|
||||
|
@ -72,7 +73,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
|
|||
}
|
||||
|
||||
// Transcode the original video file to a lower resolution.
|
||||
async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean, job: Job) {
|
||||
async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const extname = '.mp4'
|
||||
|
||||
|
@ -82,11 +83,13 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
|
|||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname,
|
||||
filename: generateVideoFilename(video, false, resolution, extname),
|
||||
size: 0,
|
||||
videoId: video.id
|
||||
})
|
||||
|
||||
const videoOutputPath = getVideoFilePath(video, newVideoFile)
|
||||
const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
|
||||
const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
|
||||
|
||||
const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
|
||||
? {
|
||||
|
@ -122,7 +125,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
|
|||
}
|
||||
|
||||
// Merge an image with an audio file to create a video
|
||||
async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution, job: Job) {
|
||||
async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
|
@ -175,7 +178,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
|
|||
|
||||
// Concat TS segments from a live video to a fragmented mp4 HLS playlist
|
||||
async function generateHlsPlaylistResolutionFromTS (options: {
|
||||
video: MVideoWithFile
|
||||
video: MVideoFullLight
|
||||
concatenatedTsFilePath: string
|
||||
resolution: VideoResolution
|
||||
isPortraitMode: boolean
|
||||
|
@ -193,7 +196,7 @@ async function generateHlsPlaylistResolutionFromTS (options: {
|
|||
|
||||
// Generate an HLS playlist from an input file, and update the master playlist
|
||||
function generateHlsPlaylistResolution (options: {
|
||||
video: MVideoWithFile
|
||||
video: MVideoFullLight
|
||||
videoInputPath: string
|
||||
resolution: VideoResolution
|
||||
copyCodecs: boolean
|
||||
|
@ -235,7 +238,7 @@ export {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function onWebTorrentVideoFileTranscoding (
|
||||
video: MVideoWithFile,
|
||||
video: MVideoFullLight,
|
||||
videoFile: MVideoFile,
|
||||
transcodingPath: string,
|
||||
outputPath: string
|
||||
|
@ -250,7 +253,7 @@ async function onWebTorrentVideoFileTranscoding (
|
|||
videoFile.fps = fps
|
||||
videoFile.metadata = metadata
|
||||
|
||||
await createTorrentAndSetInfoHash(video, videoFile)
|
||||
await createTorrentAndSetInfoHash(video, video, videoFile)
|
||||
|
||||
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
||||
video.VideoFiles = await video.$get('VideoFiles')
|
||||
|
@ -260,7 +263,7 @@ async function onWebTorrentVideoFileTranscoding (
|
|||
|
||||
async function generateHlsPlaylistCommon (options: {
|
||||
type: 'hls' | 'hls-from-ts'
|
||||
video: MVideoWithFile
|
||||
video: MVideoFullLight
|
||||
inputPath: string
|
||||
resolution: VideoResolution
|
||||
copyCodecs?: boolean
|
||||
|
@ -318,10 +321,12 @@ async function generateHlsPlaylistCommon (options: {
|
|||
videoStreamingPlaylist.Video = video
|
||||
|
||||
// Build the new playlist file
|
||||
const extname = extnameUtil(videoFilename)
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname: extnameUtil(videoFilename),
|
||||
extname,
|
||||
size: 0,
|
||||
filename: generateVideoFilename(video, true, resolution, extname),
|
||||
fps: -1,
|
||||
videoStreamingPlaylistId: videoStreamingPlaylist.id
|
||||
})
|
||||
|
@ -344,7 +349,7 @@ async function generateHlsPlaylistCommon (options: {
|
|||
newVideoFile.fps = await getVideoFileFPS(videoFilePath)
|
||||
newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
|
||||
|
||||
await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
|
||||
await createTorrentAndSetInfoHash(videoStreamingPlaylist, video, newVideoFile)
|
||||
|
||||
await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
|
||||
videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from 'sequelize-typescript'
|
||||
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
|
||||
import { afterCommitIfTransaction } from '@server/helpers/database-utils'
|
||||
import { MThumbnail, MThumbnailVideo, MVideoAccountLight } from '@server/types/models'
|
||||
import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models'
|
||||
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
|
@ -164,7 +164,7 @@ export class ThumbnailModel extends Model {
|
|||
return join(directory, filename)
|
||||
}
|
||||
|
||||
getFileUrl (video: MVideoAccountLight) {
|
||||
getFileUrl (video: MVideoWithHost) {
|
||||
const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
|
||||
|
||||
if (video.isOwned()) return WEBSERVER.URL + staticPath
|
||||
|
|
|
@ -15,8 +15,9 @@ import {
|
|||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
|
||||
import { MVideoAccountLight, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
|
||||
import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models'
|
||||
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
|
||||
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
|
||||
import { logger } from '../../helpers/logger'
|
||||
|
@ -24,7 +25,6 @@ import { CONFIG } from '../../initializers/config'
|
|||
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
|
||||
import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export enum ScopeNames {
|
||||
WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
|
||||
|
@ -204,7 +204,7 @@ export class VideoCaptionModel extends Model {
|
|||
return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
|
||||
}
|
||||
|
||||
getFileUrl (video: MVideoAccountLight) {
|
||||
getFileUrl (video: MVideoWithHost) {
|
||||
if (!this.Video) this.Video = video as VideoModel
|
||||
|
||||
if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { remove } from 'fs-extra'
|
||||
import * as memoizee from 'memoizee'
|
||||
import { join } from 'path'
|
||||
import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
|
@ -5,15 +9,22 @@ import {
|
|||
CreatedAt,
|
||||
DataType,
|
||||
Default,
|
||||
DefaultScope,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Is,
|
||||
Model,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
Scopes,
|
||||
DefaultScope
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { Where } from 'sequelize/types/lib/utils'
|
||||
import validator from 'validator'
|
||||
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { extractVideo } from '@server/helpers/video'
|
||||
import { getTorrentFilePath } from '@server/lib/video-paths'
|
||||
import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
|
||||
import {
|
||||
isVideoFileExtnameValid,
|
||||
isVideoFileInfoHashValid,
|
||||
|
@ -21,20 +32,25 @@ import {
|
|||
isVideoFileSizeValid,
|
||||
isVideoFPSResolutionValid
|
||||
} from '../../helpers/custom-validators/videos'
|
||||
import {
|
||||
LAZY_STATIC_PATHS,
|
||||
MEMOIZE_LENGTH,
|
||||
MEMOIZE_TTL,
|
||||
MIMETYPES,
|
||||
STATIC_DOWNLOAD_PATHS,
|
||||
STATIC_PATHS,
|
||||
WEBSERVER
|
||||
} from '../../initializers/constants'
|
||||
import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
|
||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||
import { parseAggregateResult, throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||
import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
|
||||
import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
|
||||
import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
|
||||
import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
|
||||
import * as memoizee from 'memoizee'
|
||||
import validator from 'validator'
|
||||
|
||||
export enum ScopeNames {
|
||||
WITH_VIDEO = 'WITH_VIDEO',
|
||||
WITH_METADATA = 'WITH_METADATA'
|
||||
WITH_METADATA = 'WITH_METADATA',
|
||||
WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST'
|
||||
}
|
||||
|
||||
@DefaultScope(() => ({
|
||||
|
@ -51,6 +67,28 @@ export enum ScopeNames {
|
|||
}
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (options: { whereVideo?: Where } = {}) => {
|
||||
return {
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: false,
|
||||
where: options.whereVideo
|
||||
},
|
||||
{
|
||||
model: VideoStreamingPlaylistModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true,
|
||||
where: options.whereVideo
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
[ScopeNames.WITH_METADATA]: {
|
||||
attributes: {
|
||||
include: [ 'metadata' ]
|
||||
|
@ -81,6 +119,16 @@ export enum ScopeNames {
|
|||
fields: [ 'infoHash' ]
|
||||
},
|
||||
|
||||
{
|
||||
fields: [ 'torrentFilename' ],
|
||||
unique: true
|
||||
},
|
||||
|
||||
{
|
||||
fields: [ 'filename' ],
|
||||
unique: true
|
||||
},
|
||||
|
||||
{
|
||||
fields: [ 'videoId', 'resolution', 'fps' ],
|
||||
unique: true,
|
||||
|
@ -142,6 +190,24 @@ export class VideoFileModel extends Model {
|
|||
@Column
|
||||
metadataUrl: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
fileUrl: string
|
||||
|
||||
// Could be null for live files
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
filename: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
torrentUrl: string
|
||||
|
||||
// Could be null for live files
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
torrentFilename: string
|
||||
|
||||
@ForeignKey(() => VideoModel)
|
||||
@Column
|
||||
videoId: number
|
||||
|
@ -199,6 +265,16 @@ export class VideoFileModel extends Model {
|
|||
return !!videoFile
|
||||
}
|
||||
|
||||
static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
|
||||
const query = {
|
||||
where: {
|
||||
torrentFilename: filename
|
||||
}
|
||||
}
|
||||
|
||||
return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
|
||||
}
|
||||
|
||||
static loadWithMetadata (id: number) {
|
||||
return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
|
||||
}
|
||||
|
@ -215,28 +291,11 @@ export class VideoFileModel extends Model {
|
|||
const options = {
|
||||
where: {
|
||||
id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: false,
|
||||
where: whereVideo
|
||||
},
|
||||
{
|
||||
model: VideoStreamingPlaylistModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true,
|
||||
where: whereVideo
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return VideoFileModel.findOne(options)
|
||||
return VideoFileModel.scope({ method: [ ScopeNames.WITH_VIDEO_OR_PLAYLIST, whereVideo ] })
|
||||
.findOne(options)
|
||||
.then(file => {
|
||||
// We used `required: false` so check we have at least a video or a streaming playlist
|
||||
if (!file.Video && !file.VideoStreamingPlaylist) return null
|
||||
|
@ -348,6 +407,10 @@ export class VideoFileModel extends Model {
|
|||
return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
|
||||
}
|
||||
|
||||
getVideo (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo {
|
||||
return extractVideo(this.getVideoOrStreamingPlaylist())
|
||||
}
|
||||
|
||||
isAudio () {
|
||||
return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
|
||||
}
|
||||
|
@ -360,6 +423,62 @@ export class VideoFileModel extends Model {
|
|||
return !!this.videoStreamingPlaylistId
|
||||
}
|
||||
|
||||
getFileUrl (video: MVideoWithHost) {
|
||||
if (!this.Video) this.Video = video as VideoModel
|
||||
|
||||
if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
|
||||
if (this.fileUrl) return this.fileUrl
|
||||
|
||||
// Fallback if we don't have a file URL
|
||||
return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
|
||||
}
|
||||
|
||||
getFileStaticPath (video: MVideo) {
|
||||
if (this.isHLS()) return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename)
|
||||
|
||||
return join(STATIC_PATHS.WEBSEED, this.filename)
|
||||
}
|
||||
|
||||
getFileDownloadUrl (video: MVideoWithHost) {
|
||||
const basePath = this.isHLS()
|
||||
? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS
|
||||
: STATIC_DOWNLOAD_PATHS.VIDEOS
|
||||
const path = join(basePath, this.filename)
|
||||
|
||||
if (video.isOwned()) return WEBSERVER.URL + path
|
||||
|
||||
// FIXME: don't guess remote URL
|
||||
return buildRemoteVideoBaseUrl(video, path)
|
||||
}
|
||||
|
||||
getRemoteTorrentUrl (video: MVideoWithHost) {
|
||||
if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
|
||||
|
||||
if (this.torrentUrl) return this.torrentUrl
|
||||
|
||||
// Fallback if we don't have a torrent URL
|
||||
return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
|
||||
}
|
||||
|
||||
// We proxify torrent requests so use a local URL
|
||||
getTorrentUrl () {
|
||||
return WEBSERVER.URL + this.getTorrentStaticPath()
|
||||
}
|
||||
|
||||
getTorrentStaticPath () {
|
||||
return join(LAZY_STATIC_PATHS.TORRENTS, this.torrentFilename)
|
||||
}
|
||||
|
||||
getTorrentDownloadUrl () {
|
||||
return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.TORRENTS, this.torrentFilename)
|
||||
}
|
||||
|
||||
removeTorrent () {
|
||||
const torrentPath = getTorrentFilePath(this)
|
||||
return remove(torrentPath)
|
||||
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
||||
}
|
||||
|
||||
hasSameUniqueKeysThan (other: MVideoFile) {
|
||||
return this.fps === other.fps &&
|
||||
this.resolution === other.resolution &&
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { Video, VideoDetails } from '../../../shared/models/videos'
|
||||
import { VideoModel } from './video'
|
||||
import { generateMagnetUri } from '@server/helpers/webtorrent'
|
||||
import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths'
|
||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
||||
import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
|
||||
import { Video, VideoDetails } from '../../../shared/models/videos'
|
||||
import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
|
||||
import { isArray } from '../../helpers/custom-validators/misc'
|
||||
import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
|
||||
import { VideoCaptionModel } from './video-caption'
|
||||
import {
|
||||
getLocalVideoCommentsActivityPubUrl,
|
||||
getLocalVideoDislikesActivityPubUrl,
|
||||
getLocalVideoLikesActivityPubUrl,
|
||||
getLocalVideoSharesActivityPubUrl
|
||||
} from '../../lib/activitypub/url'
|
||||
import { isArray } from '../../helpers/custom-validators/misc'
|
||||
import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
|
||||
import {
|
||||
MStreamingPlaylistRedundanciesOpt,
|
||||
MStreamingPlaylistVideo,
|
||||
|
@ -18,12 +19,12 @@ import {
|
|||
MVideoAP,
|
||||
MVideoFile,
|
||||
MVideoFormattable,
|
||||
MVideoFormattableDetails
|
||||
MVideoFormattableDetails,
|
||||
MVideoWithHost
|
||||
} from '../../types/models'
|
||||
import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file'
|
||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
||||
import { generateMagnetUri } from '@server/helpers/webtorrent'
|
||||
import { extractVideo } from '@server/helpers/video'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoCaptionModel } from './video-caption'
|
||||
|
||||
export type VideoFormattingJSONOptions = {
|
||||
completeDescription?: boolean
|
||||
|
@ -153,12 +154,15 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
|
|||
}
|
||||
|
||||
// Format and sort video files
|
||||
detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles)
|
||||
detailsJson.files = videoFilesModelToFormattedJSON(video, video, baseUrlHttp, baseUrlWs, video.VideoFiles)
|
||||
|
||||
return Object.assign(formattedJson, detailsJson)
|
||||
}
|
||||
|
||||
function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
|
||||
function streamingPlaylistsModelToFormattedJSON (
|
||||
video: MVideoFormattableDetails,
|
||||
playlists: MStreamingPlaylistRedundanciesOpt[]
|
||||
): VideoStreamingPlaylist[] {
|
||||
if (isArray(playlists) === false) return []
|
||||
|
||||
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
|
||||
|
@ -171,7 +175,7 @@ function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStre
|
|||
? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
|
||||
: []
|
||||
|
||||
const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
|
||||
const files = videoFilesModelToFormattedJSON(playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
|
||||
|
||||
return {
|
||||
id: playlist.id,
|
||||
|
@ -190,14 +194,14 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
|
|||
return -1
|
||||
}
|
||||
|
||||
// FIXME: refactor/merge model and video arguments
|
||||
function videoFilesModelToFormattedJSON (
|
||||
model: MVideo | MStreamingPlaylistVideo,
|
||||
video: MVideoFormattableDetails,
|
||||
baseUrlHttp: string,
|
||||
baseUrlWs: string,
|
||||
videoFiles: MVideoFileRedundanciesOpt[]
|
||||
): VideoFile[] {
|
||||
const video = extractVideo(model)
|
||||
|
||||
return [ ...videoFiles ]
|
||||
.filter(f => !f.isLive())
|
||||
.sort(sortByResolutionDesc)
|
||||
|
@ -207,21 +211,29 @@ function videoFilesModelToFormattedJSON (
|
|||
id: videoFile.resolution,
|
||||
label: videoFile.resolution + 'p'
|
||||
},
|
||||
magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs),
|
||||
|
||||
// FIXME: deprecated in 3.2
|
||||
magnetUri: generateMagnetUri(model, video, videoFile, baseUrlHttp, baseUrlWs),
|
||||
|
||||
size: videoFile.size,
|
||||
fps: videoFile.fps,
|
||||
torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp),
|
||||
torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
|
||||
fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp),
|
||||
fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp),
|
||||
metadataUrl: video.getVideoFileMetadataUrl(videoFile, baseUrlHttp)
|
||||
|
||||
torrentUrl: videoFile.getTorrentUrl(),
|
||||
torrentDownloadUrl: videoFile.getTorrentDownloadUrl(),
|
||||
|
||||
fileUrl: videoFile.getFileUrl(video),
|
||||
fileDownloadUrl: videoFile.getFileDownloadUrl(video),
|
||||
|
||||
metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile)
|
||||
} as VideoFile
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: refactor/merge model and video arguments
|
||||
function addVideoFilesInAPAcc (
|
||||
acc: ActivityUrlObject[] | ActivityTagObject[],
|
||||
model: MVideoAP | MStreamingPlaylistVideo,
|
||||
video: MVideoWithHost,
|
||||
baseUrlHttp: string,
|
||||
baseUrlWs: string,
|
||||
files: MVideoFile[]
|
||||
|
@ -234,7 +246,7 @@ function addVideoFilesInAPAcc (
|
|||
acc.push({
|
||||
type: 'Link',
|
||||
mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any,
|
||||
href: model.getVideoFileUrl(file, baseUrlHttp),
|
||||
href: file.getFileUrl(video),
|
||||
height: file.resolution,
|
||||
size: file.size,
|
||||
fps: file.fps
|
||||
|
@ -244,7 +256,7 @@ function addVideoFilesInAPAcc (
|
|||
type: 'Link',
|
||||
rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ],
|
||||
mediaType: 'application/json' as 'application/json',
|
||||
href: extractVideo(model).getVideoFileMetadataUrl(file, baseUrlHttp),
|
||||
href: getLocalVideoFileMetadataUrl(video, file),
|
||||
height: file.resolution,
|
||||
fps: file.fps
|
||||
})
|
||||
|
@ -252,14 +264,14 @@ function addVideoFilesInAPAcc (
|
|||
acc.push({
|
||||
type: 'Link',
|
||||
mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
||||
href: model.getTorrentUrl(file, baseUrlHttp),
|
||||
href: file.getTorrentUrl(),
|
||||
height: file.resolution
|
||||
})
|
||||
|
||||
acc.push({
|
||||
type: 'Link',
|
||||
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
||||
href: generateMagnetUri(model, file, baseUrlHttp, baseUrlWs),
|
||||
href: generateMagnetUri(model, video, file, baseUrlHttp, baseUrlWs),
|
||||
height: file.resolution
|
||||
})
|
||||
}
|
||||
|
@ -307,7 +319,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
}
|
||||
]
|
||||
|
||||
addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
|
||||
addVideoFilesInAPAcc(url, video, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
|
||||
|
||||
for (const playlist of (video.VideoStreamingPlaylists || [])) {
|
||||
const tag = playlist.p2pMediaLoaderInfohashes
|
||||
|
@ -320,7 +332,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
})
|
||||
|
||||
const playlistWithVideo = Object.assign(playlist, { Video: video })
|
||||
addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
|
||||
addVideoFilesInAPAcc(tag, playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
|
||||
|
||||
url.push({
|
||||
type: 'Link',
|
||||
|
|
|
@ -516,6 +516,10 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
|
|||
'"VideoFiles"."resolution"': '"VideoFiles.resolution"',
|
||||
'"VideoFiles"."size"': '"VideoFiles.size"',
|
||||
'"VideoFiles"."extname"': '"VideoFiles.extname"',
|
||||
'"VideoFiles"."filename"': '"VideoFiles.filename"',
|
||||
'"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"',
|
||||
'"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"',
|
||||
'"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"',
|
||||
'"VideoFiles"."infoHash"': '"VideoFiles.infoHash"',
|
||||
'"VideoFiles"."fps"': '"VideoFiles.fps"',
|
||||
'"VideoFiles"."videoId"': '"VideoFiles.videoId"',
|
||||
|
@ -529,6 +533,10 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
|
|||
'"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"',
|
||||
'"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"',
|
||||
|
|
|
@ -1,28 +1,18 @@
|
|||
import * as memoizee from 'memoizee'
|
||||
import { join } from 'path'
|
||||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { MStreamingPlaylist } from '@server/types/models'
|
||||
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
|
||||
import { sha1 } from '../../helpers/core-utils'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { isArrayOf } from '../../helpers/custom-validators/misc'
|
||||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
||||
import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
|
||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import {
|
||||
CONSTRAINTS_FIELDS,
|
||||
MEMOIZE_LENGTH,
|
||||
MEMOIZE_TTL,
|
||||
P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
STATIC_DOWNLOAD_PATHS,
|
||||
STATIC_PATHS
|
||||
} from '../../initializers/constants'
|
||||
import { join } from 'path'
|
||||
import { sha1 } from '../../helpers/core-utils'
|
||||
import { isArrayOf } from '../../helpers/custom-validators/misc'
|
||||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideoFile } from '@server/types/models'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { getTorrentFileName, getTorrentFilePath, getVideoFilename } from '@server/lib/video-paths'
|
||||
import * as memoizee from 'memoizee'
|
||||
import { remove } from 'fs-extra'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
|
||||
@Table({
|
||||
tableName: 'videoStreamingPlaylist',
|
||||
|
@ -196,26 +186,6 @@ export class VideoStreamingPlaylistModel extends Model {
|
|||
return 'unknown'
|
||||
}
|
||||
|
||||
getVideoRedundancyUrl (baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
|
||||
}
|
||||
|
||||
getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
|
||||
}
|
||||
|
||||
getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
|
||||
}
|
||||
|
||||
getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
|
||||
}
|
||||
|
||||
getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
|
||||
}
|
||||
|
||||
getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
|
||||
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
|
||||
}
|
||||
|
@ -224,10 +194,4 @@ export class VideoStreamingPlaylistModel extends Model {
|
|||
return this.type === other.type &&
|
||||
this.videoId === other.videoId
|
||||
}
|
||||
|
||||
removeTorrent (this: MStreamingPlaylistVideo, videoFile: MVideoFile) {
|
||||
const torrentPath = getTorrentFilePath(this, videoFile)
|
||||
return remove(torrentPath)
|
||||
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,11 @@ import {
|
|||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { buildNSFWFilter } from '@server/helpers/express-utils'
|
||||
import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
|
||||
import { LiveManager } from '@server/lib/live-manager'
|
||||
import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { ModelCache } from '@server/models/model-cache'
|
||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
||||
|
@ -60,7 +61,6 @@ import {
|
|||
CONSTRAINTS_FIELDS,
|
||||
LAZY_STATIC_PATHS,
|
||||
REMOTE_SCHEME,
|
||||
STATIC_DOWNLOAD_PATHS,
|
||||
STATIC_PATHS,
|
||||
VIDEO_CATEGORIES,
|
||||
VIDEO_LANGUAGES,
|
||||
|
@ -78,6 +78,7 @@ import {
|
|||
MStreamingPlaylistFilesVideo,
|
||||
MUserAccountId,
|
||||
MUserId,
|
||||
MVideo,
|
||||
MVideoAccountLight,
|
||||
MVideoAccountLightBlacklistAllFiles,
|
||||
MVideoAP,
|
||||
|
@ -130,7 +131,6 @@ import { VideoShareModel } from './video-share'
|
|||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||
import { VideoTagModel } from './video-tag'
|
||||
import { VideoViewModel } from './video-view'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export enum ScopeNames {
|
||||
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
||||
|
@ -790,7 +790,7 @@ export class VideoModel extends Model {
|
|||
// Remove physical files and torrents
|
||||
instance.VideoFiles.forEach(file => {
|
||||
tasks.push(instance.removeFile(file))
|
||||
tasks.push(instance.removeTorrent(file))
|
||||
tasks.push(file.removeTorrent())
|
||||
})
|
||||
|
||||
// Remove playlists file
|
||||
|
@ -853,18 +853,14 @@ export class VideoModel extends Model {
|
|||
return undefined
|
||||
}
|
||||
|
||||
static listLocal (): Promise<MVideoWithAllFiles[]> {
|
||||
static listLocal (): Promise<MVideo[]> {
|
||||
const query = {
|
||||
where: {
|
||||
remote: false
|
||||
}
|
||||
}
|
||||
|
||||
return VideoModel.scope([
|
||||
ScopeNames.WITH_WEBTORRENT_FILES,
|
||||
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
||||
ScopeNames.WITH_THUMBNAILS
|
||||
]).findAll(query)
|
||||
return VideoModel.findAll(query)
|
||||
}
|
||||
|
||||
static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
|
||||
|
@ -1623,6 +1619,10 @@ export class VideoModel extends Model {
|
|||
'resolution',
|
||||
'size',
|
||||
'extname',
|
||||
'filename',
|
||||
'fileUrl',
|
||||
'torrentFilename',
|
||||
'torrentUrl',
|
||||
'infoHash',
|
||||
'fps',
|
||||
'videoId',
|
||||
|
@ -1891,14 +1891,14 @@ export class VideoModel extends Model {
|
|||
let files: VideoFile[] = []
|
||||
|
||||
if (Array.isArray(this.VideoFiles)) {
|
||||
const result = videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
|
||||
const result = videoFilesModelToFormattedJSON(this, this, baseUrlHttp, baseUrlWs, this.VideoFiles)
|
||||
files = files.concat(result)
|
||||
}
|
||||
|
||||
for (const p of (this.VideoStreamingPlaylists || [])) {
|
||||
p.Video = this
|
||||
|
||||
const result = videoFilesModelToFormattedJSON(p, baseUrlHttp, baseUrlWs, p.VideoFiles)
|
||||
const result = videoFilesModelToFormattedJSON(p, this, baseUrlHttp, baseUrlWs, p.VideoFiles)
|
||||
files = files.concat(result)
|
||||
}
|
||||
|
||||
|
@ -1956,12 +1956,6 @@ export class VideoModel extends Model {
|
|||
.catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
|
||||
}
|
||||
|
||||
removeTorrent (videoFile: MVideoFile) {
|
||||
const torrentPath = getTorrentFilePath(this, videoFile)
|
||||
return remove(torrentPath)
|
||||
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
||||
}
|
||||
|
||||
async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
|
||||
const directoryPath = getHLSDirectory(this, isRedundancy)
|
||||
|
||||
|
@ -1977,7 +1971,7 @@ export class VideoModel extends Model {
|
|||
|
||||
// Remove physical files and torrents
|
||||
await Promise.all(
|
||||
streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
|
||||
streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2054,34 +2048,6 @@ export class VideoModel extends Model {
|
|||
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
|
||||
}
|
||||
|
||||
getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
|
||||
}
|
||||
|
||||
getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
|
||||
}
|
||||
|
||||
getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
|
||||
}
|
||||
|
||||
getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
const path = '/api/v1/videos/'
|
||||
|
||||
return this.isOwned()
|
||||
? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
|
||||
: videoFile.metadataUrl
|
||||
}
|
||||
|
||||
getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
|
||||
}
|
||||
|
||||
getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
|
||||
return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
|
||||
}
|
||||
|
||||
getBandwidthBits (videoFile: MVideoFile) {
|
||||
return Math.ceil((videoFile.size * 8) / this.duration)
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
|
|||
expect(file).to.not.be.undefined
|
||||
|
||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||
expect(file.torrentUrl).to.equal(`${baseUrl}/static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`)
|
||||
expect(file.torrentUrl).to.equal(`http://${server.host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`)
|
||||
expect(file.fileUrl).to.equal(
|
||||
`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import 'mocha'
|
||||
import * as chai from 'chai'
|
||||
import { VideoDetails } from '../../../shared/models/videos'
|
||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
||||
import {
|
||||
cleanupTests,
|
||||
doubleFollow,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
uploadVideo
|
||||
} from '../../../shared/extra-utils'
|
||||
import { waitJobs } from '../../../shared/extra-utils/server/jobs'
|
||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
||||
import { VideoDetails } from '../../../shared/models/videos'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
|
@ -62,7 +62,6 @@ describe('Test create import video jobs', function () {
|
|||
|
||||
await waitJobs(servers)
|
||||
|
||||
let magnetUri: string
|
||||
for (const server of servers) {
|
||||
const { data: videos } = (await getVideosList(server.url)).body
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
@ -74,9 +73,6 @@ describe('Test create import video jobs', function () {
|
|||
const [ originalVideo, transcodedVideo ] = videoDetail.files
|
||||
assertVideoProperties(originalVideo, 720, 'webm', 218910)
|
||||
assertVideoProperties(transcodedVideo, 480, 'webm', 69217)
|
||||
|
||||
if (!magnetUri) magnetUri = transcodedVideo.magnetUri
|
||||
else expect(transcodedVideo.magnetUri).to.equal(magnetUri)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -86,7 +82,6 @@ describe('Test create import video jobs', function () {
|
|||
|
||||
await waitJobs(servers)
|
||||
|
||||
let magnetUri: string
|
||||
for (const server of servers) {
|
||||
const { data: videos } = (await getVideosList(server.url)).body
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
@ -100,9 +95,6 @@ describe('Test create import video jobs', function () {
|
|||
assertVideoProperties(transcodedVideo420, 480, 'mp4')
|
||||
assertVideoProperties(transcodedVideo320, 360, 'mp4')
|
||||
assertVideoProperties(transcodedVideo240, 240, 'mp4')
|
||||
|
||||
if (!magnetUri) magnetUri = originalVideo.magnetUri
|
||||
else expect(originalVideo.magnetUri).to.equal(magnetUri)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -112,7 +104,6 @@ describe('Test create import video jobs', function () {
|
|||
|
||||
await waitJobs(servers)
|
||||
|
||||
let magnetUri: string
|
||||
for (const server of servers) {
|
||||
const { data: videos } = (await getVideosList(server.url)).body
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
@ -124,9 +115,6 @@ describe('Test create import video jobs', function () {
|
|||
const [ video720, video480 ] = videoDetail.files
|
||||
assertVideoProperties(video720, 720, 'webm', 942961)
|
||||
assertVideoProperties(video480, 480, 'webm', 69217)
|
||||
|
||||
if (!magnetUri) magnetUri = video720.magnetUri
|
||||
else expect(video720.magnetUri).to.equal(magnetUri)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
MActorDefault,
|
||||
MActorDefaultLight,
|
||||
MActorFormattable,
|
||||
MActorHost,
|
||||
MActorLight,
|
||||
MActorSummary,
|
||||
MActorSummaryFormattable, MActorUrl
|
||||
|
@ -71,6 +72,10 @@ export type MChannelAccountLight =
|
|||
Use<'Actor', MActorDefaultLight> &
|
||||
Use<'Account', MAccountLight>
|
||||
|
||||
export type MChannelHost =
|
||||
MChannelId &
|
||||
Use<'Actor', MActorHost>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
// Account associations
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
import { VideoModel } from '../../../models/video/video'
|
||||
import { PickWith, PickWithOpt } from '@shared/core-utils'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
import { MUserVideoHistoryTime } from '../user/user-video-history'
|
||||
import { MScheduleVideoUpdate } from './schedule-video-update'
|
||||
import { MTag } from './tag'
|
||||
import { MThumbnail } from './thumbnail'
|
||||
import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
|
||||
import { MVideoCaptionLanguage, MVideoCaptionLanguageUrl } from './video-caption'
|
||||
import {
|
||||
MChannelAccountDefault,
|
||||
MChannelAccountLight,
|
||||
MChannelAccountSummaryFormattable,
|
||||
MChannelActor,
|
||||
MChannelFormattable,
|
||||
MChannelHost,
|
||||
MChannelUserId
|
||||
} from './video-channels'
|
||||
import { MTag } from './tag'
|
||||
import { MVideoCaptionLanguage, MVideoCaptionLanguageUrl } from './video-caption'
|
||||
import { MVideoFile, MVideoFileRedundanciesAll, MVideoFileRedundanciesOpt } from './video-file'
|
||||
import { MVideoLive } from './video-live'
|
||||
import {
|
||||
MStreamingPlaylistFiles,
|
||||
MStreamingPlaylistRedundancies,
|
||||
MStreamingPlaylistRedundanciesAll,
|
||||
MStreamingPlaylistRedundanciesOpt
|
||||
} from './video-streaming-playlist'
|
||||
import { MVideoFile, MVideoFileRedundanciesAll, MVideoFileRedundanciesOpt } from './video-file'
|
||||
import { MThumbnail } from './thumbnail'
|
||||
import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
|
||||
import { MScheduleVideoUpdate } from './schedule-video-update'
|
||||
import { MUserVideoHistoryTime } from '../user/user-video-history'
|
||||
import { MVideoLive } from './video-live'
|
||||
|
||||
type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
|
||||
|
||||
|
@ -143,6 +144,10 @@ export type MVideoWithChannelActor =
|
|||
MVideo &
|
||||
Use<'VideoChannel', MChannelActor>
|
||||
|
||||
export type MVideoWithHost =
|
||||
MVideo &
|
||||
Use<'VideoChannel', MChannelHost>
|
||||
|
||||
export type MVideoFullLight =
|
||||
MVideo &
|
||||
Use<'Thumbnails', MThumbnail[]> &
|
||||
|
|
|
@ -11,7 +11,7 @@ import validator from 'validator'
|
|||
import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
|
||||
import { VideoDetails, VideoPrivacy } from '../../models/videos'
|
||||
import { buildAbsoluteFixturePath, buildServerDirectory, dateIsValid, immutableAssign, testImage, webtorrentAdd } from '../miscs/miscs'
|
||||
import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
|
||||
import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
|
||||
import { waitJobs } from '../server/jobs'
|
||||
import { ServerInfo } from '../server/servers'
|
||||
import { getMyUserInformation } from '../users/users'
|
||||
|
@ -544,6 +544,9 @@ async function completeVideoCheck (
|
|||
if (!attributes.likes) attributes.likes = 0
|
||||
if (!attributes.dislikes) attributes.dislikes = 0
|
||||
|
||||
const host = new URL(url).host
|
||||
const originHost = attributes.account.host
|
||||
|
||||
expect(video.name).to.equal(attributes.name)
|
||||
expect(video.category.id).to.equal(attributes.category)
|
||||
expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
|
||||
|
@ -603,8 +606,21 @@ async function completeVideoCheck (
|
|||
if (attributes.files.length > 1) extension = '.mp4'
|
||||
|
||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||
expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
|
||||
expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
|
||||
|
||||
expect(file.torrentDownloadUrl).to.equal(`http://${host}/download/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
|
||||
expect(file.torrentUrl).to.equal(`http://${host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
|
||||
|
||||
expect(file.fileUrl).to.equal(`http://${originHost}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
|
||||
expect(file.fileDownloadUrl).to.equal(`http://${originHost}/download/videos/${videoDetails.uuid}-${file.resolution.id}${extension}`)
|
||||
|
||||
await Promise.all([
|
||||
makeRawRequest(file.torrentUrl, 200),
|
||||
makeRawRequest(file.torrentDownloadUrl, 200),
|
||||
makeRawRequest(file.metadataUrl, 200),
|
||||
// Backward compatibility
|
||||
makeRawRequest(`http://${originHost}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`, 200)
|
||||
])
|
||||
|
||||
expect(file.resolution.id).to.equal(attributeFile.resolution)
|
||||
expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
|
||||
|
||||
|
|
|
@ -3,14 +3,20 @@ import { VideoFileMetadata } from './video-file-metadata'
|
|||
import { VideoResolution } from './video-resolution.enum'
|
||||
|
||||
export interface VideoFile {
|
||||
magnetUri: string
|
||||
resolution: VideoConstant<VideoResolution>
|
||||
size: number // Bytes
|
||||
|
||||
torrentUrl: string
|
||||
torrentDownloadUrl: string
|
||||
|
||||
fileUrl: string
|
||||
fileDownloadUrl: string
|
||||
|
||||
fps: number
|
||||
|
||||
metadata?: VideoFileMetadata
|
||||
metadataUrl?: string
|
||||
|
||||
// FIXME: deprecated in 3.2
|
||||
magnetUri: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue