Dissociate video file names and video uuid

This commit is contained in:
Chocobozzz 2021-02-16 16:25:53 +01:00 committed by Chocobozzz
parent 684cdacbbd
commit 90a8bd305d
40 changed files with 638 additions and 395 deletions

View File

@ -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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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) {

View File

@ -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()

View File

@ -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 }

View File

@ -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 })
}

View File

@ -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'

View File

@ -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 })
}

View File

@ -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')

View File

@ -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
}

View File

@ -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))

View File

@ -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',

View File

@ -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: {

View File

@ -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
}
}

View File

@ -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()))
}

View File

@ -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> {

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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()
}

View File

@ -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[]

View File

@ -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

View File

@ -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
}

View File

@ -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')

View File

@ -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

View File

@ -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()

View File

@ -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 &&

View File

@ -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',

View File

@ -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"',

View File

@ -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 }))
}
}

View File

@ -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)
}

View File

@ -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`
)

View File

@ -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)
}
})

View File

@ -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

View File

@ -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[]> &

View File

@ -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')

View File

@ -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
}