Add ability to disable webtorrent
In favour of HLS
This commit is contained in:
		
							parent
							
								
									14981d7331
								
							
						
					
					
						commit
						d7a25329f9
					
				
					 80 changed files with 1189 additions and 540 deletions
				
			
		| 
						 | 
				
			
			@ -2,12 +2,12 @@
 | 
			
		|||
// @ts-ignore
 | 
			
		||||
import * as videojs from 'video.js'
 | 
			
		||||
 | 
			
		||||
import { VideoFile } from '../../../../shared/models/videos/video.model'
 | 
			
		||||
import { PeerTubePlugin } from './peertube-plugin'
 | 
			
		||||
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
 | 
			
		||||
import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
 | 
			
		||||
import { PlayerMode } from './peertube-player-manager'
 | 
			
		||||
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
 | 
			
		||||
import { VideoFile } from '@shared/models'
 | 
			
		||||
 | 
			
		||||
declare namespace videojs {
 | 
			
		||||
  interface Player {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
import * as videojs from 'video.js'
 | 
			
		||||
 | 
			
		||||
import * as WebTorrent from 'webtorrent'
 | 
			
		||||
import { VideoFile } from '../../../../../shared/models/videos/video.model'
 | 
			
		||||
import { renderVideo } from './video-renderer'
 | 
			
		||||
import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
 | 
			
		||||
import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +14,7 @@ import {
 | 
			
		|||
  getStoredWebTorrentEnabled,
 | 
			
		||||
  saveAverageBandwidth
 | 
			
		||||
} from '../peertube-player-local-storage'
 | 
			
		||||
import { VideoFile } from '@shared/models'
 | 
			
		||||
 | 
			
		||||
const CacheChunkStore = require('cache-chunk-store')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -209,12 +209,18 @@ transcoding:
 | 
			
		|||
    720p: false
 | 
			
		||||
    1080p: false
 | 
			
		||||
    2160p: false
 | 
			
		||||
 | 
			
		||||
  # Generate videos in a WebTorrent format (what we do since the first PeerTube release)
 | 
			
		||||
  # If you also enabled the hls format, it will multiply videos storage by 2
 | 
			
		||||
  webtorrent:
 | 
			
		||||
    enabled: true
 | 
			
		||||
 | 
			
		||||
  # /!\ Requires ffmpeg >= 4.1
 | 
			
		||||
  # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent:
 | 
			
		||||
  #     * Resolution change is smoother
 | 
			
		||||
  #     * Faster playback in particular with long videos
 | 
			
		||||
  #     * More stable playback (less bugs/infinite loading)
 | 
			
		||||
  # /!\ Multiplies videos storage by 2 /!\
 | 
			
		||||
  # If you also enabled the webtorrent format, it will multiply videos storage by 2
 | 
			
		||||
  hls:
 | 
			
		||||
    enabled: false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -223,12 +223,18 @@ transcoding:
 | 
			
		|||
    720p: false
 | 
			
		||||
    1080p: false
 | 
			
		||||
    2160p: false
 | 
			
		||||
 | 
			
		||||
  # Generate videos in a WebTorrent format (what we do since the first PeerTube release)
 | 
			
		||||
  # If you also enabled the hls format, it will multiply videos storage by 2
 | 
			
		||||
  webtorrent:
 | 
			
		||||
    enabled: true
 | 
			
		||||
 | 
			
		||||
  # /!\ Requires ffmpeg >= 4.1
 | 
			
		||||
  # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent:
 | 
			
		||||
  #     * Resolution change is smoother
 | 
			
		||||
  #     * Faster playback in particular with long videos
 | 
			
		||||
  #     * More stable playback (less bugs/infinite loading)
 | 
			
		||||
  # /!\ Multiplies videos storage by 2 /!\
 | 
			
		||||
  # If you also enabled the webtorrent format, it will multiply videos storage by 2
 | 
			
		||||
  hls:
 | 
			
		||||
    enabled: false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -219,7 +219,7 @@
 | 
			
		|||
    "ts-node": "8.4.1",
 | 
			
		||||
    "tslint": "^5.7.0",
 | 
			
		||||
    "tslint-config-standard": "^8.0.1",
 | 
			
		||||
    "typescript": "^3.4.3",
 | 
			
		||||
    "typescript": "^3.7.2",
 | 
			
		||||
    "xliff": "^4.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripty": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,16 @@
 | 
			
		|||
import { registerTSPaths } from '../server/helpers/register-ts-paths'
 | 
			
		||||
registerTSPaths()
 | 
			
		||||
 | 
			
		||||
import { VIDEO_TRANSCODING_FPS } from '../server/initializers/constants'
 | 
			
		||||
import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffmpeg-utils'
 | 
			
		||||
import { getMaxBitrate } from '../shared/models/videos'
 | 
			
		||||
import { VideoModel } from '../server/models/video/video'
 | 
			
		||||
import { optimizeVideofile } from '../server/lib/video-transcoding'
 | 
			
		||||
import { optimizeOriginalVideofile } from '../server/lib/video-transcoding'
 | 
			
		||||
import { initDatabaseModels } from '../server/initializers'
 | 
			
		||||
import { basename, dirname, join } from 'path'
 | 
			
		||||
import { basename, dirname } from 'path'
 | 
			
		||||
import { copy, move, remove } from 'fs-extra'
 | 
			
		||||
import { CONFIG } from '../server/initializers/config'
 | 
			
		||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 | 
			
