aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video-file.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-02-16 16:25:53 +0100
committerChocobozzz <chocobozzz@cpy.re>2021-02-18 13:38:09 +0100
commit90a8bd305de4153ec21137a73ff482dcc2e3e19b (patch)
tree2e35b5504ec11bc51579c92a70c77ed3d5ace816 /server/models/video/video-file.ts
parent684cdacbbd775b5f404dd7b373e02dd21baf5ff0 (diff)
downloadPeerTube-90a8bd305de4153ec21137a73ff482dcc2e3e19b.tar.gz
PeerTube-90a8bd305de4153ec21137a73ff482dcc2e3e19b.tar.zst
PeerTube-90a8bd305de4153ec21137a73ff482dcc2e3e19b.zip
Dissociate video file names and video uuid
Diffstat (limited to 'server/models/video/video-file.ts')
-rw-r--r--server/models/video/video-file.ts181
1 files changed, 150 insertions, 31 deletions
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 48b337c68..57807cbfd 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -1,3 +1,7 @@
1import { remove } from 'fs-extra'
2import * as memoizee from 'memoizee'
3import { join } from 'path'
4import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
1import { 5import {
2 AllowNull, 6 AllowNull,
3 BelongsTo, 7 BelongsTo,
@@ -5,15 +9,22 @@ import {
5 CreatedAt, 9 CreatedAt,
6 DataType, 10 DataType,
7 Default, 11 Default,
12 DefaultScope,
8 ForeignKey, 13 ForeignKey,
9 HasMany, 14 HasMany,
10 Is, 15 Is,
11 Model, 16 Model,
12 Table,
13 UpdatedAt,
14 Scopes, 17 Scopes,
15 DefaultScope 18 Table,
19 UpdatedAt
16} from 'sequelize-typescript' 20} from 'sequelize-typescript'
21import { Where } from 'sequelize/types/lib/utils'
22import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { logger } from '@server/helpers/logger'
25import { extractVideo } from '@server/helpers/video'
26import { getTorrentFilePath } from '@server/lib/video-paths'
27import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
17import { 28import {
18 isVideoFileExtnameValid, 29 isVideoFileExtnameValid,
19 isVideoFileInfoHashValid, 30 isVideoFileInfoHashValid,
@@ -21,20 +32,25 @@ import {
21 isVideoFileSizeValid, 32 isVideoFileSizeValid,
22 isVideoFPSResolutionValid 33 isVideoFPSResolutionValid
23} from '../../helpers/custom-validators/videos' 34} from '../../helpers/custom-validators/videos'
35import {
36 LAZY_STATIC_PATHS,
37 MEMOIZE_LENGTH,
38 MEMOIZE_TTL,
39 MIMETYPES,
40 STATIC_DOWNLOAD_PATHS,
41 STATIC_PATHS,
42 WEBSERVER
43} from '../../initializers/constants'
44import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
45import { VideoRedundancyModel } from '../redundancy/video-redundancy'
24import { parseAggregateResult, throwIfNotValid } from '../utils' 46import { parseAggregateResult, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 47import { VideoModel } from './video'
26import { VideoRedundancyModel } from '../redundancy/video-redundancy'
27import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 48import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
28import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
29import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
30import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
31import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
32import * as memoizee from 'memoizee'
33import validator from 'validator'
34 49
35export enum ScopeNames { 50export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO', 51 WITH_VIDEO = 'WITH_VIDEO',
37 WITH_METADATA = 'WITH_METADATA' 52 WITH_METADATA = 'WITH_METADATA',
53 WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST'
38} 54}
39 55
40@DefaultScope(() => ({ 56@DefaultScope(() => ({
@@ -51,6 +67,28 @@ export enum ScopeNames {
51 } 67 }
52 ] 68 ]
53 }, 69 },
70 [ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (options: { whereVideo?: Where } = {}) => {
71 return {
72 include: [
73 {
74 model: VideoModel.unscoped(),
75 required: false,
76 where: options.whereVideo
77 },
78 {
79 model: VideoStreamingPlaylistModel.unscoped(),
80 required: false,
81 include: [
82 {
83 model: VideoModel.unscoped(),
84 required: true,
85 where: options.whereVideo
86 }
87 ]
88 }
89 ]
90 }
91 },
54 [ScopeNames.WITH_METADATA]: { 92 [ScopeNames.WITH_METADATA]: {
55 attributes: { 93 attributes: {
56 include: [ 'metadata' ] 94 include: [ 'metadata' ]
@@ -82,6 +120,16 @@ export enum ScopeNames {
82 }, 120 },
83 121
84 { 122 {
123 fields: [ 'torrentFilename' ],
124 unique: true
125 },
126
127 {
128 fields: [ 'filename' ],
129 unique: true
130 },
131
132 {
85 fields: [ 'videoId', 'resolution', 'fps' ], 133 fields: [ 'videoId', 'resolution', 'fps' ],
86 unique: true, 134 unique: true,
87 where: { 135 where: {
@@ -142,6 +190,24 @@ export class VideoFileModel extends Model {
142 @Column 190 @Column
143 metadataUrl: string 191 metadataUrl: string
144 192
193 @AllowNull(true)
194 @Column
195 fileUrl: string
196
197 // Could be null for live files
198 @AllowNull(true)
199 @Column
200 filename: string
201
202 @AllowNull(true)
203 @Column
204 torrentUrl: string
205
206 // Could be null for live files
207 @AllowNull(true)
208 @Column
209 torrentFilename: string
210
145 @ForeignKey(() => VideoModel) 211 @ForeignKey(() => VideoModel)
146 @Column 212 @Column
147 videoId: number 213 videoId: number
@@ -199,6 +265,16 @@ export class VideoFileModel extends Model {
199 return !!videoFile 265 return !!videoFile
200 } 266 }
201 267
268 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
269 const query = {
270 where: {
271 torrentFilename: filename
272 }
273 }
274
275 return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
276 }
277
202 static loadWithMetadata (id: number) { 278 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) 279 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
204 } 280 }
@@ -215,28 +291,11 @@ export class VideoFileModel extends Model {
215 const options = { 291 const options = {
216 where: { 292 where: {
217 id 293 id
218 }, 294 }
219 include: [
220 {
221 model: VideoModel.unscoped(),
222 required: false,
223 where: whereVideo
224 },
225 {
226 model: VideoStreamingPlaylistModel.unscoped(),
227 required: false,
228 include: [
229 {
230 model: VideoModel.unscoped(),
231 required: true,
232 where: whereVideo
233 }
234 ]
235 }
236 ]
237 } 295 }
238 296
239 return VideoFileModel.findOne(options) 297 return VideoFileModel.scope({ method: [ ScopeNames.WITH_VIDEO_OR_PLAYLIST, whereVideo ] })
298 .findOne(options)
240 .then(file => { 299 .then(file => {
241 // We used `required: false` so check we have at least a video or a streaming playlist 300 // We used `required: false` so check we have at least a video or a streaming playlist
242 if (!file.Video && !file.VideoStreamingPlaylist) return null 301 if (!file.Video && !file.VideoStreamingPlaylist) return null
@@ -348,6 +407,10 @@ export class VideoFileModel extends Model {
348 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist 407 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
349 } 408 }
350 409
410 getVideo (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo {
411 return extractVideo(this.getVideoOrStreamingPlaylist())
412 }
413
351 isAudio () { 414 isAudio () {
352 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] 415 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
353 } 416 }
@@ -360,6 +423,62 @@ export class VideoFileModel extends Model {
360 return !!this.videoStreamingPlaylistId 423 return !!this.videoStreamingPlaylistId
361 } 424 }
362 425
426 getFileUrl (video: MVideoWithHost) {
427 if (!this.Video) this.Video = video as VideoModel
428
429 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
430 if (this.fileUrl) return this.fileUrl
431
432 // Fallback if we don't have a file URL
433 return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
434 }
435
436 getFileStaticPath (video: MVideo) {
437 if (this.isHLS()) return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename)
438
439 return join(STATIC_PATHS.WEBSEED, this.filename)
440 }
441
442 getFileDownloadUrl (video: MVideoWithHost) {
443 const basePath = this.isHLS()
444 ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS
445 : STATIC_DOWNLOAD_PATHS.VIDEOS
446 const path = join(basePath, this.filename)
447
448 if (video.isOwned()) return WEBSERVER.URL + path
449
450 // FIXME: don't guess remote URL
451 return buildRemoteVideoBaseUrl(video, path)
452 }
453
454 getRemoteTorrentUrl (video: MVideoWithHost) {
455 if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
456
457 if (this.torrentUrl) return this.torrentUrl
458
459 // Fallback if we don't have a torrent URL
460 return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
461 }
462
463 // We proxify torrent requests so use a local URL
464 getTorrentUrl () {
465 return WEBSERVER.URL + this.getTorrentStaticPath()
466 }
467
468 getTorrentStaticPath () {
469 return join(LAZY_STATIC_PATHS.TORRENTS, this.torrentFilename)
470 }
471
472 getTorrentDownloadUrl () {
473 return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.TORRENTS, this.torrentFilename)
474 }
475
476 removeTorrent () {
477 const torrentPath = getTorrentFilePath(this)
478 return remove(torrentPath)
479 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
480 }
481
363 hasSameUniqueKeysThan (other: MVideoFile) { 482 hasSameUniqueKeysThan (other: MVideoFile) {
364 return this.fps === other.fps && 483 return this.fps === other.fps &&
365 this.resolution === other.resolution && 484 this.resolution === other.resolution &&