aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-23 11:20:00 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-07-26 11:29:31 +0200
commit764b1a14fc494f2cfd7ea590d2f07b01df65c7ad (patch)
tree198ca5f242c63a205a05fa4cfd6d063277c541fd /server/models/video
parent83903cb65d531a6b6b91715387493ba8312b264d (diff)
downloadPeerTube-764b1a14fc494f2cfd7ea590d2f07b01df65c7ad.tar.gz
PeerTube-764b1a14fc494f2cfd7ea590d2f07b01df65c7ad.tar.zst
PeerTube-764b1a14fc494f2cfd7ea590d2f07b01df65c7ad.zip
Use random names for VOD HLS playlists
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/formatter/video-format-utils.ts8
-rw-r--r--server/models/video/sql/shared/video-tables.ts3
-rw-r--r--server/models/video/video-file.ts45
-rw-r--r--server/models/video/video-streaming-playlist.ts85
-rw-r--r--server/models/video/video.ts12
5 files changed, 101 insertions, 52 deletions
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index 6b1e59063..3310b3b46 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -182,8 +182,8 @@ function streamingPlaylistsModelToFormattedJSON (
182 return { 182 return {
183 id: playlist.id, 183 id: playlist.id,
184 type: playlist.type, 184 type: playlist.type,
185 playlistUrl: playlist.playlistUrl, 185 playlistUrl: playlist.getMasterPlaylistUrl(video),
186 segmentsSha256Url: playlist.segmentsSha256Url, 186 segmentsSha256Url: playlist.getSha256SegmentsUrl(video),
187 redundancies, 187 redundancies,
188 files 188 files
189 } 189 }
@@ -331,7 +331,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
331 type: 'Link', 331 type: 'Link',
332 name: 'sha256', 332 name: 'sha256',
333 mediaType: 'application/json' as 'application/json', 333 mediaType: 'application/json' as 'application/json',
334 href: playlist.segmentsSha256Url 334 href: playlist.getSha256SegmentsUrl(video)
335 }) 335 })
336 336
337 addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) 337 addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || [])
@@ -339,7 +339,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
339 url.push({ 339 url.push({
340 type: 'Link', 340 type: 'Link',
341 mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', 341 mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
342 href: playlist.playlistUrl, 342 href: playlist.getMasterPlaylistUrl(video),
343 tag 343 tag
344 }) 344 })
345 } 345 }
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts
index abdd22188..742d19099 100644
--- a/server/models/video/sql/shared/video-tables.ts
+++ b/server/models/video/sql/shared/video-tables.ts
@@ -92,12 +92,13 @@ export class VideoTables {
92 } 92 }
93 93
94 getStreamingPlaylistAttributes () { 94 getStreamingPlaylistAttributes () {
95 let playlistKeys = [ 'id', 'playlistUrl', 'type' ] 95 let playlistKeys = [ 'id', 'playlistUrl', 'playlistFilename', 'type' ]
96 96
97 if (this.mode === 'get') { 97 if (this.mode === 'get') {
98 playlistKeys = playlistKeys.concat([ 98 playlistKeys = playlistKeys.concat([
99 'p2pMediaLoaderInfohashes', 99 'p2pMediaLoaderInfohashes',
100 'p2pMediaLoaderPeerVersion', 100 'p2pMediaLoaderPeerVersion',
101 'segmentsSha256Filename',
101 'segmentsSha256Url', 102 'segmentsSha256Url',
102 'videoId', 103 'videoId',
103 'createdAt', 104 'createdAt',
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 22cf63804..797a85a4e 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -1,7 +1,7 @@
1import { remove } from 'fs-extra' 1import { remove } from 'fs-extra'
2import * as memoizee from 'memoizee' 2import * as memoizee from 'memoizee'
3import { join } from 'path' 3import { join } from 'path'
4import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' 4import { FindOptions, Op, Transaction } from 'sequelize'
5import { 5import {
6 AllowNull, 6 AllowNull,
7 BelongsTo, 7 BelongsTo,
@@ -21,6 +21,7 @@ import {
21import { Where } from 'sequelize/types/lib/utils' 21import { Where } from 'sequelize/types/lib/utils'
22import validator from 'validator' 22import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { doesExist } from '@server/helpers/database-utils'
24import { logger } from '@server/helpers/logger' 25import { logger } from '@server/helpers/logger'
25import { extractVideo } from '@server/helpers/video' 26import { extractVideo } from '@server/helpers/video'
26import { getTorrentFilePath } from '@server/lib/video-paths' 27import { getTorrentFilePath } from '@server/lib/video-paths'
@@ -250,14 +251,8 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
250 251
251 static doesInfohashExist (infoHash: string) { 252 static doesInfohashExist (infoHash: string) {
252 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' 253 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
253 const options = {
254 type: QueryTypes.SELECT as QueryTypes.SELECT,
255 bind: { infoHash },
256 raw: true
257 }
258 254
259 return VideoModel.sequelize.query(query, options) 255 return doesExist(query, { infoHash })
260 .then(results => results.length === 1)
261 } 256 }
262 257
263 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { 258 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
@@ -266,6 +261,33 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
266 return !!videoFile 261 return !!videoFile
267 } 262 }
268 263
264 static async doesOwnedTorrentFileExist (filename: string) {
265 const query = 'SELECT 1 FROM "videoFile" ' +
266 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' +
267 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
268 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' +
269 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1'
270
271 return doesExist(query, { filename })
272 }
273
274 static async doesOwnedWebTorrentVideoFileExist (filename: string) {
275 const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' +
276 'WHERE "filename" = $filename LIMIT 1'
277
278 return doesExist(query, { filename })
279 }
280
281 static loadByFilename (filename: string) {
282 const query = {
283 where: {
284 filename
285 }
286 }
287
288 return VideoFileModel.findOne(query)
289 }
290
269 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { 291 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
270 const query = { 292 const query = {
271 where: { 293 where: {
@@ -443,10 +465,9 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
443 } 465 }
444 466
445 getFileDownloadUrl (video: MVideoWithHost) { 467 getFileDownloadUrl (video: MVideoWithHost) {
446 const basePath = this.isHLS() 468 const path = this.isHLS()
447 ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS 469 ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`)
448 : STATIC_DOWNLOAD_PATHS.VIDEOS 470 : join(STATIC_DOWNLOAD_PATHS.VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`)
449 const path = join(basePath, this.filename)
450 471
451 if (video.isOwned()) return WEBSERVER.URL + path 472 if (video.isOwned()) return WEBSERVER.URL + path
452 473
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index d627e8c9d..b15d20cf9 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -1,19 +1,27 @@
1import * as memoizee from 'memoizee' 1import * as memoizee from 'memoizee'
2import { join } from 'path' 2import { join } from 'path'
3import { Op, QueryTypes } from 'sequelize' 3import { Op } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
5import { doesExist } from '@server/helpers/database-utils'
5import { VideoFileModel } from '@server/models/video/video-file' 6import { VideoFileModel } from '@server/models/video/video-file'
6import { MStreamingPlaylist } from '@server/types/models' 7import { MStreamingPlaylist, MVideo } from '@server/types/models'
8import { AttributesOnly } from '@shared/core-utils'
7import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 9import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
8import { sha1 } from '../../helpers/core-utils' 10import { sha1 } from '../../helpers/core-utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { isArrayOf } from '../../helpers/custom-validators/misc' 12import { isArrayOf } from '../../helpers/custom-validators/misc'
11import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 13import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
12import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' 14import {
15 CONSTRAINTS_FIELDS,
16 MEMOIZE_LENGTH,
17 MEMOIZE_TTL,
18 P2P_MEDIA_LOADER_PEER_VERSION,
19 STATIC_PATHS,
20 WEBSERVER
21} from '../../initializers/constants'
13import { VideoRedundancyModel } from '../redundancy/video-redundancy' 22import { VideoRedundancyModel } from '../redundancy/video-redundancy'
14import { throwIfNotValid } from '../utils' 23import { throwIfNotValid } from '../utils'
15import { VideoModel } from './video' 24import { VideoModel } from './video'
16import { AttributesOnly } from '@shared/core-utils'
17 25
18@Table({ 26@Table({
19 tableName: 'videoStreamingPlaylist', 27 tableName: 'videoStreamingPlaylist',
@@ -43,7 +51,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
43 type: VideoStreamingPlaylistType 51 type: VideoStreamingPlaylistType
44 52
45 @AllowNull(false) 53 @AllowNull(false)
46 @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) 54 @Column
55 playlistFilename: string
56
57 @AllowNull(true)
58 @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true))
47 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) 59 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
48 playlistUrl: string 60 playlistUrl: string
49 61
@@ -57,7 +69,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
57 p2pMediaLoaderPeerVersion: number 69 p2pMediaLoaderPeerVersion: number
58 70
59 @AllowNull(false) 71 @AllowNull(false)
60 @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) 72 @Column
73 segmentsSha256Filename: string
74
75 @AllowNull(true)
76 @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true))
61 @Column 77 @Column
62 segmentsSha256Url: string 78 segmentsSha256Url: string
63 79
@@ -98,14 +114,8 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
98 114
99 static doesInfohashExist (infoHash: string) { 115 static doesInfohashExist (infoHash: string) {
100 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' 116 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
101 const options = {
102 type: QueryTypes.SELECT as QueryTypes.SELECT,
103 bind: { infoHash },
104 raw: true
105 }
106 117
107 return VideoModel.sequelize.query<object>(query, options) 118 return doesExist(query, { infoHash })
108 .then(results => results.length === 1)
109 } 119 }
110 120
111 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { 121 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
@@ -125,7 +135,13 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
125 p2pMediaLoaderPeerVersion: { 135 p2pMediaLoaderPeerVersion: {
126 [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION 136 [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
127 } 137 }
128 } 138 },
139 include: [
140 {
141 model: VideoModel.unscoped(),
142 required: true
143 }
144 ]
129 } 145 }
130 146
131 return VideoStreamingPlaylistModel.findAll(query) 147 return VideoStreamingPlaylistModel.findAll(query)
@@ -144,7 +160,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
144 return VideoStreamingPlaylistModel.findByPk(id, options) 160 return VideoStreamingPlaylistModel.findByPk(id, options)
145 } 161 }
146 162
147 static loadHLSPlaylistByVideo (videoId: number) { 163 static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> {
148 const options = { 164 const options = {
149 where: { 165 where: {
150 type: VideoStreamingPlaylistType.HLS, 166 type: VideoStreamingPlaylistType.HLS,
@@ -155,30 +171,29 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
155 return VideoStreamingPlaylistModel.findOne(options) 171 return VideoStreamingPlaylistModel.findOne(options)
156 } 172 }
157 173
158 static getHlsPlaylistFilename (resolution: number) { 174 static async loadOrGenerate (video: MVideo) {
159 return resolution + '.m3u8' 175 let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
160 } 176 if (!playlist) playlist = new VideoStreamingPlaylistModel()
161 177
162 static getMasterHlsPlaylistFilename () { 178 return Object.assign(playlist, { videoId: video.id, Video: video })
163 return 'master.m3u8'
164 } 179 }
165 180
166 static getHlsSha256SegmentsFilename () { 181 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
167 return 'segments-sha256.json' 182 const masterPlaylistUrl = this.getMasterPlaylistUrl(video)
168 }
169 183
170 static getHlsMasterPlaylistStaticPath (videoUUID: string) { 184 this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files)
171 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
172 } 185 }
173 186
174 static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { 187 getMasterPlaylistUrl (video: MVideo) {
175 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) 188 if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid)
189
190 return this.playlistUrl
176 } 191 }
177 192
178 static getHlsSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { 193 getSha256SegmentsUrl (video: MVideo) {
179 if (isLive) return join('/live', 'segments-sha256', videoUUID) 194 if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive)
180 195
181 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) 196 return this.segmentsSha256Url
182 } 197 }
183 198
184 getStringType () { 199 getStringType () {
@@ -195,4 +210,14 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
195 return this.type === other.type && 210 return this.type === other.type &&
196 this.videoId === other.videoId 211 this.videoId === other.videoId
197 } 212 }
213
214 private getMasterPlaylistStaticPath (videoUUID: string) {
215 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename)
216 }
217
218 private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
219 if (isLive) return join('/live', 'segments-sha256', videoUUID)
220
221 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename)
222 }
198} 223}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 1e5648a36..0f0f894e4 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -762,8 +762,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
762 762
763 // Remove physical files and torrents 763 // Remove physical files and torrents
764 instance.VideoFiles.forEach(file => { 764 instance.VideoFiles.forEach(file => {
765 tasks.push(instance.removeFile(file)) 765 tasks.push(instance.removeFileAndTorrent(file))
766 tasks.push(file.removeTorrent())
767 }) 766 })
768 767
769 // Remove playlists file 768 // Remove playlists file
@@ -1670,10 +1669,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1670 .concat(toAdd) 1669 .concat(toAdd)
1671 } 1670 }
1672 1671
1673 removeFile (videoFile: MVideoFile, isRedundancy = false) { 1672 removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
1674 const filePath = getVideoFilePath(this, videoFile, isRedundancy) 1673 const filePath = getVideoFilePath(this, videoFile, isRedundancy)
1675 return remove(filePath) 1674
1676 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1675 const promises: Promise<any>[] = [ remove(filePath) ]
1676 if (!isRedundancy) promises.push(videoFile.removeTorrent())
1677
1678 return Promise.all(promises)
1677 } 1679 }
1678 1680
1679 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { 1681 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {