mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-02-25 18:55:32 -06:00
Use random names for VOD HLS playlists
This commit is contained in:
@@ -19,8 +19,8 @@ import {
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
|
||||
import { doesExist } from '@server/helpers/database-utils'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
import {
|
||||
MActorFollowActorsDefault,
|
||||
MActorFollowActorsDefaultSubscription,
|
||||
@@ -166,14 +166,8 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
||||
|
||||
static isFollowedBy (actorId: number, followerActorId: number) {
|
||||
const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1'
|
||||
const options = {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
bind: { actorId, followerActorId },
|
||||
raw: true
|
||||
}
|
||||
|
||||
return VideoModel.sequelize.query(query, options)
|
||||
.then(results => results.length === 1)
|
||||
return doesExist(query, { actorId, followerActorId })
|
||||
}
|
||||
|
||||
static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> {
|
||||
|
||||
@@ -160,8 +160,8 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu
|
||||
const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
|
||||
logger.info('Removing duplicated video file %s.', logIdentifier)
|
||||
|
||||
videoFile.Video.removeFile(videoFile, true)
|
||||
.catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
|
||||
videoFile.Video.removeFileAndTorrent(videoFile, true)
|
||||
.catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
|
||||
}
|
||||
|
||||
if (instance.videoStreamingPlaylistId) {
|
||||
|
||||
@@ -182,8 +182,8 @@ function streamingPlaylistsModelToFormattedJSON (
|
||||
return {
|
||||
id: playlist.id,
|
||||
type: playlist.type,
|
||||
playlistUrl: playlist.playlistUrl,
|
||||
segmentsSha256Url: playlist.segmentsSha256Url,
|
||||
playlistUrl: playlist.getMasterPlaylistUrl(video),
|
||||
segmentsSha256Url: playlist.getSha256SegmentsUrl(video),
|
||||
redundancies,
|
||||
files
|
||||
}
|
||||
@@ -331,7 +331,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||
type: 'Link',
|
||||
name: 'sha256',
|
||||
mediaType: 'application/json' as 'application/json',
|
||||
href: playlist.segmentsSha256Url
|
||||
href: playlist.getSha256SegmentsUrl(video)
|
||||
})
|
||||
|
||||
addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || [])
|
||||
@@ -339,7 +339,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
|
||||
href: playlist.playlistUrl,
|
||||
href: playlist.getMasterPlaylistUrl(video),
|
||||
tag
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,12 +92,13 @@ export class VideoTables {
|
||||
}
|
||||
|
||||
getStreamingPlaylistAttributes () {
|
||||
let playlistKeys = [ 'id', 'playlistUrl', 'type' ]
|
||||
let playlistKeys = [ 'id', 'playlistUrl', 'playlistFilename', 'type' ]
|
||||
|
||||
if (this.mode === 'get') {
|
||||
playlistKeys = playlistKeys.concat([
|
||||
'p2pMediaLoaderInfohashes',
|
||||
'p2pMediaLoaderPeerVersion',
|
||||
'segmentsSha256Filename',
|
||||
'segmentsSha256Url',
|
||||
'videoId',
|
||||
'createdAt',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { remove } from 'fs-extra'
|
||||
import * as memoizee from 'memoizee'
|
||||
import { join } from 'path'
|
||||
import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
|
||||
import { FindOptions, Op, Transaction } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { Where } from 'sequelize/types/lib/utils'
|
||||
import validator from 'validator'
|
||||
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
|
||||
import { doesExist } from '@server/helpers/database-utils'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { extractVideo } from '@server/helpers/video'
|
||||
import { getTorrentFilePath } from '@server/lib/video-paths'
|
||||
@@ -250,14 +251,8 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
|
||||
|
||||
static doesInfohashExist (infoHash: string) {
|
||||
const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
|
||||
const options = {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
bind: { infoHash },
|
||||
raw: true
|
||||
}
|
||||
|
||||
return VideoModel.sequelize.query(query, options)
|
||||
.then(results => results.length === 1)
|
||||
return doesExist(query, { infoHash })
|
||||
}
|
||||
|
||||
static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
|
||||
@@ -266,6 +261,33 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
|
||||
return !!videoFile
|
||||
}
|
||||
|
||||
static async doesOwnedTorrentFileExist (filename: string) {
|
||||
const query = 'SELECT 1 FROM "videoFile" ' +
|
||||
'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' +
|
||||
'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
|
||||
'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' +
|
||||
'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1'
|
||||
|
||||
return doesExist(query, { filename })
|
||||
}
|
||||
|
||||
static async doesOwnedWebTorrentVideoFileExist (filename: string) {
|
||||
const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' +
|
||||
'WHERE "filename" = $filename LIMIT 1'
|
||||
|
||||
return doesExist(query, { filename })
|
||||
}
|
||||
|
||||
static loadByFilename (filename: string) {
|
||||
const query = {
|
||||
where: {
|
||||
filename
|
||||
}
|
||||
}
|
||||
|
||||
return VideoFileModel.findOne(query)
|
||||
}
|
||||
|
||||
static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
|
||||
const query = {
|
||||
where: {
|
||||
@@ -443,10 +465,9 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
|
||||
}
|
||||
|
||||
getFileDownloadUrl (video: MVideoWithHost) {
|
||||
const basePath = this.isHLS()
|
||||
? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS
|
||||
: STATIC_DOWNLOAD_PATHS.VIDEOS
|
||||
const path = join(basePath, this.filename)
|
||||
const path = this.isHLS()
|
||||
? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`)
|
||||
: join(STATIC_DOWNLOAD_PATHS.VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`)
|
||||
|
||||
if (video.isOwned()) return WEBSERVER.URL + path
|
||||
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import * as memoizee from 'memoizee'
|
||||
import { join } from 'path'
|
||||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { Op } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { doesExist } from '@server/helpers/database-utils'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
import { MStreamingPlaylist } from '@server/types/models'
|
||||
import { MStreamingPlaylist, MVideo } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/core-utils'
|
||||
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
|
||||
import { sha1 } from '../../helpers/core-utils'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { isArrayOf } from '../../helpers/custom-validators/misc'
|
||||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
||||
import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
|
||||
import {
|
||||
CONSTRAINTS_FIELDS,
|
||||
MEMOIZE_LENGTH,
|
||||
MEMOIZE_TTL,
|
||||
P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
STATIC_PATHS,
|
||||
WEBSERVER
|
||||
} from '../../initializers/constants'
|
||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { AttributesOnly } from '@shared/core-utils'
|
||||
|
||||
@Table({
|
||||
tableName: 'videoStreamingPlaylist',
|
||||
@@ -43,7 +51,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
type: VideoStreamingPlaylistType
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url'))
|
||||
@Column
|
||||
playlistFilename: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true))
|
||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
|
||||
playlistUrl: string
|
||||
|
||||
@@ -57,7 +69,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
p2pMediaLoaderPeerVersion: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url'))
|
||||
@Column
|
||||
segmentsSha256Filename: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true))
|
||||
@Column
|
||||
segmentsSha256Url: string
|
||||
|
||||
@@ -98,14 +114,8 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
|
||||
static doesInfohashExist (infoHash: string) {
|
||||
const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
|
||||
const options = {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
bind: { infoHash },
|
||||
raw: true
|
||||
}
|
||||
|
||||
return VideoModel.sequelize.query<object>(query, options)
|
||||
.then(results => results.length === 1)
|
||||
return doesExist(query, { infoHash })
|
||||
}
|
||||
|
||||
static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
|
||||
@@ -125,7 +135,13 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
p2pMediaLoaderPeerVersion: {
|
||||
[Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
|
||||
}
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return VideoStreamingPlaylistModel.findAll(query)
|
||||
@@ -144,7 +160,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
return VideoStreamingPlaylistModel.findByPk(id, options)
|
||||
}
|
||||
|
||||
static loadHLSPlaylistByVideo (videoId: number) {
|
||||
static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> {
|
||||
const options = {
|
||||
where: {
|
||||
type: VideoStreamingPlaylistType.HLS,
|
||||
@@ -155,30 +171,29 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
return VideoStreamingPlaylistModel.findOne(options)
|
||||
}
|
||||
|
||||
static getHlsPlaylistFilename (resolution: number) {
|
||||
return resolution + '.m3u8'
|
||||
static async loadOrGenerate (video: MVideo) {
|
||||
let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
|
||||
if (!playlist) playlist = new VideoStreamingPlaylistModel()
|
||||
|
||||
return Object.assign(playlist, { videoId: video.id, Video: video })
|
||||
}
|
||||
|
||||
static getMasterHlsPlaylistFilename () {
|
||||
return 'master.m3u8'
|
||||
assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
|
||||
const masterPlaylistUrl = this.getMasterPlaylistUrl(video)
|
||||
|
||||
this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files)
|
||||
}
|
||||
|
||||
static getHlsSha256SegmentsFilename () {
|
||||
return 'segments-sha256.json'
|
||||
getMasterPlaylistUrl (video: MVideo) {
|
||||
if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid)
|
||||
|
||||
return this.playlistUrl
|
||||
}
|
||||
|
||||
static getHlsMasterPlaylistStaticPath (videoUUID: string) {
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
|
||||
}
|
||||
getSha256SegmentsUrl (video: MVideo) {
|
||||
if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive)
|
||||
|
||||
static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) {
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
|
||||
}
|
||||
|
||||
static getHlsSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
|
||||
if (isLive) return join('/live', 'segments-sha256', videoUUID)
|
||||
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
|
||||
return this.segmentsSha256Url
|
||||
}
|
||||
|
||||
getStringType () {
|
||||
@@ -195,4 +210,14 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
|
||||
return this.type === other.type &&
|
||||
this.videoId === other.videoId
|
||||
}
|
||||
|
||||
private getMasterPlaylistStaticPath (videoUUID: string) {
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename)
|
||||
}
|
||||
|
||||
private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
|
||||
if (isLive) return join('/live', 'segments-sha256', videoUUID)
|
||||
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,8 +762,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||
|
||||
// Remove physical files and torrents
|
||||
instance.VideoFiles.forEach(file => {
|
||||
tasks.push(instance.removeFile(file))
|
||||
tasks.push(file.removeTorrent())
|
||||
tasks.push(instance.removeFileAndTorrent(file))
|
||||
})
|
||||
|
||||
// Remove playlists file
|
||||
@@ -1670,10 +1669,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||
.concat(toAdd)
|
||||
}
|
||||
|
||||
removeFile (videoFile: MVideoFile, isRedundancy = false) {
|
||||
removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
|
||||
const filePath = getVideoFilePath(this, videoFile, isRedundancy)
|
||||
return remove(filePath)
|
||||
.catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
|
||||
|
||||
const promises: Promise<any>[] = [ remove(filePath) ]
|
||||
if (!isRedundancy) promises.push(videoFile.removeTorrent())
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
|
||||
|
||||
Reference in New Issue
Block a user