aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-03-10 14:39:40 +0100
committerGitHub <noreply@github.com>2020-03-10 14:39:40 +0100
commit8319d6ae72d4da6de51bd3d4b5c68040fc8dc3b4 (patch)
tree1f87041b2cd76222844960602cdc9f52fe206c7b /server/models
parentedb868655e52f934a71141175cf9dc6cb4753e11 (diff)
downloadPeerTube-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')
-rw-r--r--server/models/redundancy/video-redundancy.ts6
-rw-r--r--server/models/utils.ts18
-rw-r--r--server/models/video/video-file.ts96
-rw-r--r--server/models/video/video-format-utils.ts13
-rw-r--r--server/models/video/video.ts13
5 files changed, 129 insertions, 17 deletions
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 1b63d3818..857b9eca6 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -528,7 +528,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
528 include: [ 528 include: [
529 { 529 {
530 required: false, 530 required: false,
531 model: VideoFileModel.unscoped(), 531 model: VideoFileModel,
532 include: [ 532 include: [
533 { 533 {
534 model: VideoRedundancyModel.unscoped(), 534 model: VideoRedundancyModel.unscoped(),
@@ -547,7 +547,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
547 where: redundancyWhere 547 where: redundancyWhere
548 }, 548 },
549 { 549 {
550 model: VideoFileModel.unscoped(), 550 model: VideoFileModel,
551 required: false 551 required: false
552 } 552 }
553 ] 553 ]
@@ -699,7 +699,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
699 699
700 return { 700 return {
701 attributes: [], 701 attributes: [],
702 model: VideoFileModel.unscoped(), 702 model: VideoFileModel,
703 required: true, 703 required: true,
704 where: { 704 where: {
705 id: { 705 id: {
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 674ddcbe4..06ff05864 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -3,6 +3,23 @@ import validator from 'validator'
3import { Col } from 'sequelize/types/lib/utils' 3import { Col } from 'sequelize/types/lib/utils'
4import { literal, OrderItem } from 'sequelize' 4import { literal, OrderItem } from 'sequelize'
5 5
6type Primitive = string | Function | number | boolean | Symbol | undefined | null
7type DeepOmitHelper<T, K extends keyof T> = {
8 [P in K]: // extra level of indirection needed to trigger homomorhic behavior
9 T[P] extends infer TP // distribute over unions
10 ? TP extends Primitive
11 ? TP // leave primitives and functions alone
12 : TP extends any[]
13 ? DeepOmitArray<TP, K> // Array special handling
14 : DeepOmit<TP, K>
15 : never
16}
17type DeepOmit<T, K> = T extends Primitive ? T : DeepOmitHelper<T, Exclude<keyof T, K>>
18
19type DeepOmitArray<T extends any[], K> = {
20 [P in keyof T]: DeepOmit<T[P], K>
21}
22
6type SortType = { sortModel: string, sortValue: string } 23type SortType = { sortModel: string, sortValue: string }
7 24
8// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] 25// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
@@ -193,6 +210,7 @@ function buildDirectionAndField (value: string) {
193// --------------------------------------------------------------------------- 210// ---------------------------------------------------------------------------
194 211
195export { 212export {
213 DeepOmit,
196 buildBlockedAccountSQL, 214 buildBlockedAccountSQL,
197 buildLocalActorIdsIn, 215 buildLocalActorIdsIn,
198 SortType, 216 SortType,
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'
15import { 17import {
16 isVideoFileExtnameValid, 18 isVideoFileExtnameValid,
@@ -29,6 +31,60 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '.
29import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models' 31import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
30import * as memoizee from 'memoizee' 32import * as memoizee from 'memoizee'
31 33
34export enum ScopeNames {
35 WITH_VIDEO = 'WITH_VIDEO',
36 WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST',
37 WITH_METADATA = 'WITH_METADATA'
38}
39
40const 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 {
23import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' 23import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
24import { VideoFile } from '@shared/models/videos/video-file.model' 24import { VideoFile } from '@shared/models/videos/video-file.model'
25import { generateMagnetUri } from '@server/helpers/webtorrent' 25import { generateMagnetUri } from '@server/helpers/webtorrent'
26import { extractVideo } from '@server/lib/videos'
26 27
27export type VideoFormattingJSONOptions = { 28export 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 }