diff options
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/formatter/video-format-utils.ts | 8 | ||||
-rw-r--r-- | server/models/video/sql/shared/video-tables.ts | 3 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 45 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 85 | ||||
-rw-r--r-- | server/models/video/video.ts | 12 |
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 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import * as memoizee from 'memoizee' | 2 | import * as memoizee from 'memoizee' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | 4 | import { FindOptions, Op, Transaction } from 'sequelize' |
5 | import { | 5 | import { |
6 | AllowNull, | 6 | AllowNull, |
7 | BelongsTo, | 7 | BelongsTo, |
@@ -21,6 +21,7 @@ import { | |||
21 | import { Where } from 'sequelize/types/lib/utils' | 21 | import { Where } from 'sequelize/types/lib/utils' |
22 | import validator from 'validator' | 22 | import validator from 'validator' |
23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | 23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' |
24 | import { doesExist } from '@server/helpers/database-utils' | ||
24 | import { logger } from '@server/helpers/logger' | 25 | import { logger } from '@server/helpers/logger' |
25 | import { extractVideo } from '@server/helpers/video' | 26 | import { extractVideo } from '@server/helpers/video' |
26 | import { getTorrentFilePath } from '@server/lib/video-paths' | 27 | import { 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 @@ | |||
1 | import * as memoizee from 'memoizee' | 1 | import * as memoizee from 'memoizee' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { Op, QueryTypes } from 'sequelize' | 3 | import { Op } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { doesExist } from '@server/helpers/database-utils' | ||
5 | import { VideoFileModel } from '@server/models/video/video-file' | 6 | import { VideoFileModel } from '@server/models/video/video-file' |
6 | import { MStreamingPlaylist } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
8 | import { AttributesOnly } from '@shared/core-utils' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 9 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
8 | import { sha1 } from '../../helpers/core-utils' | 10 | import { sha1 } from '../../helpers/core-utils' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { isArrayOf } from '../../helpers/custom-validators/misc' | 12 | import { isArrayOf } from '../../helpers/custom-validators/misc' |
11 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 13 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
12 | import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' | 14 | import { |
15 | CONSTRAINTS_FIELDS, | ||
16 | MEMOIZE_LENGTH, | ||
17 | MEMOIZE_TTL, | ||
18 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
19 | STATIC_PATHS, | ||
20 | WEBSERVER | ||
21 | } from '../../initializers/constants' | ||
13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 22 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
14 | import { throwIfNotValid } from '../utils' | 23 | import { throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 24 | import { VideoModel } from './video' |
16 | import { 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) { |