diff options
Diffstat (limited to 'server/models/video/video-file.ts')
-rw-r--r-- | server/models/video/video-file.ts | 181 |
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 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import * as memoizee from 'memoizee' | ||
3 | import { join } from 'path' | ||
4 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | ||
1 | import { | 5 | import { |
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' |
21 | import { Where } from 'sequelize/types/lib/utils' | ||
22 | import validator from 'validator' | ||
23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | ||
24 | import { logger } from '@server/helpers/logger' | ||
25 | import { extractVideo } from '@server/helpers/video' | ||
26 | import { getTorrentFilePath } from '@server/lib/video-paths' | ||
27 | import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' | ||
17 | import { | 28 | import { |
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' |
35 | import { | ||
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' | ||
44 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | ||
45 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
24 | import { parseAggregateResult, throwIfNotValid } from '../utils' | 46 | import { parseAggregateResult, throwIfNotValid } from '../utils' |
25 | import { VideoModel } from './video' | 47 | import { VideoModel } from './video' |
26 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
27 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 48 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
28 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | ||
29 | import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants' | ||
30 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | ||
31 | import { MStreamingPlaylistVideo, MVideo } from '@server/types/models' | ||
32 | import * as memoizee from 'memoizee' | ||
33 | import validator from 'validator' | ||
34 | 49 | ||
35 | export enum ScopeNames { | 50 | export 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 && |