		||||
import { getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
registerTSPaths()
 | 
			
		||||
 | 
			
		||||
run()
 | 
			
		||||
  .then(() => process.exit(0))
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +38,7 @@ async function run () {
 | 
			
		|||
    currentVideoId = video.id
 | 
			
		||||
 | 
			
		||||
    for (const file of video.VideoFiles) {
 | 
			
		||||
      currentFile = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file))
 | 
			
		||||
      currentFile = getVideoFilePath(video, file)
 | 
			
		||||
 | 
			
		||||
      const [ videoBitrate, fps, resolution ] = await Promise.all([
 | 
			
		||||
        getVideoFileBitrate(currentFile),
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +57,7 @@ async function run () {
 | 
			
		|||
        const backupFile = `${currentFile}_backup`
 | 
			
		||||
        await copy(currentFile, backupFile)
 | 
			
		||||
 | 
			
		||||
        await optimizeVideofile(video, file)
 | 
			
		||||
        await optimizeOriginalVideofile(video, file)
 | 
			
		||||
 | 
			
		||||
        const originalDuration = await getDurationFromVideoFile(backupFile)
 | 
			
		||||
        const newDuration = await getDurationFromVideoFile(currentFile)
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +70,7 @@ async function run () {
 | 
			
		|||
 | 
			
		||||
        console.log('Failed to optimize %s, restoring original', basename(currentFile))
 | 
			
		||||
        await move(backupFile, currentFile, { overwrite: true })
 | 
			
		||||
        await video.createTorrentAndSetInfoHash(file)
 | 
			
		||||
        await createTorrentAndSetInfoHash(video, file)
 | 
			
		||||
        await file.save()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -134,9 +134,9 @@ async function doesRedundancyExist (file: string) {
 | 
			
		|||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const videoFile = video.getFile(resolution)
 | 
			
		||||
  const videoFile = video.getWebTorrentFile(resolution)
 | 
			
		||||
  if (!videoFile) {
 | 
			
		||||
    console.error('Cannot find file of video %s - %d', video.url, resolution)
 | 
			
		||||
    console.error('Cannot find webtorrent file of video %s - %d', video.url, resolution)
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,4 @@
 | 
			
		|||
import { registerTSPaths } from '../server/helpers/register-ts-paths'
 | 
			
		||||
registerTSPaths()
 | 
			
		||||
 | 
			
		||||
import { WEBSERVER } from '../server/initializers/constants'
 | 
			
		||||
import { ActorFollowModel } from '../server/models/activitypub/actor-follow'
 | 
			
		||||
import { VideoModel } from '../server/models/video/video'
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +17,9 @@ import { AccountModel } from '../server/models/account/account'
 | 
			
		|||
import { VideoChannelModel } from '../server/models/video/video-channel'
 | 
			
		||||
import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist'
 | 
			
		||||
import { initDatabaseModels } from '../server/initializers'
 | 
			
		||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 | 
			
		||||
 | 
			
		||||
registerTSPaths()
 | 
			
		||||
 | 
			
		||||
run()
 | 
			
		||||
  .then(() => process.exit(0))
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +125,7 @@ async function run () {
 | 
			
		|||
 | 
			
		||||
    for (const file of video.VideoFiles) {
 | 
			
		||||
      console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid)
 | 
			
		||||
      await video.createTorrentAndSetInfoHash(file)
 | 
			
		||||
      await createTorrentAndSetInfoHash(video, file)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const playlist of video.VideoStreamingPlaylists) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,9 @@ async function getConfig (req: express.Request, res: express.Response) {
 | 
			
		|||
      hls: {
 | 
			
		||||
        enabled: CONFIG.TRANSCODING.HLS.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
      webtorrent: {
 | 
			
		||||
        enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
      enabledResolutions: getEnabledResolutions()
 | 
			
		||||
    },
 | 
			
		||||
    import: {
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +307,9 @@ function customConfig (): CustomConfig {
 | 
			
		|||
        '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ],
 | 
			
		||||
        '2160p': CONFIG.TRANSCODING.RESOLUTIONS[ '2160p' ]
 | 
			
		||||
      },
 | 
			
		||||
      webtorrent: {
 | 
			
		||||
        enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
      hls: {
 | 
			
		||||
        enabled: CONFIG.TRANSCODING.HLS.ENABLED
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,6 +64,8 @@ import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 | 
			
		|||
import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
 | 
			
		||||
import { Hooks } from '../../../lib/plugins/hooks'
 | 
			
		||||
import { MVideoDetails, MVideoFullLight } from '@server/typings/models'
 | 
			
		||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 | 
			
		||||
import { getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
const auditLogger = auditLoggerFactory('videos')
 | 
			
		||||
const videosRouter = express.Router()
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +205,8 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
 | 
			
		||||
  const videoFile = new VideoFileModel({
 | 
			
		||||
    extname: extname(videoPhysicalFile.filename),
 | 
			
		||||
    size: videoPhysicalFile.size
 | 
			
		||||
    size: videoPhysicalFile.size,
 | 
			
		||||
    videoStreamingPlaylistId: null
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (videoFile.isAudio()) {
 | 
			
		||||
| 
						 | 
				
			
			@ -214,11 +217,10 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  // Move physical file
 | 
			
		||||
  const videoDir = CONFIG.STORAGE.VIDEOS_DIR
 | 
			
		||||
  const destination = join(videoDir, video.getVideoFilename(videoFile))
 | 
			
		||||
  const destination = getVideoFilePath(video, videoFile)
 | 
			
		||||
  await move(videoPhysicalFile.path, destination)
 | 
			
		||||
  // This is important in case if there is another attempt in the retry process
 | 
			
		||||
  videoPhysicalFile.filename = video.getVideoFilename(videoFile)
 | 
			
		||||
  videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
 | 
			
		||||
  videoPhysicalFile.path = destination
 | 
			
		||||
 | 
			
		||||
  // Process thumbnail or create it from the video
 | 
			
		||||
| 
						 | 
				
			
			@ -234,7 +236,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
    : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW)
 | 
			
		||||
 | 
			
		||||
  // Create the torrent file
 | 
			
		||||
  await video.createTorrentAndSetInfoHash(videoFile)
 | 
			
		||||
  await createTorrentAndSetInfoHash(video, videoFile)
 | 
			
		||||
 | 
			
		||||
  const { videoCreated } = await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
    const sequelizeOptions = { transaction: t }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,9 @@ import { join } from 'path'
 | 
			
		|||
import { root } from '../helpers/core-utils'
 | 
			
		||||
import { CONFIG } from '../initializers/config'
 | 
			
		||||
import { getPreview, getVideoCaption } from './lazy-static'
 | 
			
		||||
import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
 | 
			
		||||
import { MVideoFile, MVideoFullLight } from '@server/typings/models'
 | 
			
		||||
import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
const staticRouter = express.Router()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +42,11 @@ staticRouter.use(
 | 
			
		|||
  asyncMiddleware(videosGetValidator),
 | 
			
		||||
  asyncMiddleware(downloadTorrent)
 | 
			
		||||
)
 | 
			
		||||
staticRouter.use(
 | 
			
		||||
  STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
 | 
			
		||||
  asyncMiddleware(videosGetValidator),
 | 
			
		||||
  asyncMiddleware(downloadHLSVideoFileTorrent)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Videos path for webseeding
 | 
			
		||||
staticRouter.use(
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +66,12 @@ staticRouter.use(
 | 
			
		|||
  asyncMiddleware(downloadVideoFile)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
staticRouter.use(
 | 
			
		||||
  STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+).:extension',
 | 
			
		||||
  asyncMiddleware(videosGetValidator),
 | 
			
		||||
  asyncMiddleware(downloadHLSVideoFile)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HLS
 | 
			
		||||
staticRouter.use(
 | 
			
		||||
  STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
 | 
			
		||||
| 
						 | 
				
			
			@ -227,24 +241,55 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
async function downloadTorrent (req: express.Request, res: express.Response) {
 | 
			
		||||
  const { video, videoFile } = getVideoAndFile(req, res)
 | 
			
		||||
  const video = res.locals.videoAll
 | 
			
		||||
 | 
			
		||||
  const videoFile = getVideoFile(req, video.VideoFiles)
 | 
			
		||||
  if (!videoFile) return res.status(404).end()
 | 
			
		||||
 | 
			
		||||
  return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
 | 
			
		||||
  return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) {
 | 
			
		||||
  const video = res.locals.videoAll
 | 
			
		||||
 | 
			
		||||
  const playlist = getHLSPlaylist(video)
 | 
			
		||||
  if (!playlist) return res.status(404).end
 | 
			
		||||
 | 
			
		||||
  const videoFile = getVideoFile(req, playlist.VideoFiles)
 | 
			
		||||
  if (!videoFile) return res.status(404).end()
 | 
			
		||||
 | 
			
		||||
  return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function downloadVideoFile (req: express.Request, res: express.Response) {
 | 
			
		||||
  const { video, videoFile } = getVideoAndFile(req, res)
 | 
			
		||||
  if (!videoFile) return res.status(404).end()
 | 
			
		||||
 | 
			
		||||
  return res.download(video.getVideoFilePath(videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getVideoAndFile (req: express.Request, res: express.Response) {
 | 
			
		||||
  const resolution = parseInt(req.params.resolution, 10)
 | 
			
		||||
  const video = res.locals.videoAll
 | 
			
		||||
 | 
			
		||||
  const videoFile = video.VideoFiles.find(f => f.resolution === resolution)
 | 
			
		||||
  const videoFile = getVideoFile(req, video.VideoFiles)
 | 
			
		||||
  if (!videoFile) return res.status(404).end()
 | 
			
		||||
 | 
			
		||||
  return { video, videoFile }
 | 
			
		||||
  return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
 | 
			
		||||
  const video = res.locals.videoAll
 | 
			
		||||
  const playlist = getHLSPlaylist(video)
 | 
			
		||||
  if (!playlist) return res.status(404).end
 | 
			
		||||
 | 
			
		||||
  const videoFile = getVideoFile(req, playlist.VideoFiles)
 | 
			
		||||
  if (!videoFile) return res.status(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 })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import {
 | 
			
		|||
} from '../videos'
 | 
			
		||||
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
 | 
			
		||||
import { VideoState } from '../../../../shared/models/videos'
 | 
			
		||||
import { logger } from '@server/helpers/logger'
 | 
			
		||||
 | 
			
		||||
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
 | 
			
		||||
  return isBaseActivityValid(activity, 'Update') &&
 | 
			
		||||
| 
						 | 
				
			
			@ -30,11 +31,26 @@ function isActivityPubVideoDurationValid (value: string) {
 | 
			
		|||
function sanitizeAndCheckVideoTorrentObject (video: any) {
 | 
			
		||||
  if (!video || video.type !== 'Video') return false
 | 
			
		||||
 | 
			
		||||
  if (!setValidRemoteTags(video)) return false
 | 
			
		||||
  if (!setValidRemoteVideoUrls(video)) return false
 | 
			
		||||
  if (!setRemoteVideoTruncatedContent(video)) return false
 | 
			
		||||
  if (!setValidAttributedTo(video)) return false
 | 
			
		||||
  if (!setValidRemoteCaptions(video)) return false
 | 
			
		||||
  if (!setValidRemoteTags(video)) {
 | 
			
		||||
    logger.debug('Video has invalid tags', { video })
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (!setValidRemoteVideoUrls(video)) {
 | 
			
		||||
    logger.debug('Video has invalid urls', { video })
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (!setRemoteVideoTruncatedContent(video)) {
 | 
			
		||||
    logger.debug('Video has invalid content', { video })
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (!setValidAttributedTo(video)) {
 | 
			
		||||
    logger.debug('Video has invalid attributedTo', { video })
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (!setValidRemoteCaptions(video)) {
 | 
			
		||||
    logger.debug('Video has invalid captions', { video })
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Default attributes
 | 
			
		||||
  if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
 | 
			
		||||
| 
						 | 
				
			
			@ -62,25 +78,21 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function isRemoteVideoUrlValid (url: any) {
 | 
			
		||||
  // FIXME: Old bug, we used the width to represent the resolution. Remove it in a few release (currently beta.11)
 | 
			
		||||
  if (url.width && !url.height) url.height = url.width
 | 
			
		||||
 | 
			
		||||
  return url.type === 'Link' &&
 | 
			
		||||
    (
 | 
			
		||||
      // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
 | 
			
		||||
      ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 &&
 | 
			
		||||
      ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType) !== -1 &&
 | 
			
		||||
      isActivityPubUrlValid(url.href) &&
 | 
			
		||||
      validator.isInt(url.height + '', { min: 0 }) &&
 | 
			
		||||
      validator.isInt(url.size + '', { min: 0 }) &&
 | 
			
		||||
      (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
 | 
			
		||||
    ) ||
 | 
			
		||||
    (
 | 
			
		||||
      ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 &&
 | 
			
		||||
      ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType) !== -1 &&
 | 
			
		||||
      isActivityPubUrlValid(url.href) &&
 | 
			
		||||
      validator.isInt(url.height + '', { min: 0 })
 | 
			
		||||
    ) ||
 | 
			
		||||
    (
 | 
			
		||||
      ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
 | 
			
		||||
      ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType) !== -1 &&
 | 
			
		||||
      validator.isLength(url.href, { min: 5 }) &&
 | 
			
		||||
      validator.isInt(url.height + '', { min: 0 })
 | 
			
		||||
    ) ||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,6 +79,15 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) {
 | 
			
		|||
  return fn()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> (
 | 
			
		||||
  fromDatabase: T[],
 | 
			
		||||
  newModels: T[],
 | 
			
		||||
  t: Transaction
 | 
			
		||||
) {
 | 
			
		||||
  return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f)))
 | 
			
		||||
              .map(f => f.destroy({ transaction: t }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
| 
						 | 
				
			
			@ -86,5 +95,6 @@ export {
 | 
			
		|||
  retryTransactionWrapper,
 | 
			
		||||
  transactionRetryer,
 | 
			
		||||
  updateInstanceWithAnother,
 | 
			
		||||
  afterCommitIfTransaction
 | 
			
		||||
  afterCommitIfTransaction,
 | 
			
		||||
  deleteNonExistingModels
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -130,6 +130,7 @@ interface BaseTranscodeOptions {
 | 
			
		|||
 | 
			
		||||
interface HLSTranscodeOptions extends BaseTranscodeOptions {
 | 
			
		||||
  type: 'hls'
 | 
			
		||||
  copyCodecs: boolean
 | 
			
		||||
  hlsPlaylist: {
 | 
			
		||||
    videoFilename: string
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +233,7 @@ export {
 | 
			
		|||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) {
 | 
			
		||||
async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
 | 
			
		||||
  let fps = await getVideoFileFPS(options.inputPath)
 | 
			
		||||
  // On small/medium resolutions, limit FPS
 | 
			
		||||
  if (
 | 
			
		||||
| 
						 | 
				
			
			@ -287,7 +288,8 @@ async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
 | 
			
		|||
async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
 | 
			
		||||
  const videoPath = getHLSVideoPath(options)
 | 
			
		||||
 | 
			
		||||
  command = await presetCopy(command)
 | 
			
		||||
  if (options.copyCodecs) command = await presetCopy(command)
 | 
			
		||||
  else command = await buildx264Command(command, options)
 | 
			
		||||
 | 
			
		||||
  command = command.outputOption('-hls_time 4')
 | 
			
		||||
                   .outputOption('-hls_list_size 0')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,10 +45,6 @@ function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird
 | 
			
		|||
  if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getVideo (res: Response) {
 | 
			
		||||
  return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights || res.locals.videoId
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getVideoWithAttributes (res: Response) {
 | 
			
		||||
  return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +53,6 @@ export {
 | 
			
		|||
  VideoFetchType,
 | 
			
		||||
  VideoFetchByUrlType,
 | 
			
		||||
  fetchVideo,
 | 
			
		||||
  getVideo,
 | 
			
		||||
  getVideoWithAttributes,
 | 
			
		||||
  fetchVideoByUrl
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,22 @@
 | 
			
		|||
import { logger } from './logger'
 | 
			
		||||
import { generateVideoImportTmpPath } from './utils'
 | 
			
		||||
import * as WebTorrent from 'webtorrent'
 | 
			
		||||
import { createWriteStream, ensureDir, remove } from 'fs-extra'
 | 
			
		||||
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/typings/models/video/video'
 | 
			
		||||
import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file'
 | 
			
		||||
import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist'
 | 
			
		||||
import { STATIC_PATHS, 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 { extractVideo } from '@server/lib/videos'
 | 
			
		||||
import { getTorrentFileName, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
const createTorrentPromise = promisify2<string, any, any>(createTorrent)
 | 
			
		||||
 | 
			
		||||
async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) {
 | 
			
		||||
  const id = target.magnetUri || target.torrentName
 | 
			
		||||
| 
						 | 
				
			
			@ -59,12 +70,64 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
 | 
			
		|||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const createTorrentPromise = promisify2<string, any, any>(createTorrent)
 | 
			
		||||
async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
 | 
			
		||||
  const video = extractVideo(videoOrPlaylist)
 | 
			
		||||
 | 
			
		||||
  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}`,
 | 
			
		||||
    createdBy: 'PeerTube',
 | 
			
		||||
    announceList: [
 | 
			
		||||
      [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
 | 
			
		||||
      [ WEBSERVER.URL + '/tracker/announce' ]
 | 
			
		||||
    ],
 | 
			
		||||
    urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + getVideoFilename(videoOrPlaylist, videoFile) ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const torrent = await createTorrentPromise(getVideoFilePath(videoOrPlaylist, videoFile), options)
 | 
			
		||||
 | 
			
		||||
  const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile))
 | 
			
		||||
  logger.info('Creating torrent %s.', filePath)
 | 
			
		||||
 | 
			
		||||
  await writeFile(filePath, torrent)
 | 
			
		||||
 | 
			
		||||
  const parsedTorrent = parseTorrent(torrent)
 | 
			
		||||
  videoFile.infoHash = parsedTorrent.infoHash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateMagnetUri (
 | 
			
		||||
  videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
 | 
			
		||||
  videoFile: MVideoFileRedundanciesOpt,
 | 
			
		||||
  baseUrlHttp: string,
 | 
			
		||||
  baseUrlWs: string
 | 
			
		||||
) {
 | 
			
		||||
  const video = isStreamingPlaylist(videoOrPlaylist)
 | 
			
		||||
    ? videoOrPlaylist.Video
 | 
			
		||||
    : videoOrPlaylist
 | 
			
		||||
 | 
			
		||||
  const xs = videoOrPlaylist.getTorrentUrl(videoFile, baseUrlHttp)
 | 
			
		||||
  const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs)
 | 
			
		||||
  let urlList = [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ]
 | 
			
		||||
 | 
			
		||||
  const redundancies = videoFile.RedundancyVideos
 | 
			
		||||
  if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
 | 
			
		||||
 | 
			
		||||
  const magnetHash = {
 | 
			
		||||
    xs,
 | 
			
		||||
    announce,
 | 
			
		||||
    urlList,
 | 
			
		||||
    infoHash: videoFile.infoHash,
 | 
			
		||||
    name: video.name
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return magnetUtil.encode(magnetHash)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  createTorrentPromise,
 | 
			
		||||
  createTorrentAndSetInfoHash,
 | 
			
		||||
  generateMagnetUri,
 | 
			
		||||
  downloadWebTorrentVideo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,6 +101,13 @@ function checkConfig () {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Transcoding
 | 
			
		||||
  if (CONFIG.TRANSCODING.ENABLED) {
 | 
			
		||||
    if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
 | 
			
		||||
      return 'You need to enable at least WebTorrent transcoding or HLS transcoding.'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -177,6 +177,9 @@ const CONFIG = {
 | 
			
		|||
    },
 | 
			
		||||
    HLS: {
 | 
			
		||||
      get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') }
 | 
			
		||||
    },
 | 
			
		||||
    WEBTORRENT: {
 | 
			
		||||
      get ENABLED () { return config.get<boolean>('transcoding.webtorrent.enabled') }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  IMPORT: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 | 
			
		|||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
const LAST_MIGRATION_VERSION = 445
 | 
			
		||||
const LAST_MIGRATION_VERSION = 450
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -505,7 +505,8 @@ const STATIC_PATHS = {
 | 
			
		|||
}
 | 
			
		||||
const STATIC_DOWNLOAD_PATHS = {
 | 
			
		||||
  TORRENTS: '/download/torrents/',
 | 
			
		||||
  VIDEOS: '/download/videos/'
 | 
			
		||||
  VIDEOS: '/download/videos/',
 | 
			
		||||
  HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
 | 
			
		||||
}
 | 
			
		||||
const LAZY_STATIC_PATHS = {
 | 
			
		||||
  AVATARS: '/lazy-static/avatars/',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import * as Sequelize from 'sequelize'
 | 
			
		|||
import * as Promise from 'bluebird'
 | 
			
		||||
import { stat } from 'fs-extra'
 | 
			
		||||
import { VideoModel } from '../../models/video/video'
 | 
			
		||||
import { getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
function up (utils: {
 | 
			
		||||
  transaction: Sequelize.Transaction,
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +17,7 @@ function up (utils: {
 | 
			
		|||
      videos.forEach(video => {
 | 
			
		||||
        video.VideoFiles.forEach(videoFile => {
 | 
			
		||||
          const p = new Promise((res, rej) => {
 | 
			
		||||
            stat(video.getVideoFilePath(videoFile), (err, stats) => {
 | 
			
		||||
            stat(getVideoFilePath(video, videoFile), (err, stats) => {
 | 
			
		||||
              if (err) return rej(err)
 | 
			
		||||
 | 
			
		||||
              videoFile.size = stats.size
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
import * as Sequelize from 'sequelize'
 | 
			
		||||
 | 
			
		||||
async function up (utils: {
 | 
			
		||||
  transaction: Sequelize.Transaction,
 | 
			
		||||
  queryInterface: Sequelize.QueryInterface,
 | 
			
		||||
  sequelize: Sequelize.Sequelize,
 | 
			
		||||
  db: any
 | 
			
		||||
}): Promise<void> {
 | 
			
		||||
  {
 | 
			
		||||
    const data = {
 | 
			
		||||
      type: Sequelize.INTEGER,
 | 
			
		||||
      allowNull: true,
 | 
			
		||||
      references: {
 | 
			
		||||
        model: 'videoStreamingPlaylist',
 | 
			
		||||
        key: 'id'
 | 
			
		||||
      },
 | 
			
		||||
      onDelete: 'CASCADE'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await utils.queryInterface.addColumn('videoFile', 'videoStreamingPlaylistId', data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    const data = {
 | 
			
		||||
      type: Sequelize.INTEGER,
 | 
			
		||||
      allowNull: true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await utils.queryInterface.changeColumn('videoFile', 'videoId', data)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function down (options) {
 | 
			
		||||
  throw new Error('Not implemented.')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  up,
 | 
			
		||||
  down
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,8 +3,10 @@ import * as sequelize from 'sequelize'
 | 
			
		|||
import * as magnetUtil from 'magnet-uri'
 | 
			
		||||
import * as request from 'request'
 | 
			
		||||
import {
 | 
			
		||||
  ActivityHashTagObject,
 | 
			
		||||
  ActivityMagnetUrlObject,
 | 
			
		||||
  ActivityPlaylistSegmentHashesObject,
 | 
			
		||||
  ActivityPlaylistUrlObject,
 | 
			
		||||
  ActivityPlaylistUrlObject, ActivityTagObject,
 | 
			
		||||
  ActivityUrlObject,
 | 
			
		||||
  ActivityVideoUrlObject,
 | 
			
		||||
  VideoState
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +15,7 @@ import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 | 
			
		|||
import { VideoPrivacy } from '../../../shared/models/videos'
 | 
			
		||||
import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
 | 
			
		||||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
 | 
			
		||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
 | 
			
		||||
import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
 | 
			
		||||
import { logger } from '../../helpers/logger'
 | 
			
		||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +59,7 @@ import {
 | 
			
		|||
  MChannelAccountLight,
 | 
			
		||||
  MChannelDefault,
 | 
			
		||||
  MChannelId,
 | 
			
		||||
  MStreamingPlaylist,
 | 
			
		||||
  MVideo,
 | 
			
		||||
  MVideoAccountLight,
 | 
			
		||||
  MVideoAccountLightBlacklistAllFiles,
 | 
			
		||||
| 
						 | 
				
			
			@ -330,21 +333,15 @@ async function updateVideoFromAP (options: {
 | 
			
		|||
      await videoUpdated.addAndSaveThumbnail(previewModel, t)
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject)
 | 
			
		||||
        const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
 | 
			
		||||
        const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
 | 
			
		||||
 | 
			
		||||
        // Remove video files that do not exist anymore
 | 
			
		||||
        const destroyTasks = videoUpdated.VideoFiles
 | 
			
		||||
                                  .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
 | 
			
		||||
                                  .map(f => f.destroy(sequelizeOptions))
 | 
			
		||||
        const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t)
 | 
			
		||||
        await Promise.all(destroyTasks)
 | 
			
		||||
 | 
			
		||||
        // Update or add other one
 | 
			
		||||
        const upsertTasks = videoFileAttributes.map(a => {
 | 
			
		||||
          return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
 | 
			
		||||
            .then(([ file ]) => file)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
 | 
			
		||||
        videoUpdated.VideoFiles = await Promise.all(upsertTasks)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -352,24 +349,39 @@ async function updateVideoFromAP (options: {
 | 
			
		|||
        const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles)
 | 
			
		||||
        const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
 | 
			
		||||
 | 
			
		||||
        // Remove video files that do not exist anymore
 | 
			
		||||
        const destroyTasks = videoUpdated.VideoStreamingPlaylists
 | 
			
		||||
                                  .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
 | 
			
		||||
                                  .map(f => f.destroy(sequelizeOptions))
 | 
			
		||||
        // Remove video playlists that do not exist anymore
 | 
			
		||||
        const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t)
 | 
			
		||||
        await Promise.all(destroyTasks)
 | 
			
		||||
 | 
			
		||||
        // Update or add other one
 | 
			
		||||
        const upsertTasks = streamingPlaylistAttributes.map(a => {
 | 
			
		||||
          return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
 | 
			
		||||
                               .then(([ streamingPlaylist ]) => streamingPlaylist)
 | 
			
		||||
        })
 | 
			
		||||
        let oldStreamingPlaylistFiles: MVideoFile[] = []
 | 
			
		||||
        for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) {
 | 
			
		||||
          oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        videoUpdated.VideoStreamingPlaylists = await Promise.all(upsertTasks)
 | 
			
		||||
        videoUpdated.VideoStreamingPlaylists = []
 | 
			
		||||
 | 
			
		||||
        for (const playlistAttributes of streamingPlaylistAttributes) {
 | 
			
		||||
          const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t })
 | 
			
		||||
                                     .then(([ streamingPlaylist ]) => streamingPlaylist)
 | 
			
		||||
 | 
			
		||||
          const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject)
 | 
			
		||||
            .map(a => new VideoFileModel(a))
 | 
			
		||||
          const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t)
 | 
			
		||||
          await Promise.all(destroyTasks)
 | 
			
		||||
 | 
			
		||||
          // Update or add other one
 | 
			
		||||
          const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
 | 
			
		||||
          streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks)
 | 
			
		||||
 | 
			
		||||
          videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        // Update Tags
 | 
			
		||||
        const tags = videoObject.tag.map(tag => tag.name)
 | 
			
		||||
        const tags = videoObject.tag
 | 
			
		||||
                                .filter(isAPHashTagObject)
 | 
			
		||||
                                .map(tag => tag.name)
 | 
			
		||||
        const tagInstances = await TagModel.findOrCreateTags(tags, t)
 | 
			
		||||
        await videoUpdated.$set('Tags', tagInstances, sequelizeOptions)
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -478,23 +490,27 @@ export {
 | 
			
		|||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
 | 
			
		||||
function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
 | 
			
		||||
  const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
 | 
			
		||||
 | 
			
		||||
  const urlMediaType = url.mediaType || url.mimeType
 | 
			
		||||
  const urlMediaType = url.mediaType
 | 
			
		||||
  return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
 | 
			
		||||
  const urlMediaType = url.mediaType || url.mimeType
 | 
			
		||||
 | 
			
		||||
  return urlMediaType === 'application/x-mpegURL'
 | 
			
		||||
  return url && url.mediaType === 'application/x-mpegURL'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
 | 
			
		||||
  const urlMediaType = tag.mediaType || tag.mimeType
 | 
			
		||||
  return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
 | 
			
		||||
function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
 | 
			
		||||
  return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isAPHashTagObject (url: any): url is ActivityHashTagObject {
 | 
			
		||||
  return url && url.type === 'Hashtag'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
 | 
			
		||||
| 
						 | 
				
			
			@ -524,21 +540,27 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
 | 
			
		|||
    if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
 | 
			
		||||
 | 
			
		||||
    // Process files
 | 
			
		||||
    const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
 | 
			
		||||
    if (videoFileAttributes.length === 0) {
 | 
			
		||||
      throw new Error('Cannot find valid files for video %s ' + videoObject.url)
 | 
			
		||||
    }
 | 
			
		||||
    const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
 | 
			
		||||
 | 
			
		||||
    const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
 | 
			
		||||
    const videoFiles = await Promise.all(videoFilePromises)
 | 
			
		||||
 | 
			
		||||
    const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
 | 
			
		||||
    const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
 | 
			
		||||
    const streamingPlaylists = await Promise.all(playlistPromises)
 | 
			
		||||
    const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
 | 
			
		||||
    videoCreated.VideoStreamingPlaylists = []
 | 
			
		||||
 | 
			
		||||
    for (const playlistAttributes of streamingPlaylistsAttributes) {
 | 
			
		||||
      const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t })
 | 
			
		||||
 | 
			
		||||
      const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject)
 | 
			
		||||
      const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
 | 
			
		||||
      playlistModel.VideoFiles = await Promise.all(videoFilePromises)
 | 
			
		||||
 | 
			
		||||
      videoCreated.VideoStreamingPlaylists.push(playlistModel)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Process tags
 | 
			
		||||
    const tags = videoObject.tag
 | 
			
		||||
                            .filter(t => t.type === 'Hashtag')
 | 
			
		||||
                            .filter(isAPHashTagObject)
 | 
			
		||||
                            .map(t => t.name)
 | 
			
		||||
    const tagInstances = await TagModel.findOrCreateTags(tags, t)
 | 
			
		||||
    await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
 | 
			
		||||
| 
						 | 
				
			
			@ -550,7 +572,6 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
 | 
			
		|||
    await Promise.all(videoCaptionsPromises)
 | 
			
		||||
 | 
			
		||||
    videoCreated.VideoFiles = videoFiles
 | 
			
		||||
    videoCreated.VideoStreamingPlaylists = streamingPlaylists
 | 
			
		||||
    videoCreated.Tags = tagInstances
 | 
			
		||||
 | 
			
		||||
    const autoBlacklisted = await autoBlacklistVideoIfNeeded({
 | 
			
		||||
| 
						 | 
				
			
			@ -628,20 +649,19 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) {
 | 
			
		||||
  const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
 | 
			
		||||
function videoFileActivityUrlToDBAttributes (
 | 
			
		||||
  videoOrPlaylist: MVideo | MStreamingPlaylist,
 | 
			
		||||
  urls: (ActivityTagObject | ActivityUrlObject)[]
 | 
			
		||||
) {
 | 
			
		||||
  const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
 | 
			
		||||
 | 
			
		||||
  if (fileUrls.length === 0) {
 | 
			
		||||
    throw new Error('Cannot find video files for ' + video.url)
 | 
			
		||||
  }
 | 
			
		||||
  if (fileUrls.length === 0) return []
 | 
			
		||||
 | 
			
		||||
  const attributes: FilteredModelAttributes<VideoFileModel>[] = []
 | 
			
		||||
  for (const fileUrl of fileUrls) {
 | 
			
		||||
    // Fetch associated magnet uri
 | 
			
		||||
    const magnet = videoObject.url.find(u => {
 | 
			
		||||
      const mediaType = u.mediaType || u.mimeType
 | 
			
		||||
      return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
 | 
			
		||||
    })
 | 
			
		||||
    const magnet = urls.filter(isAPMagnetUrlObject)
 | 
			
		||||
                       .find(u => u.height === fileUrl.height)
 | 
			
		||||
 | 
			
		||||
    if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -650,14 +670,17 @@ function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTo
 | 
			
		|||
      throw new Error('Cannot parse magnet URI ' + magnet.href)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mediaType = fileUrl.mediaType || fileUrl.mimeType
 | 
			
		||||
    const mediaType = fileUrl.mediaType
 | 
			
		||||
    const attribute = {
 | 
			
		||||
      extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
 | 
			
		||||
      infoHash: parsed.infoHash,
 | 
			
		||||
      resolution: fileUrl.height,
 | 
			
		||||
      size: fileUrl.size,
 | 
			
		||||
      videoId: video.id,
 | 
			
		||||
      fps: fileUrl.fps || -1
 | 
			
		||||
      fps: fileUrl.fps || -1,
 | 
			
		||||
 | 
			
		||||
      // 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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    attributes.push(attribute)
 | 
			
		||||
| 
						 | 
				
			
			@ -670,12 +693,15 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
 | 
			
		|||
  const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
 | 
			
		||||
  if (playlistUrls.length === 0) return []
 | 
			
		||||
 | 
			
		||||
  const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
 | 
			
		||||
  const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
 | 
			
		||||
  for (const playlistUrlObject of playlistUrls) {
 | 
			
		||||
    const segmentsSha256UrlObject = playlistUrlObject.tag
 | 
			
		||||
                                                     .find(t => {
 | 
			
		||||
                                                       return isAPPlaylistSegmentHashesUrlObject(t)
 | 
			
		||||
                                                     }) as ActivityPlaylistSegmentHashesObject
 | 
			
		||||
    const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
 | 
			
		||||
 | 
			
		||||
    let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
 | 
			
		||||
 | 
			
		||||
    // FIXME: backward compatibility introduced in v2.1.0
 | 
			
		||||
    if (files.length === 0) files = videoFiles
 | 
			
		||||
 | 
			
		||||
    if (!segmentsSha256UrlObject) {
 | 
			
		||||
      logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
 | 
			
		||||
      continue
 | 
			
		||||
| 
						 | 
				
			
			@ -685,9 +711,10 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
 | 
			
		|||
      type: VideoStreamingPlaylistType.HLS,
 | 
			
		||||
      playlistUrl: playlistUrlObject.href,
 | 
			
		||||
      segmentsSha256Url: segmentsSha256UrlObject.href,
 | 
			
		||||
      p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles),
 | 
			
		||||
      p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
 | 
			
		||||
      p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
 | 
			
		||||
      videoId: video.id
 | 
			
		||||
      videoId: video.id,
 | 
			
		||||
      tagAPObject: playlistUrlObject.tag
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    attributes.push(attribute)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import { VideoFileModel } from '../models/video/video-file'
 | 
			
		|||
import { CONFIG } from '../initializers/config'
 | 
			
		||||
import { sequelizeTypescript } from '../initializers/database'
 | 
			
		||||
import { MVideoWithFile } from '@server/typings/models'
 | 
			
		||||
import { getVideoFilename, getVideoFilePath } from './video-paths'
 | 
			
		||||
 | 
			
		||||
async function updateStreamingPlaylistsInfohashesIfNeeded () {
 | 
			
		||||
  const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
 | 
			
		||||
| 
						 | 
				
			
			@ -32,13 +33,14 @@ async function updateMasterHLSPlaylist (video: MVideoWithFile) {
 | 
			
		|||
  const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
 | 
			
		||||
  const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
 | 
			
		||||
  const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
 | 
			
		||||
  const streamingPlaylist = video.getHLSPlaylist()
 | 
			
		||||
 | 
			
		||||
  for (const file of video.VideoFiles) {
 | 
			
		||||
  for (const file of streamingPlaylist.VideoFiles) {
 | 
			
		||||
    // If we did not generated a playlist for this resolution, skip
 | 
			
		||||
    const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
 | 
			
		||||
    if (await pathExists(filePlaylistPath) === false) continue
 | 
			
		||||
 | 
			
		||||
    const videoFilePath = video.getVideoFilePath(file)
 | 
			
		||||
    const videoFilePath = getVideoFilePath(streamingPlaylist, file)
 | 
			
		||||
 | 
			
		||||
    const size = await getVideoFileSize(videoFilePath)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,12 +61,13 @@ async function updateSha256Segments (video: MVideoWithFile) {
 | 
			
		|||
  const json: { [filename: string]: { [range: string]: string } } = {}
 | 
			
		||||
 | 
			
		||||
  const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
 | 
			
		||||
  const hlsPlaylist = video.getHLSPlaylist()
 | 
			
		||||
 | 
			
		||||
  // For all the resolutions available for this video
 | 
			
		||||
  for (const file of video.VideoFiles) {
 | 
			
		||||
  for (const file of hlsPlaylist.VideoFiles) {
 | 
			
		||||
    const rangeHashes: { [range: string]: string } = {}
 | 
			
		||||
 | 
			
		||||
    const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
 | 
			
		||||
    const videoPath = getVideoFilePath(hlsPlaylist, file)
 | 
			
		||||
    const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
 | 
			
		||||
 | 
			
		||||
    // Maybe the playlist is not generated for this resolution yet
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +85,7 @@ async function updateSha256Segments (video: MVideoWithFile) {
 | 
			
		|||
    }
 | 
			
		||||
    await close(fd)
 | 
			
		||||
 | 
			
		||||
    const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
 | 
			
		||||
    const videoFilename = getVideoFilename(hlsPlaylist, file)
 | 
			
		||||
    json[videoFilename] = rangeHashes
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,8 @@ import { copy, stat } from 'fs-extra'
 | 
			
		|||
import { VideoFileModel } from '../../../models/video/video-file'
 | 
			
		||||
import { extname } from 'path'
 | 
			
		||||
import { MVideoFile, MVideoWithFile } from '@server/typings/models'
 | 
			
		||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 | 
			
		||||
import { getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
export type VideoFileImportPayload = {
 | 
			
		||||
  videoUUID: string,
 | 
			
		||||
| 
						 | 
				
			
			@ -68,10 +70,10 @@ async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
 | 
			
		|||
    updatedVideoFile = currentVideoFile
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const outputPath = video.getVideoFilePath(updatedVideoFile)
 | 
			
		||||
  const outputPath = getVideoFilePath(video, updatedVideoFile)
 | 
			
		||||
  await copy(inputFilePath, outputPath)
 | 
			
		||||
 | 
			
		||||
  await video.createTorrentAndSetInfoHash(updatedVideoFile)
 | 
			
		||||
  await createTorrentAndSetInfoHash(video, updatedVideoFile)
 | 
			
		||||
 | 
			
		||||
  await updatedVideoFile.save()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,14 +4,14 @@ import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl'
 | 
			
		|||
import { VideoImportModel } from '../../../models/video/video-import'
 | 
			
		||||
import { VideoImportState } from '../../../../shared/models/videos'
 | 
			
		||||
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
 | 
			
		||||
import { extname, join } from 'path'
 | 
			
		||||
import { extname } from 'path'
 | 
			
		||||
import { VideoFileModel } from '../../../models/video/video-file'
 | 
			
		||||
import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
 | 
			
		||||
import { VideoState } from '../../../../shared'
 | 
			
		||||
import { JobQueue } from '../index'
 | 
			
		||||
import { federateVideoIfNeeded } from '../../activitypub'
 | 
			
		||||
import { VideoModel } from '../../../models/video/video'
 | 
			
		||||
import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
 | 
			
		||||
import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
 | 
			
		||||
import { getSecureTorrentName } from '../../../helpers/utils'
 | 
			
		||||
import { move, remove, stat } from 'fs-extra'
 | 
			
		||||
import { Notifier } from '../../notifier'
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumb
 | 
			
		|||
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 | 
			
		||||
import { MThumbnail } from '../../../typings/models/video/thumbnail'
 | 
			
		||||
import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
 | 
			
		||||
import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models'
 | 
			
		||||
import { getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
type VideoImportYoutubeDLPayload = {
 | 
			
		||||
  type: 'youtube-dl'
 | 
			
		||||
| 
						 | 
				
			
			@ -142,12 +142,12 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
 | 
			
		|||
    }
 | 
			
		||||
    videoFile = new VideoFileModel(videoFileData)
 | 
			
		||||
 | 
			
		||||
    const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ] })
 | 
			
		||||
    const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ], VideoStreamingPlaylists: [] })
 | 
			
		||||
    // To clean files if the import fails
 | 
			
		||||
    const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles })
 | 
			
		||||
 | 
			
		||||
    // Move file
 | 
			
		||||
    videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImportWithFiles.Video.getVideoFilename(videoFile))
 | 
			
		||||
    videoDestFile = getVideoFilePath(videoImportWithFiles.Video, videoFile)
 | 
			
		||||
    await move(tempVideoPath, videoDestFile)
 | 
			
		||||
    tempVideoPath = null // This path is not used anymore
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +168,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    // Create torrent
 | 
			
		||||
    await videoImportWithFiles.Video.createTorrentAndSetInfoHash(videoFile)
 | 
			
		||||
    await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile)
 | 
			
		||||
 | 
			
		||||
    const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
      const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import * as Bull from 'bull'
 | 
			
		||||
import { VideoResolution, VideoState } from '../../../../shared'
 | 
			
		||||
import { VideoResolution } from '../../../../shared'
 | 
			
		||||
import { logger } from '../../../helpers/logger'
 | 
			
		||||
import { VideoModel } from '../../../models/video/video'
 | 
			
		||||
import { JobQueue } from '../job-queue'
 | 
			
		||||
| 
						 | 
				
			
			@ -8,10 +8,10 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
 | 
			
		|||
import { sequelizeTypescript } from '../../../initializers'
 | 
			
		||||
import * as Bluebird from 'bluebird'
 | 
			
		||||
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
 | 
			
		||||
import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding'
 | 
			
		||||
import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
 | 
			
		||||
import { Notifier } from '../../notifier'
 | 
			
		||||
import { CONFIG } from '../../../initializers/config'
 | 
			
		||||
import { MVideoUUID, MVideoWithFile } from '@server/typings/models'
 | 
			
		||||
import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models'
 | 
			
		||||
 | 
			
		||||
interface BaseTranscodingPayload {
 | 
			
		||||
  videoUUID: string
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ interface HLSTranscodingPayload extends BaseTranscodingPayload {
 | 
			
		|||
  type: 'hls'
 | 
			
		||||
  isPortraitMode?: boolean
 | 
			
		||||
  resolution: VideoResolution
 | 
			
		||||
  copyCodecs: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,11 +55,11 @@ async function processVideoTranscoding (job: Bull.Job) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  if (payload.type === 'hls') {
 | 
			
		||||
    await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false)
 | 
			
		||||
    await generateHlsPlaylist(video, payload.resolution, payload.copyCodecs, payload.isPortraitMode || false)
 | 
			
		||||
 | 
			
		||||
    await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
 | 
			
		||||
  } else if (payload.type === 'new-resolution') {
 | 
			
		||||
    await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
 | 
			
		||||
    await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false)
 | 
			
		||||
 | 
			
		||||
    await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
 | 
			
		||||
  } else if (payload.type === 'merge-audio') {
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +67,7 @@ async function processVideoTranscoding (job: Bull.Job) {
 | 
			
		|||
 | 
			
		||||
    await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
 | 
			
		||||
  } else {
 | 
			
		||||
    await optimizeVideofile(video)
 | 
			
		||||
    await optimizeOriginalVideofile(video)
 | 
			
		||||
 | 
			
		||||
    await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload)
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -74,48 +75,24 @@ async function processVideoTranscoding (job: Bull.Job) {
 | 
			
		|||
  return video
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onHlsPlaylistGenerationSuccess (video: MVideoUUID) {
 | 
			
		||||
async function onHlsPlaylistGenerationSuccess (video: MVideoFullLight) {
 | 
			
		||||
  if (video === undefined) return undefined
 | 
			
		||||
 | 
			
		||||
  await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
    // Maybe the video changed in database, refresh it
 | 
			
		||||
    let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
 | 
			
		||||
    // Video does not exist anymore
 | 
			
		||||
    if (!videoDatabase) return undefined
 | 
			
		||||
  // We generated the HLS playlist, we don't need the webtorrent files anymore if the admin disabled it
 | 
			
		||||
  if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
 | 
			
		||||
    for (const file of video.VideoFiles) {
 | 
			
		||||
      await video.removeFile(file)
 | 
			
		||||
      await file.destroy()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the video was not published, we consider it is a new one for other instances
 | 
			
		||||
    await federateVideoIfNeeded(videoDatabase, false, t)
 | 
			
		||||
  })
 | 
			
		||||
    video.VideoFiles = []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return publishAndFederateIfNeeded(video)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
 | 
			
		||||
  const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
    // Maybe the video changed in database, refresh it
 | 
			
		||||
    let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
 | 
			
		||||
    // Video does not exist anymore
 | 
			
		||||
    if (!videoDatabase) return undefined
 | 
			
		||||
 | 
			
		||||
    let videoPublished = false
 | 
			
		||||
 | 
			
		||||
    // We transcoded the video file in another format, now we can publish it
 | 
			
		||||
    if (videoDatabase.state !== VideoState.PUBLISHED) {
 | 
			
		||||
      videoPublished = true
 | 
			
		||||
 | 
			
		||||
      videoDatabase.state = VideoState.PUBLISHED
 | 
			
		||||
      videoDatabase.publishedAt = new Date()
 | 
			
		||||
      videoDatabase = await videoDatabase.save({ transaction: t })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the video was not published, we consider it is a new one for other instances
 | 
			
		||||
    await federateVideoIfNeeded(videoDatabase, videoPublished, t)
 | 
			
		||||
 | 
			
		||||
    return { videoDatabase, videoPublished }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (videoPublished) {
 | 
			
		||||
    Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
 | 
			
		||||
    Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
 | 
			
		||||
  }
 | 
			
		||||
  await publishAndFederateIfNeeded(video)
 | 
			
		||||
 | 
			
		||||
  await createHlsJobIfEnabled(payload)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +101,7 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
 | 
			
		|||
  if (videoArg === undefined) return undefined
 | 
			
		||||
 | 
			
		||||
  // Outside the transaction (IO on disk)
 | 
			
		||||
  const { videoFileResolution } = await videoArg.getOriginalFileResolution()
 | 
			
		||||
  const { videoFileResolution } = await videoArg.getMaxQualityResolution()
 | 
			
		||||
 | 
			
		||||
  const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
    // Maybe the video changed in database, refresh it
 | 
			
		||||
| 
						 | 
				
			
			@ -141,14 +118,29 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
 | 
			
		|||
 | 
			
		||||
    let videoPublished = false
 | 
			
		||||
 | 
			
		||||
    const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution })
 | 
			
		||||
    await createHlsJobIfEnabled(hlsPayload)
 | 
			
		||||
 | 
			
		||||
    if (resolutionsEnabled.length !== 0) {
 | 
			
		||||
      const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
 | 
			
		||||
 | 
			
		||||
      for (const resolution of resolutionsEnabled) {
 | 
			
		||||
        const dataInput = {
 | 
			
		||||
          type: 'new-resolution' as 'new-resolution',
 | 
			
		||||
          videoUUID: videoDatabase.uuid,
 | 
			
		||||
          resolution
 | 
			
		||||
        let dataInput: VideoTranscodingPayload
 | 
			
		||||
 | 
			
		||||
        if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
 | 
			
		||||
          dataInput = {
 | 
			
		||||
            type: 'new-resolution' as 'new-resolution',
 | 
			
		||||
            videoUUID: videoDatabase.uuid,
 | 
			
		||||
            resolution
 | 
			
		||||
          }
 | 
			
		||||
        } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
 | 
			
		||||
          dataInput = {
 | 
			
		||||
            type: 'hls',
 | 
			
		||||
            videoUUID: videoDatabase.uuid,
 | 
			
		||||
            resolution,
 | 
			
		||||
            isPortraitMode: false,
 | 
			
		||||
            copyCodecs: false
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
 | 
			
		||||
| 
						 | 
				
			
			@ -159,11 +151,8 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
 | 
			
		|||
 | 
			
		||||
      logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
 | 
			
		||||
    } else {
 | 
			
		||||
      videoPublished = true
 | 
			
		||||
 | 
			
		||||
      // No transcoding to do, it's now published
 | 
			
		||||
      videoDatabase.state = VideoState.PUBLISHED
 | 
			
		||||
      videoDatabase = await videoDatabase.save({ transaction: t })
 | 
			
		||||
      videoPublished = await videoDatabase.publishIfNeededAndSave(t)
 | 
			
		||||
 | 
			
		||||
      logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy })
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -175,9 +164,6 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
 | 
			
		|||
 | 
			
		||||
  if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
 | 
			
		||||
  if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
 | 
			
		||||
 | 
			
		||||
  const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })
 | 
			
		||||
  await createHlsJobIfEnabled(hlsPayload)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
| 
						 | 
				
			
			@ -196,9 +182,32 @@ function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: numbe
 | 
			
		|||
      type: 'hls' as 'hls',
 | 
			
		||||
      videoUUID: payload.videoUUID,
 | 
			
		||||
      resolution: payload.resolution,
 | 
			
		||||
      isPortraitMode: payload.isPortraitMode
 | 
			
		||||
      isPortraitMode: payload.isPortraitMode,
 | 
			
		||||
      copyCodecs: true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function publishAndFederateIfNeeded (video: MVideoUUID) {
 | 
			
		||||
  const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
    // Maybe the video changed in database, refresh it
 | 
			
		||||
    const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
 | 
			
		||||
    // Video does not exist anymore
 | 
			
		||||
    if (!videoDatabase) return undefined
 | 
			
		||||
 | 
			
		||||
    // We transcoded the video file in another format, now we can publish it
 | 
			
		||||
    const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
 | 
			
		||||
 | 
			
		||||
    // If the video was not published, we consider it is a new one for other instances
 | 
			
		||||
    await federateVideoIfNeeded(videoDatabase, videoPublished, t)
 | 
			
		||||
 | 
			
		||||
    return { videoDatabase, videoPublished }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (videoPublished) {
 | 
			
		||||
    Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
 | 
			
		||||
    Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,8 @@ import { federateVideoIfNeeded } from '../activitypub'
 | 
			
		|||
import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
 | 
			
		||||
import { VideoPrivacy } from '../../../shared/models/videos'
 | 
			
		||||
import { Notifier } from '../notifier'
 | 
			
		||||
import { VideoModel } from '../../models/video/video'
 | 
			
		||||
import { sequelizeTypescript } from '../../initializers/database'
 | 
			
		||||
import { MVideoFullLight } from '@server/typings/models'
 | 
			
		||||
 | 
			
		||||
export class UpdateVideosScheduler extends AbstractScheduler {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
 | 
			
		|||
 | 
			
		||||
    const publishedVideos = await sequelizeTypescript.transaction(async t => {
 | 
			
		||||
      const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
 | 
			
		||||
      const publishedVideos: VideoModel[] = []
 | 
			
		||||
      const publishedVideos: MVideoFullLight[] = []
 | 
			
		||||
 | 
			
		||||
      for (const schedule of schedules) {
 | 
			
		||||
        const video = schedule.Video
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +45,8 @@ export class UpdateVideosScheduler extends AbstractScheduler {
 | 
			
		|||
          await federateVideoIfNeeded(video, isNewVideo, t)
 | 
			
		||||
 | 
			
		||||
          if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) {
 | 
			
		||||
            video.ScheduleVideoUpdate = schedule
 | 
			
		||||
            publishedVideos.push(video)
 | 
			
		||||
            const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] })
 | 
			
		||||
            publishedVideos.push(videoToPublish)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER }
 | 
			
		|||
import { logger } from '../../helpers/logger'
 | 
			
		||||
import { VideosRedundancy } from '../../../shared/models/redundancy'
 | 
			
		||||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
 | 
			
		||||
import { downloadWebTorrentVideo } from '../../helpers/webtorrent'
 | 
			
		||||
import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { move } from 'fs-extra'
 | 
			
		||||
import { getServerActor } from '../../helpers/utils'
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ import {
 | 
			
		|||
  MVideoRedundancyVideo,
 | 
			
		||||
  MVideoWithAllFiles
 | 
			
		||||
} from '@server/typings/models'
 | 
			
		||||
import { getVideoFilename } from '../video-paths'
 | 
			
		||||
 | 
			
		||||
type CandidateToDuplicate = {
 | 
			
		||||
  redundancy: VideosRedundancy,
 | 
			
		||||
| 
						 | 
				
			
			@ -195,11 +196,11 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
 | 
			
		|||
    logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy)
 | 
			
		||||
 | 
			
		||||
    const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
 | 
			
		||||
    const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs)
 | 
			
		||||
    const magnetUri = await generateMagnetUri(video, file, baseUrlHttp, baseUrlWs)
 | 
			
		||||
 | 
			
		||||
    const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
 | 
			
		||||
 | 
			
		||||
    const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
 | 
			
		||||
    const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, getVideoFilename(video, file))
 | 
			
		||||
    await move(tmpPath, destPath, { overwrite: true })
 | 
			
		||||
 | 
			
		||||
    const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import { downloadImage } from '../helpers/requests'
 | 
			
		|||
import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist'
 | 
			
		||||
import { MVideoFile, MVideoThumbnail } from '../typings/models'
 | 
			
		||||
import { MThumbnail } from '../typings/models/video/thumbnail'
 | 
			
		||||
import { getVideoFilePath } from './video-paths'
 | 
			
		||||
 | 
			
		||||
type ImageSize = { height: number, width: number }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +56,7 @@ function createVideoMiniatureFromExisting (
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) {
 | 
			
		||||
  const input = video.getVideoFilePath(videoFile)
 | 
			
		||||
  const input = getVideoFilePath(video, videoFile)
 | 
			
		||||
 | 
			
		||||
  const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
 | 
			
		||||
  const thumbnailCreator = videoFile.isAudio()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										64
									
								
								server/lib/video-paths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								server/lib/video-paths.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/typings/models'
 | 
			
		||||
import { extractVideo } from './videos'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { CONFIG } from '@server/initializers/config'
 | 
			
		||||
import { HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
 | 
			
		||||
 | 
			
		||||
// ################## Video file name ##################
 | 
			
		||||
 | 
			
		||||
function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
 | 
			
		||||
  const video = extractVideo(videoOrPlaylist)
 | 
			
		||||
 | 
			
		||||
  if (isStreamingPlaylist(videoOrPlaylist)) {
 | 
			
		||||
    return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return generateWebTorrentVideoName(video.uuid, videoFile.resolution, videoFile.extname)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateVideoStreamingPlaylistName (uuid: string, resolution: number) {
 | 
			
		||||
  return `${uuid}-${resolution}-fragmented.mp4`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateWebTorrentVideoName (uuid: string, resolution: number, extname: string) {
 | 
			
		||||
  return uuid + '-' + resolution + extname
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) {
 | 
			
		||||
  if (isStreamingPlaylist(videoOrPlaylist)) {
 | 
			
		||||
    const video = extractVideo(videoOrPlaylist)
 | 
			
		||||
    return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid, getVideoFilename(videoOrPlaylist, videoFile))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
 | 
			
		||||
  return join(baseDir, getVideoFilename(videoOrPlaylist, videoFile))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ################## Torrents ##################
 | 
			
		||||
 | 
			
		||||
function getTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
 | 
			
		||||
  const video = extractVideo(videoOrPlaylist)
 | 
			
		||||
  const extension = '.torrent'
 | 
			
		||||
 | 
			
		||||
  if (isStreamingPlaylist(videoOrPlaylist)) {
 | 
			
		||||
    return `${video.uuid}-${videoFile.resolution}-${videoOrPlaylist.getStringType()}${extension}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return video.uuid + '-' + videoFile.resolution + extension
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
 | 
			
		||||
  return join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  generateVideoStreamingPlaylistName,
 | 
			
		||||
  generateWebTorrentVideoName,
 | 
			
		||||
  getVideoFilename,
 | 
			
		||||
  getVideoFilePath,
 | 
			
		||||
 | 
			
		||||
  getTorrentFileName,
 | 
			
		||||
  getTorrentFilePath
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
 | 
			
		||||
import { basename, join } from 'path'
 | 
			
		||||
import { basename, extname as extnameUtil, join } from 'path'
 | 
			
		||||
import {
 | 
			
		||||
  canDoQuickTranscode,
 | 
			
		||||
  getDurationFromVideoFile,
 | 
			
		||||
| 
						 | 
				
			
			@ -16,18 +16,19 @@ import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
 | 
			
		|||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
 | 
			
		||||
import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
 | 
			
		||||
import { CONFIG } from '../initializers/config'
 | 
			
		||||
import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models'
 | 
			
		||||
import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/typings/models'
 | 
			
		||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 | 
			
		||||
import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Optimize the original video file and replace it. The resolution is not changed.
 | 
			
		||||
 */
 | 
			
		||||
async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
 | 
			
		||||
  const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
 | 
			
		||||
async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
 | 
			
		||||
  const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
 | 
			
		||||
  const newExtname = '.mp4'
 | 
			
		||||
 | 
			
		||||
  const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
 | 
			
		||||
  const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
 | 
			
		||||
  const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
 | 
			
		||||
  const videoInputPath = getVideoFilePath(video, inputVideoFile)
 | 
			
		||||
  const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
 | 
			
		||||
 | 
			
		||||
  const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +36,7 @@ async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVi
 | 
			
		|||
    : 'video'
 | 
			
		||||
 | 
			
		||||
  const transcodeOptions: TranscodeOptions = {
 | 
			
		||||
    type: transcodeType as any, // FIXME: typing issue
 | 
			
		||||
    type: transcodeType,
 | 
			
		||||
    inputPath: videoInputPath,
 | 
			
		||||
    outputPath: videoTranscodedPath,
 | 
			
		||||
    resolution: inputVideoFile.resolution
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +51,7 @@ async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVi
 | 
			
		|||
    // Important to do this before getVideoFilename() to take in account the new file extension
 | 
			
		||||
    inputVideoFile.extname = newExtname
 | 
			
		||||
 | 
			
		||||
    const videoOutputPath = video.getVideoFilePath(inputVideoFile)
 | 
			
		||||
    const videoOutputPath = getVideoFilePath(video, inputVideoFile)
 | 
			
		||||
 | 
			
		||||
    await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,13 +65,12 @@ async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVi
 | 
			
		|||
/**
 | 
			
		||||
 * Transcode the original video file to a lower resolution.
 | 
			
		||||
 */
 | 
			
		||||
async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
 | 
			
		||||
  const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
 | 
			
		||||
async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
 | 
			
		||||
  const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
 | 
			
		||||
  const extname = '.mp4'
 | 
			
		||||
 | 
			
		||||
  // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
 | 
			
		||||
  const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
 | 
			
		||||
  const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
 | 
			
		||||
 | 
			
		||||
  const newVideoFile = new VideoFileModel({
 | 
			
		||||
    resolution,
 | 
			
		||||
| 
						 | 
				
			
			@ -78,8 +78,8 @@ async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: Vi
 | 
			
		|||
    size: 0,
 | 
			
		||||
    videoId: video.id
 | 
			
		||||
  })
 | 
			
		||||
  const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
 | 
			
		||||
  const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
 | 
			
		||||
  const videoOutputPath = getVideoFilePath(video, newVideoFile)
 | 
			
		||||
  const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
 | 
			
		||||
 | 
			
		||||
  const transcodeOptions = {
 | 
			
		||||
    type: 'video' as 'video',
 | 
			
		||||
| 
						 | 
				
			
			@ -94,14 +94,13 @@ async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: Vi
 | 
			
		|||
  return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) {
 | 
			
		||||
  const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
 | 
			
		||||
async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
 | 
			
		||||
  const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
 | 
			
		||||
  const newExtname = '.mp4'
 | 
			
		||||
 | 
			
		||||
  const inputVideoFile = video.getOriginalFile()
 | 
			
		||||
  const inputVideoFile = video.getMaxQualityFile()
 | 
			
		||||
 | 
			
		||||
  const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
 | 
			
		||||
  const audioInputPath = getVideoFilePath(video, inputVideoFile)
 | 
			
		||||
  const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
 | 
			
		||||
 | 
			
		||||
  // If the user updates the video preview during transcoding
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +129,7 @@ async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution:
 | 
			
		|||
  // Important to do this before getVideoFilename() to take in account the new file extension
 | 
			
		||||
  inputVideoFile.extname = newExtname
 | 
			
		||||
 | 
			
		||||
  const videoOutputPath = video.getVideoFilePath(inputVideoFile)
 | 
			
		||||
  const videoOutputPath = getVideoFilePath(video, inputVideoFile)
 | 
			
		||||
  // ffmpeg generated a new video file, so update the video duration
 | 
			
		||||
  // See https://trac.ffmpeg.org/ticket/5456
 | 
			
		||||
  video.duration = await getDurationFromVideoFile(videoTranscodedPath)
 | 
			
		||||
| 
						 | 
				
			
			@ -139,33 +138,40 @@ async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution:
 | 
			
		|||
  return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) {
 | 
			
		||||
async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, copyCodecs: boolean, isPortraitMode: boolean) {
 | 
			
		||||
  const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
 | 
			
		||||
  await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
 | 
			
		||||
 | 
			
		||||
  const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getFile(resolution)))
 | 
			
		||||
  const videoFileInput = copyCodecs
 | 
			
		||||
    ? video.getWebTorrentFile(resolution)
 | 
			
		||||
    : video.getMaxQualityFile()
 | 
			
		||||
 | 
			
		||||
  const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist()
 | 
			
		||||
  const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput)
 | 
			
		||||
 | 
			
		||||
  const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
 | 
			
		||||
  const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
 | 
			
		||||
 | 
			
		||||
  const transcodeOptions = {
 | 
			
		||||
    type: 'hls' as 'hls',
 | 
			
		||||
    inputPath: videoInputPath,
 | 
			
		||||
    outputPath,
 | 
			
		||||
    resolution,
 | 
			
		||||
    copyCodecs,
 | 
			
		||||
    isPortraitMode,
 | 
			
		||||
 | 
			
		||||
    hlsPlaylist: {
 | 
			
		||||
      videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
 | 
			
		||||
      videoFilename
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await transcode(transcodeOptions)
 | 
			
		||||
  logger.debug('Will run transcode.', { transcodeOptions })
 | 
			
		||||
 | 
			
		||||
  await updateMasterHLSPlaylist(video)
 | 
			
		||||
  await updateSha256Segments(video)
 | 
			
		||||
  await transcode(transcodeOptions)
 | 
			
		||||
 | 
			
		||||
  const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
 | 
			
		||||
 | 
			
		||||
  await VideoStreamingPlaylistModel.upsert({
 | 
			
		||||
  const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
 | 
			
		||||
    videoId: video.id,
 | 
			
		||||
    playlistUrl,
 | 
			
		||||
    segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid),
 | 
			
		||||
| 
						 | 
				
			
			@ -173,15 +179,44 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso
 | 
			
		|||
    p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
 | 
			
		||||
 | 
			
		||||
    type: VideoStreamingPlaylistType.HLS
 | 
			
		||||
  }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
 | 
			
		||||
  videoStreamingPlaylist.Video = video
 | 
			
		||||
 | 
			
		||||
  const newVideoFile = new VideoFileModel({
 | 
			
		||||
    resolution,
 | 
			
		||||
    extname: extnameUtil(videoFilename),
 | 
			
		||||
    size: 0,
 | 
			
		||||
    fps: -1,
 | 
			
		||||
    videoStreamingPlaylistId: videoStreamingPlaylist.id
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
 | 
			
		||||
  const stats = await stat(videoFilePath)
 | 
			
		||||
 | 
			
		||||
  newVideoFile.size = stats.size
 | 
			
		||||
  newVideoFile.fps = await getVideoFileFPS(videoFilePath)
 | 
			
		||||
 | 
			
		||||
  await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
 | 
			
		||||
 | 
			
		||||
  const updatedVideoFile = await newVideoFile.save()
 | 
			
		||||
 | 
			
		||||
  videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') as VideoFileModel[]
 | 
			
		||||
  videoStreamingPlaylist.VideoFiles.push(updatedVideoFile)
 | 
			
		||||
 | 
			
		||||
  video.setHLSPlaylist(videoStreamingPlaylist)
 | 
			
		||||
 | 
			
		||||
  await updateMasterHLSPlaylist(video)
 | 
			
		||||
  await updateSha256Segments(video)
 | 
			
		||||
 | 
			
		||||
  return video
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  generateHlsPlaylist,
 | 
			
		||||
  optimizeVideofile,
 | 
			
		||||
  transcodeOriginalVideofile,
 | 
			
		||||
  optimizeOriginalVideofile,
 | 
			
		||||
  transcodeNewResolution,
 | 
			
		||||
  mergeAudioVideofile
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +231,7 @@ async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoF
 | 
			
		|||
  videoFile.size = stats.size
 | 
			
		||||
  videoFile.fps = fps
 | 
			
		||||
 | 
			
		||||
  await video.createTorrentAndSetInfoHash(videoFile)
 | 
			
		||||
  await createTorrentAndSetInfoHash(video, videoFile)
 | 
			
		||||
 | 
			
		||||
  const updatedVideoFile = await videoFile.save()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								server/lib/videos.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								server/lib/videos.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
 | 
			
		||||
 | 
			
		||||
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
 | 
			
		||||
  return isStreamingPlaylist(videoOrPlaylist)
 | 
			
		||||
    ? videoOrPlaylist.Video
 | 
			
		||||
    : videoOrPlaylist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  extractVideo
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +43,9 @@ const customConfigUpdateValidator = [
 | 
			
		|||
  body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'),
 | 
			
		||||
  body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
 | 
			
		||||
 | 
			
		||||
  body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
 | 
			
		||||
  body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
 | 
			
		||||
 | 
			
		||||
  body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
 | 
			
		||||
  body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +59,7 @@ const customConfigUpdateValidator = [
 | 
			
		|||
 | 
			
		||||
    if (areValidationErrors(req, res)) return
 | 
			
		||||
    if (!checkInvalidConfigIfEmailDisabled(req.body as CustomConfig, res)) return
 | 
			
		||||
    if (!checkInvalidTranscodingConfig(req.body as CustomConfig, res)) return
 | 
			
		||||
 | 
			
		||||
    return next()
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -79,3 +83,16 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp
 | 
			
		|||
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) {
 | 
			
		||||
  if (customConfig.transcoding.enabled === false) return true
 | 
			
		||||
 | 
			
		||||
  if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) {
 | 
			
		||||
    res.status(400)
 | 
			
		||||
       .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' })
 | 
			
		||||
       .end()
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -270,7 +270,7 @@ const videosAcceptChangeOwnershipValidator = [
 | 
			
		|||
 | 
			
		||||
    const user = res.locals.oauth.token.User
 | 
			
		||||
    const videoChangeOwnership = res.locals.videoChangeOwnership
 | 
			
		||||
    const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
 | 
			
		||||
    const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
 | 
			
		||||
    if (isAble === false) {
 | 
			
		||||
      res.status(403)
 | 
			
		||||
        .json({ error: 'The user video quota is exceeded with this video.' })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -497,7 +497,6 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
 | 
			
		|||
        expires: this.expiresOn.toISOString(),
 | 
			
		||||
        url: {
 | 
			
		||||
          type: 'Link',
 | 
			
		||||
          mimeType: 'application/x-mpegURL',
 | 
			
		||||
          mediaType: 'application/x-mpegURL',
 | 
			
		||||
          href: this.fileUrl
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -511,7 +510,6 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
 | 
			
		|||
      expires: this.expiresOn.toISOString(),
 | 
			
		||||
      url: {
 | 
			
		||||
        type: 'Link',
 | 
			
		||||
        mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
 | 
			
		||||
        mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
 | 
			
		||||
        href: this.fileUrl,
 | 
			
		||||
        height: this.VideoFile.resolution,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { Model, Sequelize } from 'sequelize-typescript'
 | 
			
		||||
import * as validator from 'validator'
 | 
			
		||||
import { Col } from 'sequelize/types/lib/utils'
 | 
			
		||||
import { col, literal, OrderItem } from 'sequelize'
 | 
			
		||||
import { literal, OrderItem } from 'sequelize'
 | 
			
		||||
 | 
			
		||||
type SortType = { sortModel: string, sortValue: string }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Ta
 | 
			
		|||
import { ScopeNames as VideoScopeNames, VideoModel } from './video'
 | 
			
		||||
import { VideoPrivacy } from '../../../shared/models/videos'
 | 
			
		||||
import { Op, Transaction } from 'sequelize'
 | 
			
		||||
import { MScheduleVideoUpdateFormattable } from '@server/typings/models'
 | 
			
		||||
import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/typings/models'
 | 
			
		||||
 | 
			
		||||
@Table({
 | 
			
		||||
  tableName: 'scheduleVideoUpdate',
 | 
			
		||||
| 
						 | 
				
			
			@ -72,10 +72,12 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
 | 
			
		|||
        {
 | 
			
		||||
          model: VideoModel.scope(
 | 
			
		||||
            [
 | 
			
		||||
              VideoScopeNames.WITH_FILES,
 | 
			
		||||
              VideoScopeNames.WITH_WEBTORRENT_FILES,
 | 
			
		||||
              VideoScopeNames.WITH_STREAMING_PLAYLISTS,
 | 
			
		||||
              VideoScopeNames.WITH_ACCOUNT_DETAILS,
 | 
			
		||||
              VideoScopeNames.WITH_BLACKLISTED,
 | 
			
		||||
              VideoScopeNames.WITH_THUMBNAILS
 | 
			
		||||
              VideoScopeNames.WITH_THUMBNAILS,
 | 
			
		||||
              VideoScopeNames.WITH_TAGS
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +85,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
 | 
			
		|||
      transaction: t
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ScheduleVideoUpdateModel.findAll(query)
 | 
			
		||||
    return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdateVideoAll>(query)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static deleteByVideoId (videoId: number, t: Transaction) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,11 @@ enum ScopeNames {
 | 
			
		|||
  [ScopeNames.WITH_VIDEO]: {
 | 
			
		||||
    include: [
 | 
			
		||||
      {
 | 
			
		||||
        model: VideoModel.scope([ VideoScopeNames.WITH_THUMBNAILS, VideoScopeNames.WITH_FILES ]),
 | 
			
		||||
        model: VideoModel.scope([
 | 
			
		||||
          VideoScopeNames.WITH_THUMBNAILS,
 | 
			
		||||
          VideoScopeNames.WITH_WEBTORRENT_FILES,
 | 
			
		||||
          VideoScopeNames.WITH_STREAMING_PLAYLISTS
 | 
			
		||||
        ]),
 | 
			
		||||
        required: true
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,22 +23,52 @@ import { parseAggregateResult, throwIfNotValid } from '../utils'
 | 
			
		|||
import { VideoModel } from './video'
 | 
			
		||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 | 
			
		||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 | 
			
		||||
import { FindOptions, QueryTypes, Transaction } from 'sequelize'
 | 
			
		||||
import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
 | 
			
		||||
import { MIMETYPES } from '../../initializers/constants'
 | 
			
		||||
import { MVideoFile } from '@server/typings/models'
 | 
			
		||||
import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
 | 
			
		||||
import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
 | 
			
		||||
 | 
			
		||||
@Table({
 | 
			
		||||
  tableName: 'videoFile',
 | 
			
		||||
  indexes: [
 | 
			
		||||
    {
 | 
			
		||||
      fields: [ 'videoId' ]
 | 
			
		||||
      fields: [ 'videoId' ],
 | 
			
		||||
      where: {
 | 
			
		||||
        videoId: {
 | 
			
		||||
          [Op.ne]: null
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fields: [ 'videoStreamingPlaylistId' ],
 | 
			
		||||
      where: {
 | 
			
		||||
        videoStreamingPlaylistId: {
 | 
			
		||||
          [Op.ne]: null
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      fields: [ 'infoHash' ]
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      fields: [ 'videoId', 'resolution', 'fps' ],
 | 
			
		||||
      unique: true
 | 
			
		||||
      unique: true,
 | 
			
		||||
      where: {
 | 
			
		||||
        videoId: {
 | 
			
		||||
          [Op.ne]: null
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
 | 
			
		||||
      unique: true,
 | 
			
		||||
      where: {
 | 
			
		||||
        videoStreamingPlaylistId: {
 | 
			
		||||
          [Op.ne]: null
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -81,12 +111,24 @@ export class VideoFileModel extends Model<VideoFileModel> {
 | 
			
		|||
 | 
			
		||||
  @BelongsTo(() => VideoModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      allowNull: false
 | 
			
		||||
      allowNull: true
 | 
			
		||||
    },
 | 
			
		||||
    onDelete: 'CASCADE'
 | 
			
		||||
  })
 | 
			
		||||
  Video: VideoModel
 | 
			
		||||
 | 
			
		||||
  @ForeignKey(() => VideoStreamingPlaylistModel)
 | 
			
		||||
  @Column
 | 
			
		||||
  videoStreamingPlaylistId: number
 | 
			
		||||
 | 
			
		||||
  @BelongsTo(() => VideoStreamingPlaylistModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      allowNull: true
 | 
			
		||||
    },
 | 
			
		||||
    onDelete: 'CASCADE'
 | 
			
		||||
  })
 | 
			
		||||
  VideoStreamingPlaylist: VideoStreamingPlaylistModel
 | 
			
		||||
 | 
			
		||||
  @HasMany(() => VideoRedundancyModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      allowNull: true
 | 
			
		||||
| 
						 | 
				
			
			@ -163,6 +205,36 @@ export class VideoFileModel extends Model<VideoFileModel> {
 | 
			
		|||
      }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
 | 
			
		||||
  static async customUpsert (
 | 
			
		||||
    videoFile: MVideoFile,
 | 
			
		||||
    mode: 'streaming-playlist' | 'video',
 | 
			
		||||
    transaction: Transaction
 | 
			
		||||
  ) {
 | 
			
		||||
    const baseWhere = {
 | 
			
		||||
      fps: videoFile.fps,
 | 
			
		||||
      resolution: videoFile.resolution
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
 | 
			
		||||
    else Object.assign(baseWhere, { videoId: videoFile.videoId })
 | 
			
		||||
 | 
			
		||||
    const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
 | 
			
		||||
    if (!element) return videoFile.save({ transaction })
 | 
			
		||||
 | 
			
		||||
    for (const k of Object.keys(videoFile.toJSON())) {
 | 
			
		||||
      element[k] = videoFile[k]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return element.save({ transaction })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
 | 
			
		||||
    if (this.videoId) return (this as MVideoFileVideo).Video
 | 
			
		||||
 | 
			
		||||
    return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isAudio () {
 | 
			
		||||
    return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -170,6 +242,9 @@ export class VideoFileModel extends Model<VideoFileModel> {
 | 
			
		|||
  hasSameUniqueKeysThan (other: MVideoFile) {
 | 
			
		||||
    return this.fps === other.fps &&
 | 
			
		||||
      this.resolution === other.resolution &&
 | 
			
		||||
      this.videoId === other.videoId
 | 
			
		||||
      (
 | 
			
		||||
        (this.videoId !== null && this.videoId === other.videoId) ||
 | 
			
		||||
        (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,6 @@
 | 
			
		|||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
 | 
			
		||||
import { Video, VideoDetails } from '../../../shared/models/videos'
 | 
			
		||||
import { VideoModel } from './video'
 | 
			
		||||
import {
 | 
			
		||||
  ActivityPlaylistInfohashesObject,
 | 
			
		||||
  ActivityPlaylistSegmentHashesObject,
 | 
			
		||||
  ActivityUrlObject,
 | 
			
		||||
  VideoTorrentObject
 | 
			
		||||
} from '../../../shared/models/activitypub/objects'
 | 
			
		||||
import { ActivityTagObject, ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 | 
			
		||||
import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
 | 
			
		||||
import { VideoCaptionModel } from './video-caption'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,9 +11,18 @@ import {
 | 
			
		|||
} from '../../lib/activitypub'
 | 
			
		||||
import { isArray } from '../../helpers/custom-validators/misc'
 | 
			
		||||
import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
 | 
			
		||||
import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models'
 | 
			
		||||
import { MStreamingPlaylistRedundancies } from '../../typings/models/video/video-streaming-playlist'
 | 
			
		||||
import {
 | 
			
		||||
  MStreamingPlaylistRedundanciesOpt,
 | 
			
		||||
  MStreamingPlaylistVideo,
 | 
			
		||||
  MVideo,
 | 
			
		||||
  MVideoAP,
 | 
			
		||||
  MVideoFile,
 | 
			
		||||
  MVideoFormattable,
 | 
			
		||||
  MVideoFormattableDetails
 | 
			
		||||
} from '../../typings/models'
 | 
			
		||||
import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
 | 
			
		||||
import { VideoFile } from '@shared/models/videos/video-file.model'
 | 
			
		||||
import { generateMagnetUri } from '@server/helpers/webtorrent'
 | 
			
		||||
 | 
			
		||||
export type VideoFormattingJSONOptions = {
 | 
			
		||||
  completeDescription?: boolean
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +119,7 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
 | 
			
		|||
 | 
			
		||||
  const tags = video.Tags ? video.Tags.map(t => t.name) : []
 | 
			
		||||
 | 
			
		||||
  const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video.VideoStreamingPlaylists)
 | 
			
		||||
  const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
 | 
			
		||||
 | 
			
		||||
  const detailsJson = {
 | 
			
		||||
    support: video.support,
 | 
			
		||||
| 
						 | 
				
			
			@ -138,33 +142,43 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  // Format and sort video files
 | 
			
		||||
  detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
 | 
			
		||||
  detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles)
 | 
			
		||||
 | 
			
		||||
  return Object.assign(formattedJson, detailsJson)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
 | 
			
		||||
function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
 | 
			
		||||
  if (isArray(playlists) === false) return []
 | 
			
		||||
 | 
			
		||||
  const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
 | 
			
		||||
 | 
			
		||||
  return playlists
 | 
			
		||||
    .map(playlist => {
 | 
			
		||||
      const playlistWithVideo = Object.assign(playlist, { Video: video })
 | 
			
		||||
 | 
			
		||||
      const redundancies = isArray(playlist.RedundancyVideos)
 | 
			
		||||
        ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
 | 
			
		||||
        : []
 | 
			
		||||
 | 
			
		||||
      const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        id: playlist.id,
 | 
			
		||||
        type: playlist.type,
 | 
			
		||||
        playlistUrl: playlist.playlistUrl,
 | 
			
		||||
        segmentsSha256Url: playlist.segmentsSha256Url,
 | 
			
		||||
        redundancies
 | 
			
		||||
      } as VideoStreamingPlaylist
 | 
			
		||||
        redundancies,
 | 
			
		||||
        files
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] {
 | 
			
		||||
  const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
 | 
			
		||||
 | 
			
		||||
function videoFilesModelToFormattedJSON (
 | 
			
		||||
  model: MVideo | MStreamingPlaylistVideo,
 | 
			
		||||
  baseUrlHttp: string,
 | 
			
		||||
  baseUrlWs: string,
 | 
			
		||||
  videoFiles: MVideoFileRedundanciesOpt[]
 | 
			
		||||
): VideoFile[] {
 | 
			
		||||
  return videoFiles
 | 
			
		||||
    .map(videoFile => {
 | 
			
		||||
      let resolutionLabel = videoFile.resolution + 'p'
 | 
			
		||||
| 
						 | 
				
			
			@ -174,13 +188,13 @@ function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRe
 | 
			
		|||
          id: videoFile.resolution,
 | 
			
		||||
          label: resolutionLabel
 | 
			
		||||
        },
 | 
			
		||||
        magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
 | 
			
		||||
        magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs),
 | 
			
		||||
        size: videoFile.size,
 | 
			
		||||
        fps: videoFile.fps,
 | 
			
		||||
        torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp),
 | 
			
		||||
        torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp),
 | 
			
		||||
        fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp),
 | 
			
		||||
        fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
 | 
			
		||||
        torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp),
 | 
			
		||||
        torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
 | 
			
		||||
        fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp),
 | 
			
		||||
        fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
 | 
			
		||||
      } as VideoFile
 | 
			
		||||
    })
 | 
			
		||||
    .sort((a, b) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +204,39 @@ function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRe
 | 
			
		|||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addVideoFilesInAPAcc (
 | 
			
		||||
  acc: ActivityUrlObject[] | ActivityTagObject[],
 | 
			
		||||
  model: MVideoAP | MStreamingPlaylistVideo,
 | 
			
		||||
  baseUrlHttp: string,
 | 
			
		||||
  baseUrlWs: string,
 | 
			
		||||
  files: MVideoFile[]
 | 
			
		||||
) {
 | 
			
		||||
  for (const file of files) {
 | 
			
		||||
    acc.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
 | 
			
		||||
      href: model.getVideoFileUrl(file, baseUrlHttp),
 | 
			
		||||
      height: file.resolution,
 | 
			
		||||
      size: file.size,
 | 
			
		||||
      fps: file.fps
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    acc.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
 | 
			
		||||
      href: model.getTorrentUrl(file, baseUrlHttp),
 | 
			
		||||
      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),
 | 
			
		||||
      height: file.resolution
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
 | 
			
		||||
  const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
 | 
			
		||||
  if (!video.Tags) video.Tags = []
 | 
			
		||||
| 
						 | 
				
			
			@ -224,50 +271,25 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  const url: ActivityUrlObject[] = []
 | 
			
		||||
  for (const file of video.VideoFiles) {
 | 
			
		||||
    url.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
 | 
			
		||||
      mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
 | 
			
		||||
      href: video.getVideoFileUrl(file, baseUrlHttp),
 | 
			
		||||
      height: file.resolution,
 | 
			
		||||
      size: file.size,
 | 
			
		||||
      fps: file.fps
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    url.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
 | 
			
		||||
      mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
 | 
			
		||||
      href: video.getTorrentUrl(file, baseUrlHttp),
 | 
			
		||||
      height: file.resolution
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    url.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
 | 
			
		||||
      mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
 | 
			
		||||
      href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
 | 
			
		||||
      height: file.resolution
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
 | 
			
		||||
 | 
			
		||||
  for (const playlist of (video.VideoStreamingPlaylists || [])) {
 | 
			
		||||
    let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
 | 
			
		||||
    let tag: ActivityTagObject[]
 | 
			
		||||
 | 
			
		||||
    tag = playlist.p2pMediaLoaderInfohashes
 | 
			
		||||
                  .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
 | 
			
		||||
    tag.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      name: 'sha256',
 | 
			
		||||
      mimeType: 'application/json' as 'application/json',
 | 
			
		||||
      mediaType: 'application/json' as 'application/json',
 | 
			
		||||
      href: playlist.segmentsSha256Url
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const playlistWithVideo = Object.assign(playlist, { Video: video })
 | 
			
		||||
    addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
 | 
			
		||||
 | 
			
		||||
    url.push({
 | 
			
		||||
      type: 'Link',
 | 
			
		||||
      mimeType: 'application/x-mpegURL' as 'application/x-mpegURL',
 | 
			
		||||
      mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
 | 
			
		||||
      href: playlist.playlistUrl,
 | 
			
		||||
      tag
 | 
			
		||||
| 
						 | 
				
			
			@ -277,7 +299,6 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
 | 
			
		|||
  // Add video url too
 | 
			
		||||
  url.push({
 | 
			
		||||
    type: 'Link',
 | 
			
		||||
    mimeType: 'text/html',
 | 
			
		||||
    mediaType: 'text/html',
 | 
			
		||||
    href: WEBSERVER.URL + '/videos/watch/' + video.uuid
 | 
			
		||||
  })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,12 +5,14 @@ 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, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
 | 
			
		||||
import { CONSTRAINTS_FIELDS, 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, MVideoFile } from '@server/typings/models'
 | 
			
		||||
import { VideoFileModel } from '@server/models/video/video-file'
 | 
			
		||||
import { getTorrentFileName, getVideoFilename } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
@Table({
 | 
			
		||||
  tableName: 'videoStreamingPlaylist',
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +72,14 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
 | 
			
		|||
  })
 | 
			
		||||
  Video: VideoModel
 | 
			
		||||
 | 
			
		||||
  @HasMany(() => VideoFileModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      allowNull: true
 | 
			
		||||
    },
 | 
			
		||||
    onDelete: 'CASCADE'
 | 
			
		||||
  })
 | 
			
		||||
  VideoFiles: VideoFileModel[]
 | 
			
		||||
 | 
			
		||||
  @HasMany(() => VideoRedundancyModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      allowNull: false
 | 
			
		||||
| 
						 | 
				
			
			@ -91,11 +101,11 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
 | 
			
		|||
              .then(results => results.length === 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: MVideoFile[]) {
 | 
			
		||||
  static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
 | 
			
		||||
    const hashes: string[] = []
 | 
			
		||||
 | 
			
		||||
    // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
 | 
			
		||||
    for (let i = 0; i < videoFiles.length; i++) {
 | 
			
		||||
    for (let i = 0; i < files.length; i++) {
 | 
			
		||||
      hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -139,10 +149,6 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
 | 
			
		|||
    return 'segments-sha256.json'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getHlsVideoName (uuid: string, resolution: number) {
 | 
			
		||||
    return `${uuid}-${resolution}-fragmented.mp4`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getHlsMasterPlaylistStaticPath (videoUUID: string) {
 | 
			
		||||
    return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -165,6 +171,26 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
 | 
			
		|||
    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' ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hasSameUniqueKeysThan (other: MStreamingPlaylist) {
 | 
			
		||||
    return this.type === other.type &&
 | 
			
		||||
      this.videoId === other.videoId
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,5 @@
 | 
			
		|||
import * as Bluebird from 'bluebird'
 | 
			
		||||
import { maxBy } from 'lodash'
 | 
			
		||||
import * as magnetUtil from 'magnet-uri'
 | 
			
		||||
import * as parseTorrent from 'parse-torrent'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import {
 | 
			
		||||
  CountOptions,
 | 
			
		||||
| 
						 | 
				
			
			@ -38,11 +36,11 @@ import {
 | 
			
		|||
} from 'sequelize-typescript'
 | 
			
		||||
import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
 | 
			
		||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 | 
			
		||||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
 | 
			
		||||
import { Video, VideoDetails } from '../../../shared/models/videos'
 | 
			
		||||
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
 | 
			
		||||
import { peertubeTruncate } from '../../helpers/core-utils'
 | 
			
		||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 | 
			
		||||
import { isArray, isBooleanValid } from '../../helpers/custom-validators/misc'
 | 
			
		||||
import { isBooleanValid } from '../../helpers/custom-validators/misc'
 | 
			
		||||
import {
 | 
			
		||||
  isVideoCategoryValid,
 | 
			
		||||
  isVideoDescriptionValid,
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +98,7 @@ import { VideoTagModel } from './video-tag'
 | 
			
		|||
import { ScheduleVideoUpdateModel } from './schedule-video-update'
 | 
			
		||||
import { VideoCaptionModel } from './video-caption'
 | 
			
		||||
import { VideoBlacklistModel } from './video-blacklist'
 | 
			
		||||
import { remove, writeFile } from 'fs-extra'
 | 
			
		||||
import { remove } from 'fs-extra'
 | 
			
		||||
import { VideoViewModel } from './video-views'
 | 
			
		||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -117,18 +115,20 @@ import { VideoPlaylistElementModel } from './video-playlist-element'
 | 
			
		|||
import { CONFIG } from '../../initializers/config'
 | 
			
		||||
import { ThumbnailModel } from './thumbnail'
 | 
			
		||||
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 | 
			
		||||
import { createTorrentPromise } from '../../helpers/webtorrent'
 | 
			
		||||
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 | 
			
		||||
import {
 | 
			
		||||
  MChannel,
 | 
			
		||||
  MChannelAccountDefault,
 | 
			
		||||
  MChannelId,
 | 
			
		||||
  MStreamingPlaylist,
 | 
			
		||||
  MStreamingPlaylistFilesVideo,
 | 
			
		||||
  MUserAccountId,
 | 
			
		||||
  MUserId,
 | 
			
		||||
  MVideoAccountLight,
 | 
			
		||||
  MVideoAccountLightBlacklistAllFiles,
 | 
			
		||||
  MVideoAP,
 | 
			
		||||
  MVideoDetails,
 | 
			
		||||
  MVideoFileVideo,
 | 
			
		||||
  MVideoFormattable,
 | 
			
		||||
  MVideoFormattableDetails,
 | 
			
		||||
  MVideoForUser,
 | 
			
		||||
| 
						 | 
				
			
			@ -140,8 +140,10 @@ import {
 | 
			
		|||
  MVideoWithFile,
 | 
			
		||||
  MVideoWithRights
 | 
			
		||||
} from '../../typings/models'
 | 
			
		||||
import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
 | 
			
		||||
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
 | 
			
		||||
import { MThumbnail } from '../../typings/models/video/thumbnail'
 | 
			
		||||
import { VideoFile } from '@shared/models/videos/video-file.model'
 | 
			
		||||
import { getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
 | 
			
		||||
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 | 
			
		||||
const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
 | 
			
		||||
| 
						 | 
				
			
			@ -211,7 +213,7 @@ export enum ScopeNames {
 | 
			
		|||
  FOR_API = 'FOR_API',
 | 
			
		||||
  WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
 | 
			
		||||
  WITH_TAGS = 'WITH_TAGS',
 | 
			
		||||
  WITH_FILES = 'WITH_FILES',
 | 
			
		||||
  WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
 | 
			
		||||
  WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
 | 
			
		||||
  WITH_BLACKLISTED = 'WITH_BLACKLISTED',
 | 
			
		||||
  WITH_BLOCKLIST = 'WITH_BLOCKLIST',
 | 
			
		||||
| 
						 | 
				
			
			@ -666,7 +668,7 @@ export type AvailableForListIDsOptions = {
 | 
			
		|||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => {
 | 
			
		||||
  [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => {
 | 
			
		||||
    let subInclude: any[] = []
 | 
			
		||||
 | 
			
		||||
    if (withRedundancies === true) {
 | 
			
		||||
| 
						 | 
				
			
			@ -691,16 +693,19 @@ export type AvailableForListIDsOptions = {
 | 
			
		|||
    }
 | 
			
		||||
  },
 | 
			
		||||
  [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
 | 
			
		||||
    let subInclude: any[] = []
 | 
			
		||||
    const subInclude: IncludeOptions[] = [
 | 
			
		||||
      {
 | 
			
		||||
        model: VideoFileModel.unscoped(),
 | 
			
		||||
        required: false
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if (withRedundancies === true) {
 | 
			
		||||
      subInclude = [
 | 
			
		||||
        {
 | 
			
		||||
          attributes: [ 'fileUrl' ],
 | 
			
		||||
          model: VideoRedundancyModel.unscoped(),
 | 
			
		||||
          required: false
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
      subInclude.push({
 | 
			
		||||
        attributes: [ 'fileUrl' ],
 | 
			
		||||
        model: VideoRedundancyModel.unscoped(),
 | 
			
		||||
        required: false
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -913,7 +918,7 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
  @HasMany(() => VideoFileModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      name: 'videoId',
 | 
			
		||||
      allowNull: false
 | 
			
		||||
      allowNull: true
 | 
			
		||||
    },
 | 
			
		||||
    hooks: true,
 | 
			
		||||
    onDelete: 'cascade'
 | 
			
		||||
| 
						 | 
				
			
			@ -1071,7 +1076,7 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    return VideoModel.scope([
 | 
			
		||||
      ScopeNames.WITH_FILES,
 | 
			
		||||
      ScopeNames.WITH_WEBTORRENT_FILES,
 | 
			
		||||
      ScopeNames.WITH_STREAMING_PLAYLISTS,
 | 
			
		||||
      ScopeNames.WITH_THUMBNAILS
 | 
			
		||||
    ]).findAll(query)
 | 
			
		||||
| 
						 | 
				
			
			@ -1463,7 +1468,7 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    return VideoModel.scope([
 | 
			
		||||
      ScopeNames.WITH_FILES,
 | 
			
		||||
      ScopeNames.WITH_WEBTORRENT_FILES,
 | 
			
		||||
      ScopeNames.WITH_STREAMING_PLAYLISTS,
 | 
			
		||||
      ScopeNames.WITH_THUMBNAILS
 | 
			
		||||
    ]).findOne(query)
 | 
			
		||||
| 
						 | 
				
			
			@ -1500,7 +1505,7 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
 | 
			
		||||
    return VideoModel.scope([
 | 
			
		||||
      ScopeNames.WITH_ACCOUNT_DETAILS,
 | 
			
		||||
      ScopeNames.WITH_FILES,
 | 
			
		||||
      ScopeNames.WITH_WEBTORRENT_FILES,
 | 
			
		||||
      ScopeNames.WITH_STREAMING_PLAYLISTS,
 | 
			
		||||
      ScopeNames.WITH_THUMBNAILS,
 | 
			
		||||
      ScopeNames.WITH_BLACKLISTED
 | 
			
		||||
| 
						 | 
				
			
			@ -1521,7 +1526,7 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
      ScopeNames.WITH_BLACKLISTED,
 | 
			
		||||
      ScopeNames.WITH_ACCOUNT_DETAILS,
 | 
			
		||||
      ScopeNames.WITH_SCHEDULED_UPDATE,
 | 
			
		||||
      ScopeNames.WITH_FILES,
 | 
			
		||||
      ScopeNames.WITH_WEBTORRENT_FILES,
 | 
			
		||||
      ScopeNames.WITH_STREAMING_PLAYLISTS,
 | 
			
		||||
      ScopeNames.WITH_THUMBNAILS
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -1555,7 +1560,7 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
      ScopeNames.WITH_ACCOUNT_DETAILS,
 | 
			
		||||
      ScopeNames.WITH_SCHEDULED_UPDATE,
 | 
			
		||||
      ScopeNames.WITH_THUMBNAILS,
 | 
			
		||||
      { method: [ ScopeNames.WITH_FILES, true ] },
 | 
			
		||||
      { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
 | 
			
		||||
      { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1787,17 +1792,31 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
      this.VideoChannel.Account.isBlocked()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getOriginalFile <T extends MVideoWithFile> (this: T) {
 | 
			
		||||
    if (Array.isArray(this.VideoFiles) === false) return undefined
 | 
			
		||||
  getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
 | 
			
		||||
    if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
 | 
			
		||||
      const file = maxBy(this.VideoFiles, file => file.resolution)
 | 
			
		||||
 | 
			
		||||
    // The original file is the file that have the higher resolution
 | 
			
		||||
    return maxBy(this.VideoFiles, file => file.resolution)
 | 
			
		||||
      return Object.assign(file, { Video: this })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // No webtorrent files, try with streaming playlist files
 | 
			
		||||
    if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
 | 
			
		||||
      const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
 | 
			
		||||
 | 
			
		||||
      const file = maxBy(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
 | 
			
		||||
      return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFile <T extends MVideoWithFile> (this: T, resolution: number) {
 | 
			
		||||
  getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
 | 
			
		||||
    if (Array.isArray(this.VideoFiles) === false) return undefined
 | 
			
		||||
 | 
			
		||||
    return this.VideoFiles.find(f => f.resolution === resolution)
 | 
			
		||||
    const file = this.VideoFiles.find(f => f.resolution === resolution)
 | 
			
		||||
    if (!file) return undefined
 | 
			
		||||
 | 
			
		||||
    return Object.assign(file, { Video: this })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1813,10 +1832,6 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    this.Thumbnails.push(savedThumbnail)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoFilename (videoFile: MVideoFile) {
 | 
			
		||||
    return this.uuid + '-' + videoFile.resolution + videoFile.extname
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateThumbnailName () {
 | 
			
		||||
    return this.uuid + '.jpg'
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -1837,46 +1852,10 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTorrentFileName (videoFile: MVideoFile) {
 | 
			
		||||
    const extension = '.torrent'
 | 
			
		||||
    return this.uuid + '-' + videoFile.resolution + extension
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isOwned () {
 | 
			
		||||
    return this.remote === false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTorrentFilePath (videoFile: MVideoFile) {
 | 
			
		||||
    return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoFilePath (videoFile: MVideoFile) {
 | 
			
		||||
    return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
 | 
			
		||||
    const options = {
 | 
			
		||||
      // Keep the extname, it's used by the client to stream the file inside a web browser
 | 
			
		||||
      name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
 | 
			
		||||
      createdBy: 'PeerTube',
 | 
			
		||||
      announceList: [
 | 
			
		||||
        [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
 | 
			
		||||
        [ WEBSERVER.URL + '/tracker/announce' ]
 | 
			
		||||
      ],
 | 
			
		||||
      urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
 | 
			
		||||
 | 
			
		||||
    const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
 | 
			
		||||
    logger.info('Creating torrent %s.', filePath)
 | 
			
		||||
 | 
			
		||||
    await writeFile(filePath, torrent)
 | 
			
		||||
 | 
			
		||||
    const parsedTorrent = parseTorrent(torrent)
 | 
			
		||||
    videoFile.infoHash = parsedTorrent.infoHash
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWatchStaticPath () {
 | 
			
		||||
    return '/videos/watch/' + this.uuid
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -1909,7 +1888,8 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  getFormattedVideoFilesJSON (): VideoFile[] {
 | 
			
		||||
    return videoFilesModelToFormattedJSON(this, this.VideoFiles)
 | 
			
		||||
    const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
 | 
			
		||||
    return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toActivityPubObject (this: MVideoAP): VideoTorrentObject {
 | 
			
		||||
| 
						 | 
				
			
			@ -1923,8 +1903,10 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    return peertubeTruncate(this.description, { length: maxLength })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getOriginalFileResolution () {
 | 
			
		||||
    const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
 | 
			
		||||
  getMaxQualityResolution () {
 | 
			
		||||
    const file = this.getMaxQualityFile()
 | 
			
		||||
    const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
 | 
			
		||||
    const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
 | 
			
		||||
 | 
			
		||||
    return getVideoFileResolution(originalFilePath)
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -1933,22 +1915,36 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    return `/api/${API_VERSION}/videos/${this.uuid}/description`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHLSPlaylist () {
 | 
			
		||||
  getHLSPlaylist (): MStreamingPlaylistFilesVideo {
 | 
			
		||||
    if (!this.VideoStreamingPlaylists) return undefined
 | 
			
		||||
 | 
			
		||||
    return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
 | 
			
		||||
    const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
 | 
			
		||||
    playlist.Video = this
 | 
			
		||||
 | 
			
		||||
    return playlist
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setHLSPlaylist (playlist: MStreamingPlaylist) {
 | 
			
		||||
    const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
 | 
			
		||||
 | 
			
		||||
    if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
 | 
			
		||||
      this.VideoStreamingPlaylists = toAdd
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
 | 
			
		||||
      .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
 | 
			
		||||
      .concat(toAdd)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeFile (videoFile: MVideoFile, isRedundancy = false) {
 | 
			
		||||
    const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
 | 
			
		||||
 | 
			
		||||
    const filePath = join(baseDir, this.getVideoFilename(videoFile))
 | 
			
		||||
    const filePath = getVideoFilePath(this, videoFile, isRedundancy)
 | 
			
		||||
    return remove(filePath)
 | 
			
		||||
      .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeTorrent (videoFile: MVideoFile) {
 | 
			
		||||
    const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
 | 
			
		||||
    const torrentPath = getTorrentFilePath(this, videoFile)
 | 
			
		||||
    return remove(torrentPath)
 | 
			
		||||
      .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -1973,38 +1969,30 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
    return this.save()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBaseUrls () {
 | 
			
		||||
    let baseUrlHttp
 | 
			
		||||
    let baseUrlWs
 | 
			
		||||
  async publishIfNeededAndSave (t: Transaction) {
 | 
			
		||||
    if (this.state !== VideoState.PUBLISHED) {
 | 
			
		||||
      this.state = VideoState.PUBLISHED
 | 
			
		||||
      this.publishedAt = new Date()
 | 
			
		||||
      await this.save({ transaction: t })
 | 
			
		||||
 | 
			
		||||
    if (this.isOwned()) {
 | 
			
		||||
      baseUrlHttp = WEBSERVER.URL
 | 
			
		||||
      baseUrlWs = WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
 | 
			
		||||
    } else {
 | 
			
		||||
      baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
 | 
			
		||||
      baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { baseUrlHttp, baseUrlWs }
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) {
 | 
			
		||||
    const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
 | 
			
		||||
    const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
 | 
			
		||||
    let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
 | 
			
		||||
 | 
			
		||||
    const redundancies = videoFile.RedundancyVideos
 | 
			
		||||
    if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
 | 
			
		||||
 | 
			
		||||
    const magnetHash = {
 | 
			
		||||
      xs,
 | 
			
		||||
      announce,
 | 
			
		||||
      urlList,
 | 
			
		||||
      infoHash: videoFile.infoHash,
 | 
			
		||||
      name: this.name
 | 
			
		||||
  getBaseUrls () {
 | 
			
		||||
    if (this.isOwned()) {
 | 
			
		||||
      return {
 | 
			
		||||
        baseUrlHttp: WEBSERVER.URL,
 | 
			
		||||
        baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return magnetUtil.encode(magnetHash)
 | 
			
		||||
    return {
 | 
			
		||||
      baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
 | 
			
		||||
      baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
 | 
			
		||||
| 
						 | 
				
			
			@ -2012,23 +2000,23 @@ export class VideoModel extends Model<VideoModel> {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
 | 
			
		||||
    return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
 | 
			
		||||
    return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
 | 
			
		||||
    return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
 | 
			
		||||
    return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
 | 
			
		||||
    return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
 | 
			
		||||
    return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
 | 
			
		||||
    return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
 | 
			
		||||
    return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
 | 
			
		||||
    return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
 | 
			
		||||
    return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBandwidthBits (videoFile: MVideoFile) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,6 +92,9 @@ describe('Test config API validators', function () {
 | 
			
		|||
        '1080p': false,
 | 
			
		||||
        '2160p': false
 | 
			
		||||
      },
 | 
			
		||||
      webtorrent: {
 | 
			
		||||
        enabled: true
 | 
			
		||||
      },
 | 
			
		||||
      hls: {
 | 
			
		||||
        enabled: false
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -235,6 +238,27 @@ describe('Test config API validators', function () {
 | 
			
		|||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a disabled webtorrent & hls transcoding', async function () {
 | 
			
		||||
      const newUpdateParams = immutableAssign(updateParams, {
 | 
			
		||||
        transcoding: {
 | 
			
		||||
          hls: {
 | 
			
		||||
            enabled: false
 | 
			
		||||
          },
 | 
			
		||||
          webtorrent: {
 | 
			
		||||
            enabled: false
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await makePutBodyRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path,
 | 
			
		||||
        fields: newUpdateParams,
 | 
			
		||||
        token: server.accessToken,
 | 
			
		||||
        statusCodeExpected: 400
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should success with the correct parameters', async function () {
 | 
			
		||||
      await makePutBodyRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,6 +72,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
 | 
			
		|||
  expect(data.transcoding.resolutions['720p']).to.be.true
 | 
			
		||||
  expect(data.transcoding.resolutions['1080p']).to.be.true
 | 
			
		||||
  expect(data.transcoding.resolutions['2160p']).to.be.true
 | 
			
		||||
  expect(data.transcoding.webtorrent.enabled).to.be.true
 | 
			
		||||
  expect(data.transcoding.hls.enabled).to.be.true
 | 
			
		||||
 | 
			
		||||
  expect(data.import.videos.http.enabled).to.be.true
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +141,7 @@ function checkUpdatedConfig (data: CustomConfig) {
 | 
			
		|||
  expect(data.transcoding.resolutions['1080p']).to.be.false
 | 
			
		||||
  expect(data.transcoding.resolutions['2160p']).to.be.false
 | 
			
		||||
  expect(data.transcoding.hls.enabled).to.be.false
 | 
			
		||||
  expect(data.transcoding.webtorrent.enabled).to.be.true
 | 
			
		||||
 | 
			
		||||
  expect(data.import.videos.http.enabled).to.be.false
 | 
			
		||||
  expect(data.import.videos.torrent.enabled).to.be.false
 | 
			
		||||
| 
						 | 
				
			
			@ -279,6 +281,9 @@ describe('Test config', function () {
 | 
			
		|||
          '1080p': false,
 | 
			
		||||
          '2160p': false
 | 
			
		||||
        },
 | 
			
		||||
        webtorrent: {
 | 
			
		||||
          enabled: true
 | 
			
		||||
        },
 | 
			
		||||
        hls: {
 | 
			
		||||
          enabled: false
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,13 +10,13 @@ import {
 | 
			
		|||
  doubleFollow,
 | 
			
		||||
  flushAndRunMultipleServers,
 | 
			
		||||
  getPlaylist,
 | 
			
		||||
  getVideo,
 | 
			
		||||
  getVideo, makeGetRequest, makeRawRequest,
 | 
			
		||||
  removeVideo,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  setAccessTokensToServers, updateCustomSubConfig,
 | 
			
		||||
  updateVideo,
 | 
			
		||||
  uploadVideo,
 | 
			
		||||
  waitJobs
 | 
			
		||||
  waitJobs, webtorrentAdd
 | 
			
		||||
} from '../../../../shared/extra-utils'
 | 
			
		||||
import { VideoDetails } from '../../../../shared/models/videos'
 | 
			
		||||
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
 | 
			
		||||
| 
						 | 
				
			
			@ -25,20 +25,45 @@ import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
 | 
			
		|||
 | 
			
		||||
const expect = chai.expect
 | 
			
		||||
 | 
			
		||||
async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resolutions = [ 240, 360, 480, 720 ]) {
 | 
			
		||||
async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOnly: boolean, resolutions = [ 240, 360, 480, 720 ]) {
 | 
			
		||||
  for (const server of servers) {
 | 
			
		||||
    const res = await getVideo(server.url, videoUUID)
 | 
			
		||||
    const videoDetails: VideoDetails = res.body
 | 
			
		||||
    const resVideoDetails = await getVideo(server.url, videoUUID)
 | 
			
		||||
    const videoDetails: VideoDetails = resVideoDetails.body
 | 
			
		||||
    const baseUrl = `http://${videoDetails.account.host}`
 | 
			
		||||
 | 
			
		||||
    expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
 | 
			
		||||
 | 
			
		||||
    const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
 | 
			
		||||
    expect(hlsPlaylist).to.not.be.undefined
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      const res2 = await getPlaylist(hlsPlaylist.playlistUrl)
 | 
			
		||||
    const hlsFiles = hlsPlaylist.files
 | 
			
		||||
    expect(hlsFiles).to.have.lengthOf(resolutions.length)
 | 
			
		||||
 | 
			
		||||
      const masterPlaylist = res2.text
 | 
			
		||||
    if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0)
 | 
			
		||||
    else expect(videoDetails.files).to.have.lengthOf(resolutions.length)
 | 
			
		||||
 | 
			
		||||
    for (const resolution of resolutions) {
 | 
			
		||||
      const file = hlsFiles.find(f => f.resolution.id === resolution)
 | 
			
		||||
      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.fileUrl).to.equal(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`)
 | 
			
		||||
      expect(file.resolution.label).to.equal(resolution + 'p')
 | 
			
		||||
 | 
			
		||||
      await makeRawRequest(file.torrentUrl, 200)
 | 
			
		||||
      await makeRawRequest(file.fileUrl, 200)
 | 
			
		||||
 | 
			
		||||
      const torrent = await webtorrentAdd(file.magnetUri, true)
 | 
			
		||||
      expect(torrent.files).to.be.an('array')
 | 
			
		||||
      expect(torrent.files.length).to.equal(1)
 | 
			
		||||
      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      const res = await getPlaylist(hlsPlaylist.playlistUrl)
 | 
			
		||||
 | 
			
		||||
      const masterPlaylist = res.text
 | 
			
		||||
 | 
			
		||||
      for (const resolution of resolutions) {
 | 
			
		||||
        expect(masterPlaylist).to.match(new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+'))
 | 
			
		||||
| 
						 | 
				
			
			@ -48,18 +73,18 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resol
 | 
			
		|||
 | 
			
		||||
    {
 | 
			
		||||
      for (const resolution of resolutions) {
 | 
			
		||||
        const res2 = await getPlaylist(`http://localhost:${servers[0].port}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
 | 
			
		||||
        const res = await getPlaylist(`${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
 | 
			
		||||
 | 
			
		||||
        const subPlaylist = res2.text
 | 
			
		||||
        const subPlaylist = res.text
 | 
			
		||||
        expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      const baseUrl = 'http://localhost:' + servers[0].port + '/static/streaming-playlists/hls'
 | 
			
		||||
      const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls'
 | 
			
		||||
 | 
			
		||||
      for (const resolution of resolutions) {
 | 
			
		||||
        await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
 | 
			
		||||
        await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +95,67 @@ describe('Test HLS videos', function () {
 | 
			
		|||
  let videoUUID = ''
 | 
			
		||||
  let videoAudioUUID = ''
 | 
			
		||||
 | 
			
		||||
  function runTestSuite (hlsOnly: boolean) {
 | 
			
		||||
    it('Should upload a video and transcode it to HLS', async function () {
 | 
			
		||||
      this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
      const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
 | 
			
		||||
      videoUUID = res.body.video.uuid
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
      await checkHlsPlaylist(servers, videoUUID, hlsOnly)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should upload an audio file and transcode it to HLS', async function () {
 | 
			
		||||
      this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
      const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
 | 
			
		||||
      videoAudioUUID = res.body.video.uuid
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
      await checkHlsPlaylist(servers, videoAudioUUID, hlsOnly, [ DEFAULT_AUDIO_RESOLUTION ])
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should update the video', async function () {
 | 
			
		||||
      this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
      await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, { name: 'video 1 updated' })
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
      await checkHlsPlaylist(servers, videoUUID, hlsOnly)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should delete videos', async function () {
 | 
			
		||||
      this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
      await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID)
 | 
			
		||||
      await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAudioUUID)
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
      for (const server of servers) {
 | 
			
		||||
        await getVideo(server.url, videoUUID, 404)
 | 
			
		||||
        await getVideo(server.url, videoAudioUUID, 404)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should have the playlists/segment deleted from the disk', async function () {
 | 
			
		||||
      for (const server of servers) {
 | 
			
		||||
        await checkDirectoryIsEmpty(server, 'videos')
 | 
			
		||||
        await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should have an empty tmp directory', async function () {
 | 
			
		||||
      for (const server of servers) {
 | 
			
		||||
        await checkTmpIsEmpty(server)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -91,63 +177,36 @@ describe('Test HLS videos', function () {
 | 
			
		|||
    await doubleFollow(servers[0], servers[1])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should upload a video and transcode it to HLS', async function () {
 | 
			
		||||
    this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
    const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
 | 
			
		||||
    videoUUID = res.body.video.uuid
 | 
			
		||||
 | 
			
		||||
    await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
    await checkHlsPlaylist(servers, videoUUID)
 | 
			
		||||
  describe('With WebTorrent & HLS enabled', function () {
 | 
			
		||||
    runTestSuite(false)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should upload an audio file and transcode it to HLS', async function () {
 | 
			
		||||
    this.timeout(120000)
 | 
			
		||||
  describe('With only HLS enabled', function () {
 | 
			
		||||
 | 
			
		||||
    const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
 | 
			
		||||
    videoAudioUUID = res.body.video.uuid
 | 
			
		||||
    before(async function () {
 | 
			
		||||
      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
 | 
			
		||||
        transcoding: {
 | 
			
		||||
          enabled: true,
 | 
			
		||||
          allowAudioFiles: true,
 | 
			
		||||
          resolutions: {
 | 
			
		||||
            '240p': true,
 | 
			
		||||
            '360p': true,
 | 
			
		||||
            '480p': true,
 | 
			
		||||
            '720p': true,
 | 
			
		||||
            '1080p': true,
 | 
			
		||||
            '2160p': true
 | 
			
		||||
          },
 | 
			
		||||
          hls: {
 | 
			
		||||
            enabled: true
 | 
			
		||||
          },
 | 
			
		||||
          webtorrent: {
 | 
			
		||||
            enabled: false
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
    await checkHlsPlaylist(servers, videoAudioUUID, [ DEFAULT_AUDIO_RESOLUTION ])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should update the video', async function () {
 | 
			
		||||
    this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
    await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' })
 | 
			
		||||
 | 
			
		||||
    await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
    await checkHlsPlaylist(servers, videoUUID)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should delete videos', async function () {
 | 
			
		||||
    this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
    await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
 | 
			
		||||
    await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID)
 | 
			
		||||
 | 
			
		||||
    await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
    for (const server of servers) {
 | 
			
		||||
      await getVideo(server.url, videoUUID, 404)
 | 
			
		||||
      await getVideo(server.url, videoAudioUUID, 404)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have the playlists/segment deleted from the disk', async function () {
 | 
			
		||||
    for (const server of servers) {
 | 
			
		||||
      await checkDirectoryIsEmpty(server, 'videos')
 | 
			
		||||
      await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'))
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have an empty tmp directory', async function () {
 | 
			
		||||
    for (const server of servers) {
 | 
			
		||||
      await checkTmpIsEmpty(server)
 | 
			
		||||
    }
 | 
			
		||||
    runTestSuite(true)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  after(async function () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,22 +2,21 @@
 | 
			
		|||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import * as chai from 'chai'
 | 
			
		||||
import { VideoDetails, VideoFile } from '../../../shared/models/videos'
 | 
			
		||||
import { VideoDetails } from '../../../shared/models/videos'
 | 
			
		||||
import {
 | 
			
		||||
  cleanupTests,
 | 
			
		||||
  doubleFollow,
 | 
			
		||||
  execCLI,
 | 
			
		||||
  flushAndRunMultipleServers,
 | 
			
		||||
  flushTests,
 | 
			
		||||
  getEnvCli,
 | 
			
		||||
  getVideo,
 | 
			
		||||
  getVideosList,
 | 
			
		||||
  killallServers,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  uploadVideo
 | 
			
		||||
} from '../../../shared/extra-utils'
 | 
			
		||||
import { waitJobs } from '../../../shared/extra-utils/server/jobs'
 | 
			
		||||
import { VideoFile } from '@shared/models/videos/video-file.model'
 | 
			
		||||
 | 
			
		||||
const expect = chai.expect
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ import {
 | 
			
		|||
} from './actor'
 | 
			
		||||
import { FunctionProperties, PickWith } from '../../utils'
 | 
			
		||||
import { MAccountBlocklistId } from './account-blocklist'
 | 
			
		||||
import { MChannelDefault } from '@server/typings/models'
 | 
			
		||||
import { MChannelDefault } from '../video/video-channels'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,16 @@
 | 
			
		|||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 | 
			
		||||
import {
 | 
			
		||||
  MActor,
 | 
			
		||||
  MActorAccount,
 | 
			
		||||
  MActorDefaultAccountChannel,
 | 
			
		||||
  MActorChannelAccountActor,
 | 
			
		||||
  MActorDefault,
 | 
			
		||||
  MActorDefaultAccountChannel,
 | 
			
		||||
  MActorFormattable,
 | 
			
		||||
  MActorHost,
 | 
			
		||||
  MActorUsername
 | 
			
		||||
} from './actor'
 | 
			
		||||
import { PickWith } from '../../utils'
 | 
			
		||||
import { ActorModel } from '@server/models/activitypub/actor'
 | 
			
		||||
import { MChannelDefault } from '@server/typings/models'
 | 
			
		||||
import { MChannelDefault } from '../video/video-channels'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MUserAccountUrl } from '@server/typings/models'
 | 
			
		||||
import { MUserAccountUrl } from '../user/user'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MAccountDefault, MAccountFormattable, MServer, MServerFormattable } from '@server/typings/models'
 | 
			
		||||
import { MAccountDefault, MAccountFormattable } from '../account/account'
 | 
			
		||||
import { MServer, MServerFormattable } from './server'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof ServerBlocklistModel, M> = PickWith<ServerBlocklistModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ import {
 | 
			
		|||
} from '../account'
 | 
			
		||||
import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
 | 
			
		||||
import { AccountModel } from '@server/models/account/account'
 | 
			
		||||
import { MChannelFormattable } from '@server/typings/models'
 | 
			
		||||
import { MChannelFormattable } from '../video/video-channels'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,18 @@
 | 
			
		|||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MVideoAPWithoutCaption, MVideoWithBlacklistLight } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof ScheduleVideoUpdateModel, M> = PickWith<ScheduleVideoUpdateModel, K, M>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
export type MScheduleVideoUpdateVideoAll = MScheduleVideoUpdate &
 | 
			
		||||
  Use<'Video', MVideoAPWithoutCaption & MVideoWithBlacklistLight>
 | 
			
		||||
 | 
			
		||||
// Format for API or AP object
 | 
			
		||||
 | 
			
		||||
export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MVideo, MVideoFormattable } from '@server/typings/models'
 | 
			
		||||
import { MVideo, MVideoFormattable } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoBlacklistModel, M> = PickWith<VideoBlacklistModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { VideoCaptionModel } from '../../../models/video/video-caption'
 | 
			
		||||
import { FunctionProperties, PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MVideo, MVideoUUID } from '@server/typings/models'
 | 
			
		||||
import { MVideo, MVideoUUID } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MAccountDefault, MAccountFormattable, MVideo, MVideoWithFileThumbnail } from '@server/typings/models'
 | 
			
		||||
import { MAccountDefault, MAccountFormattable } from '../account/account'
 | 
			
		||||
import { MVideo, MVideoWithAllFiles } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwnershipModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +12,7 @@ export type MVideoChangeOwnership = Omit<VideoChangeOwnershipModel, 'Initiator'
 | 
			
		|||
export type MVideoChangeOwnershipFull = MVideoChangeOwnership &
 | 
			
		||||
  Use<'Initiator', MAccountDefault> &
 | 
			
		||||
  Use<'NextOwner', MAccountDefault> &
 | 
			
		||||
  Use<'Video', MVideoWithFileThumbnail>
 | 
			
		||||
  Use<'Video', MVideoWithAllFiles>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { VideoCommentModel } from '../../../models/video/video-comment'
 | 
			
		||||
import { PickWith, PickWithOpt } from '../../utils'
 | 
			
		||||
import { MAccountDefault, MAccountFormattable, MAccountUrl, MActorUrl } from '../account'
 | 
			
		||||
import { MAccountDefault, MAccountFormattable, MAccountUrl } from '../account'
 | 
			
		||||
import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,18 +2,33 @@ import { VideoFileModel } from '../../../models/video/video-file'
 | 
			
		|||
import { PickWith, PickWithOpt } from '../../utils'
 | 
			
		||||
import { MVideo, MVideoUUID } from './video'
 | 
			
		||||
import { MVideoRedundancyFileUrl } from './video-redundancy'
 | 
			
		||||
import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos'>
 | 
			
		||||
export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos' | 'VideoStreamingPlaylist'>
 | 
			
		||||
 | 
			
		||||
export type MVideoFileVideo = MVideoFile &
 | 
			
		||||
  Use<'Video', MVideo>
 | 
			
		||||
 | 
			
		||||
export type MVideoFileStreamingPlaylist = MVideoFile &
 | 
			
		||||
  Use<'VideoStreamingPlaylist', MStreamingPlaylist>
 | 
			
		||||
 | 
			
		||||
export type MVideoFileStreamingPlaylistVideo = MVideoFile &
 | 
			
		||||
  Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
 | 
			
		||||
 | 
			
		||||
export type MVideoFileVideoUUID = MVideoFile &
 | 
			
		||||
  Use<'Video', MVideoUUID>
 | 
			
		||||
 | 
			
		||||
export type MVideoFileRedundanciesOpt = MVideoFile &
 | 
			
		||||
  PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
 | 
			
		||||
 | 
			
		||||
export function isStreamingPlaylistFile (file: any): file is MVideoFileStreamingPlaylist {
 | 
			
		||||
  return !!file.videoStreamingPlaylistId
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isWebtorrentFile (file: any): file is MVideoFileVideo {
 | 
			
		||||
  return !!file.videoId
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { VideoImportModel } from '@server/models/video/video-import'
 | 
			
		||||
import { PickWith, PickWithOpt } from '@server/typings/utils'
 | 
			
		||||
import { MUser, MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from '@server/typings/models'
 | 
			
		||||
import { MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from './video'
 | 
			
		||||
import { MUser } from '../user/user'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MVideoFormattable, MVideoPlaylistPrivacy, MVideoThumbnail, MVideoUrl } from '@server/typings/models'
 | 
			
		||||
import { MVideoFormattable, MVideoThumbnail, MVideoUrl } from './video'
 | 
			
		||||
import { MVideoPlaylistPrivacy } from './video-playlist'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoPlaylistElementModel, M> = PickWith<VideoPlaylistElementModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { AccountVideoRateModel } from '@server/models/account/account-video-rate'
 | 
			
		||||
import { PickWith } from '@server/typings/utils'
 | 
			
		||||
import { MAccountAudience, MAccountUrl, MVideo, MVideoFormattable } from '..'
 | 
			
		||||
import { MAccountAudience, MAccountUrl } from '../account/account'
 | 
			
		||||
import { MVideo, MVideoFormattable } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
 | 
			
		||||
import { PickWith, PickWithOpt } from '@server/typings/utils'
 | 
			
		||||
import { MStreamingPlaylistVideo, MVideoFile, MVideoFileVideo, MVideoUrl } from '@server/typings/models'
 | 
			
		||||
import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
 | 
			
		||||
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
 | 
			
		||||
import { VideoFile } from '../../../../shared/models/videos'
 | 
			
		||||
import { VideoFileModel } from '@server/models/video/video-file'
 | 
			
		||||
import { MVideoFile, MVideoFileVideo } from './video-file'
 | 
			
		||||
import { MStreamingPlaylistVideo } from './video-streaming-playlist'
 | 
			
		||||
import { MVideoUrl } from './video'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoRedundancyModel, M> = PickWith<VideoRedundancyModel, K, M>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,33 @@
 | 
			
		|||
import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist'
 | 
			
		||||
import { PickWith, PickWithOpt } from '../../utils'
 | 
			
		||||
import { MVideoRedundancyFileUrl } from './video-redundancy'
 | 
			
		||||
import { MVideo, MVideoUrl } from '@server/typings/models'
 | 
			
		||||
import { MVideo } from './video'
 | 
			
		||||
import { MVideoFile } from './video-file'
 | 
			
		||||
 | 
			
		||||
type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreamingPlaylistModel, K, M>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos'>
 | 
			
		||||
export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos' | 'VideoFiles'>
 | 
			
		||||
 | 
			
		||||
export type MStreamingPlaylistFiles = MStreamingPlaylist &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]>
 | 
			
		||||
 | 
			
		||||
export type MStreamingPlaylistVideo = MStreamingPlaylist &
 | 
			
		||||
  Use<'Video', MVideo>
 | 
			
		||||
 | 
			
		||||
export type MStreamingPlaylistFilesVideo = MStreamingPlaylist &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  Use<'Video', MVideo>
 | 
			
		||||
 | 
			
		||||
export type MStreamingPlaylistRedundancies = MStreamingPlaylist &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  Use<'RedundancyVideos', MVideoRedundancyFileUrl[]>
 | 
			
		||||
 | 
			
		||||
export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
 | 
			
		||||
 | 
			
		||||
export function isStreamingPlaylist (value: MVideo | MStreamingPlaylistVideo): value is MStreamingPlaylistVideo {
 | 
			
		||||
  return !!(value as MStreamingPlaylist).playlistUrl
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import {
 | 
			
		|||
} from './video-channels'
 | 
			
		||||
import { MTag } from './tag'
 | 
			
		||||
import { MVideoCaptionLanguage } from './video-caption'
 | 
			
		||||
import { MStreamingPlaylist, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist'
 | 
			
		||||
import { MStreamingPlaylistFiles, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist'
 | 
			
		||||
import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file'
 | 
			
		||||
import { MThumbnail } from './thumbnail'
 | 
			
		||||
import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +40,8 @@ export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
 | 
			
		|||
 | 
			
		||||
// "With" to not confuse with the VideoFile model
 | 
			
		||||
export type MVideoWithFile = MVideo &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]>
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
 | 
			
		||||
 | 
			
		||||
export type MVideoThumbnail = MVideo &
 | 
			
		||||
  Use<'Thumbnails', MThumbnail[]>
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +67,7 @@ export type MVideoWithCaptions = MVideo &
 | 
			
		|||
  Use<'VideoCaptions', MVideoCaptionLanguage[]>
 | 
			
		||||
 | 
			
		||||
export type MVideoWithStreamingPlaylist = MVideo &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -93,12 +94,12 @@ export type MVideoWithRights = MVideo &
 | 
			
		|||
export type MVideoWithAllFiles = MVideo &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  Use<'Thumbnails', MThumbnail[]> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
 | 
			
		||||
 | 
			
		||||
export type MVideoAccountLightBlacklistAllFiles = MVideo &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  Use<'Thumbnails', MThumbnail[]> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
 | 
			
		||||
  Use<'VideoChannel', MChannelAccountLight> &
 | 
			
		||||
  Use<'VideoBlacklist', MVideoBlacklistLight>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +125,7 @@ export type MVideoFullLight = MVideo &
 | 
			
		|||
  Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
 | 
			
		||||
  Use<'VideoFiles', MVideoFile[]> &
 | 
			
		||||
  Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
 | 
			
		||||
 | 
			
		||||
// ############################################################################
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -133,10 +134,11 @@ export type MVideoFullLight = MVideo &
 | 
			
		|||
export type MVideoAP = MVideo &
 | 
			
		||||
  Use<'Tags', MTag[]> &
 | 
			
		||||
  Use<'VideoChannel', MChannelAccountLight> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
 | 
			
		||||
  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
 | 
			
		||||
  Use<'VideoCaptions', MVideoCaptionLanguage[]> &
 | 
			
		||||
  Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
 | 
			
		||||
  Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
 | 
			
		||||
  Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
 | 
			
		||||
  Use<'Thumbnails', MThumbnail[]>
 | 
			
		||||
 | 
			
		||||
export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,6 +118,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
 | 
			
		|||
        '1080p': false,
 | 
			
		||||
        '2160p': false
 | 
			
		||||
      },
 | 
			
		||||
      webtorrent: {
 | 
			
		||||
        enabled: true
 | 
			
		||||
      },
 | 
			
		||||
      hls: {
 | 
			
		||||
        enabled: false
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -573,7 +573,6 @@ async function completeVideoCheck (
 | 
			
		|||
    // Transcoding enabled: extension will always be .mp4
 | 
			
		||||
    if (attributes.files.length > 1) extension = '.mp4'
 | 
			
		||||
 | 
			
		||||
    const magnetUri = file.magnetUri
 | 
			
		||||
    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}`)
 | 
			
		||||
| 
						 | 
				
			
			@ -594,7 +593,7 @@ async function completeVideoCheck (
 | 
			
		|||
      await testImage(url, attributes.previewfile, videoDetails.previewPath)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const torrent = await webtorrentAdd(magnetUri, true)
 | 
			
		||||
    const torrent = await webtorrentAdd(file.magnetUri, true)
 | 
			
		||||
    expect(torrent.files).to.be.an('array')
 | 
			
		||||
    expect(torrent.files.length).to.equal(1)
 | 
			
		||||
    expect(torrent.files[0].path).to.exist.and.to.not.equal('')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,12 +3,6 @@ export interface ActivityIdentifierObject {
 | 
			
		|||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ActivityTagObject {
 | 
			
		||||
  type: 'Hashtag' | 'Mention'
 | 
			
		||||
  href?: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ActivityIconObject {
 | 
			
		||||
  type: 'Image'
 | 
			
		||||
  url: string
 | 
			
		||||
| 
						 | 
				
			
			@ -19,8 +13,6 @@ export interface ActivityIconObject {
 | 
			
		|||
 | 
			
		||||
export type ActivityVideoUrlObject = {
 | 
			
		||||
  type: 'Link'
 | 
			
		||||
  // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
 | 
			
		||||
  mimeType?: 'video/mp4' | 'video/webm' | 'video/ogg'
 | 
			
		||||
  mediaType: 'video/mp4' | 'video/webm' | 'video/ogg'
 | 
			
		||||
  href: string
 | 
			
		||||
  height: number
 | 
			
		||||
| 
						 | 
				
			
			@ -31,8 +23,6 @@ export type ActivityVideoUrlObject = {
 | 
			
		|||
export type ActivityPlaylistSegmentHashesObject = {
 | 
			
		||||
  type: 'Link'
 | 
			
		||||
  name: 'sha256'
 | 
			
		||||
  // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
 | 
			
		||||
  mimeType?: 'application/json'
 | 
			
		||||
  mediaType: 'application/json'
 | 
			
		||||
  href: string
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -44,31 +34,56 @@ export type ActivityPlaylistInfohashesObject = {
 | 
			
		|||
 | 
			
		||||
export type ActivityPlaylistUrlObject = {
 | 
			
		||||
  type: 'Link'
 | 
			
		||||
  // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
 | 
			
		||||
  mimeType?: 'application/x-mpegURL'
 | 
			
		||||
  mediaType: 'application/x-mpegURL'
 | 
			
		||||
  href: string
 | 
			
		||||
  tag?: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
 | 
			
		||||
  tag?: ActivityTagObject[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ActivityBitTorrentUrlObject = {
 | 
			
		||||
  type: 'Link'
 | 
			
		||||
  // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
 | 
			
		||||
  mimeType?: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
 | 
			
		||||
  mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
 | 
			
		||||
  href: string
 | 
			
		||||
  height: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ActivityMagnetUrlObject = {
 | 
			
		||||
  type: 'Link'
 | 
			
		||||
  mediaType: 'application/x-bittorrent;x-scheme-handler/magnet'
 | 
			
		||||
  href: string
 | 
			
		||||
  height: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ActivityHtmlUrlObject = {
 | 
			
		||||
  type: 'Link'
 | 
			
		||||
  // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
 | 
			
		||||
  mimeType?: 'text/html'
 | 
			
		||||
  mediaType: 'text/html'
 | 
			
		||||
  href: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ActivityUrlObject = ActivityVideoUrlObject | ActivityPlaylistUrlObject | ActivityBitTorrentUrlObject | ActivityHtmlUrlObject
 | 
			
		||||
export interface ActivityHashTagObject {
 | 
			
		||||
  type: 'Hashtag' | 'Mention'
 | 
			
		||||
  href?: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ActivityMentionObject {
 | 
			
		||||
  type: 'Hashtag' | 'Mention'
 | 
			
		||||
  href?: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ActivityTagObject = ActivityPlaylistSegmentHashesObject |
 | 
			
		||||
  ActivityPlaylistInfohashesObject |
 | 
			
		||||
  ActivityVideoUrlObject |
 | 
			
		||||
  ActivityHashTagObject |
 | 
			
		||||
  ActivityMentionObject |
 | 
			
		||||
  ActivityBitTorrentUrlObject |
 | 
			
		||||
  ActivityMagnetUrlObject
 | 
			
		||||
 | 
			
		||||
export type ActivityUrlObject = ActivityVideoUrlObject |
 | 
			
		||||
  ActivityPlaylistUrlObject |
 | 
			
		||||
  ActivityBitTorrentUrlObject |
 | 
			
		||||
  ActivityMagnetUrlObject |
 | 
			
		||||
  ActivityHtmlUrlObject
 | 
			
		||||
 | 
			
		||||
export interface ActivityPubAttributedTo {
 | 
			
		||||
  type: 'Group' | 'Person'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,8 +69,10 @@ export interface CustomConfig {
 | 
			
		|||
 | 
			
		||||
  transcoding: {
 | 
			
		||||
    enabled: boolean
 | 
			
		||||
 | 
			
		||||
    allowAdditionalExtensions: boolean
 | 
			
		||||
    allowAudioFiles: boolean
 | 
			
		||||
 | 
			
		||||
    threads: number
 | 
			
		||||
    resolutions: {
 | 
			
		||||
      '240p': boolean
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +82,11 @@ export interface CustomConfig {
 | 
			
		|||
      '1080p': boolean
 | 
			
		||||
      '2160p': boolean
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    webtorrent: {
 | 
			
		||||
      enabled: boolean
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hls: {
 | 
			
		||||
      enabled: boolean
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,6 +56,10 @@ export interface ServerConfig {
 | 
			
		|||
      enabled: boolean
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    webtorrent: {
 | 
			
		||||
      enabled: boolean
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enabledResolutions: number[]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ export * from './playlist/video-playlist-element.model'
 | 
			
		|||
export * from './video-change-ownership.model'
 | 
			
		||||
export * from './video-change-ownership-create.model'
 | 
			
		||||
export * from './video-create.model'
 | 
			
		||||
export * from './video-file.model'
 | 
			
		||||
export * from './video-privacy.enum'
 | 
			
		||||
export * from './video-rate.type'
 | 
			
		||||
export * from './video-resolution.enum'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								shared/models/videos/video-file.model.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								shared/models/videos/video-file.model.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
import { VideoConstant, VideoResolution } from '@shared/models'
 | 
			
		||||
 | 
			
		||||
export interface VideoFile {
 | 
			
		||||
  magnetUri: string
 | 
			
		||||
  resolution: VideoConstant<VideoResolution>
 | 
			
		||||
  size: number // Bytes
 | 
			
		||||
  torrentUrl: string
 | 
			
		||||
  torrentDownloadUrl: string
 | 
			
		||||
  fileUrl: string
 | 
			
		||||
  fileDownloadUrl: string
 | 
			
		||||
  fps: number
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { VideoStreamingPlaylistType } from './video-streaming-playlist.type'
 | 
			
		||||
import { VideoFile } from '@shared/models/videos/video-file.model'
 | 
			
		||||
 | 
			
		||||
export class VideoStreamingPlaylist {
 | 
			
		||||
  id: number
 | 
			
		||||
| 
						 | 
				
			
			@ -9,4 +10,6 @@ export class VideoStreamingPlaylist {
 | 
			
		|||
  redundancies: {
 | 
			
		||||
    baseUrl: string
 | 
			
		||||
  }[]
 | 
			
		||||
 | 
			
		||||
  files: VideoFile[]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,17 +5,7 @@ import { VideoPrivacy } from './video-privacy.enum'
 | 
			
		|||
import { VideoScheduleUpdate } from './video-schedule-update.model'
 | 
			
		||||
import { VideoConstant } from './video-constant.model'
 | 
			
		||||
import { VideoStreamingPlaylist } from './video-streaming-playlist.model'
 | 
			
		||||
 | 
			
		||||
export interface VideoFile {
 | 
			
		||||
  magnetUri: string
 | 
			
		||||
  resolution: VideoConstant<VideoResolution>
 | 
			
		||||
  size: number // Bytes
 | 
			
		||||
  torrentUrl: string
 | 
			
		||||
  torrentDownloadUrl: string
 | 
			
		||||
  fileUrl: string
 | 
			
		||||
  fileDownloadUrl: string
 | 
			
		||||
  fps: number
 | 
			
		||||
}
 | 
			
		||||
import { VideoFile } from './video-file.model'
 | 
			
		||||
 | 
			
		||||
export interface Video {
 | 
			
		||||
  id: number
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,8 +16,7 @@
 | 
			
		|||
    ],
 | 
			
		||||
    "typeRoots": [
 | 
			
		||||
      "node_modules/sitemap/node_modules/@types",
 | 
			
		||||
      "node_modules/@types",
 | 
			
		||||
      "server/typings"
 | 
			
		||||
      "node_modules/@types"
 | 
			
		||||
    ],
 | 
			
		||||
    "baseUrl": "./",
 | 
			
		||||
    "paths": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7240,10 +7240,10 @@ typedarray@^0.0.6:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 | 
			
		||||
  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 | 
			
		||||
 | 
			
		||||
typescript@^3.4.3:
 | 
			
		||||
  version "3.6.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
 | 
			
		||||
  integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
 | 
			
		||||
typescript@^3.7.2:
 | 
			
		||||
  version "3.7.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
 | 
			
		||||
  integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
 | 
			
		||||
 | 
			
		||||
uint64be@^2.0.2:
 | 
			
		||||
  version "2.0.2"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue