diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-03-10 14:39:40 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-10 14:39:40 +0100 |
commit | 8319d6ae72d4da6de51bd3d4b5c68040fc8dc3b4 (patch) | |
tree | 1f87041b2cd76222844960602cdc9f52fe206c7b /server/models/video | |
parent | edb868655e52f934a71141175cf9dc6cb4753e11 (diff) | |
download | PeerTube-8319d6ae72d4da6de51bd3d4b5c68040fc8dc3b4.tar.gz PeerTube-8319d6ae72d4da6de51bd3d4b5c68040fc8dc3b4.tar.zst PeerTube-8319d6ae72d4da6de51bd3d4b5c68040fc8dc3b4.zip |
Add video file metadata to download modal, via ffprobe (#2411)
* Add video file metadata via ffprobe
* Federate video file metadata
* Add tests for file metadata generation
* Complete tests for videoFile metadata federation
* Lint migration and video-file for metadata
* Objectify metadata from getter in ffmpeg-utils
* Add metadataUrl to all videoFiles
* Simplify metadata API middleware
* Load playlist in videoFile when requesting metadata
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/video-file.ts | 96 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 13 | ||||
-rw-r--r-- | server/models/video/video.ts | 13 |
3 files changed, 108 insertions, 14 deletions
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index e08999385..029468004 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -10,7 +10,9 @@ import { | |||
10 | Is, | 10 | Is, |
11 | Model, | 11 | Model, |
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt, |
14 | Scopes, | ||
15 | DefaultScope | ||
14 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
15 | import { | 17 | import { |
16 | isVideoFileExtnameValid, | 18 | isVideoFileExtnameValid, |
@@ -29,6 +31,60 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '. | |||
29 | import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models' | 31 | import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models' |
30 | import * as memoizee from 'memoizee' | 32 | import * as memoizee from 'memoizee' |
31 | 33 | ||
34 | export enum ScopeNames { | ||
35 | WITH_VIDEO = 'WITH_VIDEO', | ||
36 | WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST', | ||
37 | WITH_METADATA = 'WITH_METADATA' | ||
38 | } | ||
39 | |||
40 | const METADATA_FIELDS = [ 'metadata', 'metadataUrl' ] | ||
41 | |||
42 | @DefaultScope(() => ({ | ||
43 | attributes: { | ||
44 | exclude: [ METADATA_FIELDS[0] ] | ||
45 | } | ||
46 | })) | ||
47 | @Scopes(() => ({ | ||
48 | [ScopeNames.WITH_VIDEO]: { | ||
49 | include: [ | ||
50 | { | ||
51 | model: VideoModel.unscoped(), | ||
52 | required: true | ||
53 | } | ||
54 | ] | ||
55 | }, | ||
56 | [ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (videoIdOrUUID: string | number) => { | ||
57 | const where = (typeof videoIdOrUUID === 'number') | ||
58 | ? { id: videoIdOrUUID } | ||
59 | : { uuid: videoIdOrUUID } | ||
60 | |||
61 | return { | ||
62 | include: [ | ||
63 | { | ||
64 | model: VideoModel.unscoped(), | ||
65 | required: false, | ||
66 | where | ||
67 | }, | ||
68 | { | ||
69 | model: VideoStreamingPlaylistModel.unscoped(), | ||
70 | required: false, | ||
71 | include: [ | ||
72 | { | ||
73 | model: VideoModel.unscoped(), | ||
74 | required: true, | ||
75 | where | ||
76 | } | ||
77 | ] | ||
78 | } | ||
79 | ] | ||
80 | } | ||
81 | }, | ||
82 | [ScopeNames.WITH_METADATA]: { | ||
83 | attributes: { | ||
84 | include: METADATA_FIELDS | ||
85 | } | ||
86 | } | ||
87 | })) | ||
32 | @Table({ | 88 | @Table({ |
33 | tableName: 'videoFile', | 89 | tableName: 'videoFile', |
34 | indexes: [ | 90 | indexes: [ |
@@ -106,6 +162,14 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
106 | @Column | 162 | @Column |
107 | fps: number | 163 | fps: number |
108 | 164 | ||
165 | @AllowNull(true) | ||
166 | @Column(DataType.JSONB) | ||
167 | metadata: any | ||
168 | |||
169 | @AllowNull(true) | ||
170 | @Column | ||
171 | metadataUrl: string | ||
172 | |||
109 | @ForeignKey(() => VideoModel) | 173 | @ForeignKey(() => VideoModel) |
110 | @Column | 174 | @Column |
111 | videoId: number | 175 | videoId: number |
@@ -157,17 +221,29 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
157 | .then(results => results.length === 1) | 221 | .then(results => results.length === 1) |
158 | } | 222 | } |
159 | 223 | ||
224 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | ||
225 | const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID) | ||
226 | return (videoFile?.Video.id === videoIdOrUUID) || | ||
227 | (videoFile?.Video.uuid === videoIdOrUUID) || | ||
228 | (videoFile?.VideoStreamingPlaylist?.Video?.id === videoIdOrUUID) || | ||
229 | (videoFile?.VideoStreamingPlaylist?.Video?.uuid === videoIdOrUUID) | ||
230 | } | ||
231 | |||
232 | static loadWithMetadata (id: number) { | ||
233 | return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) | ||
234 | } | ||
235 | |||
160 | static loadWithVideo (id: number) { | 236 | static loadWithVideo (id: number) { |
161 | const options = { | 237 | return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id) |
162 | include: [ | 238 | } |
163 | { | ||
164 | model: VideoModel.unscoped(), | ||
165 | required: true | ||
166 | } | ||
167 | ] | ||
168 | } | ||
169 | 239 | ||
170 | return VideoFileModel.findByPk(id, options) | 240 | static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) { |
241 | return VideoFileModel.scope({ | ||
242 | method: [ | ||
243 | ScopeNames.WITH_VIDEO_OR_PLAYLIST, | ||
244 | videoIdOrUUID | ||
245 | ] | ||
246 | }).findByPk(id) | ||
171 | } | 247 | } |
172 | 248 | ||
173 | static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) { | 249 | static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) { |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index 1fa66fd63..21f0e0a68 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -23,6 +23,7 @@ import { | |||
23 | import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' | 23 | import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' |
24 | import { VideoFile } from '@shared/models/videos/video-file.model' | 24 | import { VideoFile } from '@shared/models/videos/video-file.model' |
25 | import { generateMagnetUri } from '@server/helpers/webtorrent' | 25 | import { generateMagnetUri } from '@server/helpers/webtorrent' |
26 | import { extractVideo } from '@server/lib/videos' | ||
26 | 27 | ||
27 | export type VideoFormattingJSONOptions = { | 28 | export type VideoFormattingJSONOptions = { |
28 | completeDescription?: boolean | 29 | completeDescription?: boolean |
@@ -193,7 +194,8 @@ function videoFilesModelToFormattedJSON ( | |||
193 | torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp), | 194 | torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp), |
194 | torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), | 195 | torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), |
195 | fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), | 196 | fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), |
196 | fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp) | 197 | fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp), |
198 | metadataUrl: videoFile.metadataUrl // only send the metadataUrl and not the metadata over the wire | ||
197 | } as VideoFile | 199 | } as VideoFile |
198 | }) | 200 | }) |
199 | .sort((a, b) => { | 201 | .sort((a, b) => { |
@@ -222,6 +224,15 @@ function addVideoFilesInAPAcc ( | |||
222 | 224 | ||
223 | acc.push({ | 225 | acc.push({ |
224 | type: 'Link', | 226 | type: 'Link', |
227 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
228 | mediaType: 'application/json' as 'application/json', | ||
229 | href: extractVideo(model).getVideoFileMetadataUrl(file, baseUrlHttp), | ||
230 | height: file.resolution, | ||
231 | fps: file.fps | ||
232 | }) | ||
233 | |||
234 | acc.push({ | ||
235 | type: 'Link', | ||
225 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | 236 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', |
226 | href: model.getTorrentUrl(file, baseUrlHttp), | 237 | href: model.getTorrentUrl(file, baseUrlHttp), |
227 | height: file.resolution | 238 | height: file.resolution |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 7f94e834a..5e4b7d44c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -216,7 +216,7 @@ export type AvailableForListIDsOptions = { | |||
216 | 216 | ||
217 | if (options.withFiles === true) { | 217 | if (options.withFiles === true) { |
218 | query.include.push({ | 218 | query.include.push({ |
219 | model: VideoFileModel.unscoped(), | 219 | model: VideoFileModel, |
220 | required: true | 220 | required: true |
221 | }) | 221 | }) |
222 | } | 222 | } |
@@ -337,7 +337,7 @@ export type AvailableForListIDsOptions = { | |||
337 | return { | 337 | return { |
338 | include: [ | 338 | include: [ |
339 | { | 339 | { |
340 | model: VideoFileModel.unscoped(), | 340 | model: VideoFileModel, |
341 | separate: true, // We may have multiple files, having multiple redundancies so let's separate this join | 341 | separate: true, // We may have multiple files, having multiple redundancies so let's separate this join |
342 | required: false, | 342 | required: false, |
343 | include: subInclude | 343 | include: subInclude |
@@ -348,7 +348,7 @@ export type AvailableForListIDsOptions = { | |||
348 | [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => { | 348 | [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => { |
349 | const subInclude: IncludeOptions[] = [ | 349 | const subInclude: IncludeOptions[] = [ |
350 | { | 350 | { |
351 | model: VideoFileModel.unscoped(), | 351 | model: VideoFileModel, |
352 | required: false | 352 | required: false |
353 | } | 353 | } |
354 | ] | 354 | ] |
@@ -1847,6 +1847,13 @@ export class VideoModel extends Model<VideoModel> { | |||
1847 | return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile) | 1847 | return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile) |
1848 | } | 1848 | } |
1849 | 1849 | ||
1850 | getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) { | ||
1851 | const path = '/api/v1/videos/' | ||
1852 | return videoFile.metadata | ||
1853 | ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id | ||
1854 | : videoFile.metadataUrl | ||
1855 | } | ||
1856 | |||
1850 | getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) { | 1857 | getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) { |
1851 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile) | 1858 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile) |
1852 | } | 1859 | } |