aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-06-01 14:51:16 +0200
committerChocobozzz <me@florianbigard.com>2023-06-29 10:16:55 +0200
commitd8f39b126d9fe4bec1c12fb213548cc6edc87867 (patch)
tree7f0f1cb23165cf4dd789b2d78b1fef7ee116f647 /server/models
parent1fb7d094229acdc190c3f7551b43ac5445814dee (diff)
downloadPeerTube-d8f39b126d9fe4bec1c12fb213548cc6edc87867.tar.gz
PeerTube-d8f39b126d9fe4bec1c12fb213548cc6edc87867.tar.zst
PeerTube-d8f39b126d9fe4bec1c12fb213548cc6edc87867.zip
Add storyboard support
Diffstat (limited to 'server/models')
-rw-r--r--server/models/video/formatter/video-format-utils.ts60
-rw-r--r--server/models/video/storyboard.ts169
-rw-r--r--server/models/video/video-caption.ts8
-rw-r--r--server/models/video/video.ts45
4 files changed, 255 insertions, 27 deletions
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index f2001e432..4179545b8 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -5,6 +5,7 @@ import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
5import { VideoViewsManager } from '@server/lib/views/video-views-manager' 5import { VideoViewsManager } from '@server/lib/views/video-views-manager'
6import { uuidToShort } from '@shared/extra-utils' 6import { uuidToShort } from '@shared/extra-utils'
7import { 7import {
8 ActivityPubStoryboard,
8 ActivityTagObject, 9 ActivityTagObject,
9 ActivityUrlObject, 10 ActivityUrlObject,
10 Video, 11 Video,
@@ -347,29 +348,17 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
347 name: t.name 348 name: t.name
348 })) 349 }))
349 350
350 let language 351 const language = video.language
351 if (video.language) { 352 ? { identifier: video.language, name: getLanguageLabel(video.language) }
352 language = { 353 : undefined
353 identifier: video.language,
354 name: getLanguageLabel(video.language)
355 }
356 }
357 354
358 let category 355 const category = video.category
359 if (video.category) { 356 ? { identifier: video.category + '', name: getCategoryLabel(video.category) }
360 category = { 357 : undefined
361 identifier: video.category + '',
362 name: getCategoryLabel(video.category)
363 }
364 }
365 358
366 let licence 359 const licence = video.licence
367 if (video.licence) { 360 ? { identifier: video.licence + '', name: getLicenceLabel(video.licence) }
368 licence = { 361 : undefined
369 identifier: video.licence + '',
370 name: getLicenceLabel(video.licence)
371 }
372 }
373 362
374 const url: ActivityUrlObject[] = [ 363 const url: ActivityUrlObject[] = [
375 // HTML url should be the first element in the array so Mastodon correctly displays the embed 364 // HTML url should be the first element in the array so Mastodon correctly displays the embed
@@ -465,6 +454,8 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
465 height: i.height 454 height: i.height
466 })), 455 })),
467 456
457 preview: buildPreviewAPAttribute(video),
458
468 url, 459 url,
469 460
470 likes: getLocalVideoLikesActivityPubUrl(video), 461 likes: getLocalVideoLikesActivityPubUrl(video),
@@ -541,3 +532,30 @@ function buildLiveAPAttributes (video: MVideoAP) {
541 latencyMode: video.VideoLive.latencyMode 532 latencyMode: video.VideoLive.latencyMode
542 } 533 }
543} 534}
535
536function buildPreviewAPAttribute (video: MVideoAP): ActivityPubStoryboard[] {
537 if (!video.Storyboard) return undefined
538
539 const storyboard = video.Storyboard
540
541 return [
542 {
543 type: 'Image',
544 rel: [ 'storyboard' ],
545 url: [
546 {
547 mediaType: 'image/jpeg',
548
549 href: storyboard.getOriginFileUrl(video),
550
551 width: storyboard.totalWidth,
552 height: storyboard.totalHeight,
553
554 tileWidth: storyboard.spriteWidth,
555 tileHeight: storyboard.spriteHeight,
556 tileDuration: getActivityStreamDuration(storyboard.spriteDuration)
557 }
558 ]
559 }
560 ]
561}
diff --git a/server/models/video/storyboard.ts b/server/models/video/storyboard.ts
new file mode 100644
index 000000000..65a044c98
--- /dev/null
+++ b/server/models/video/storyboard.ts
@@ -0,0 +1,169 @@
1import { remove } from 'fs-extra'
2import { join } from 'path'
3import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { CONFIG } from '@server/initializers/config'
5import { MStoryboard, MStoryboardVideo, MVideo } from '@server/types/models'
6import { Storyboard } from '@shared/models'
7import { AttributesOnly } from '@shared/typescript-utils'
8import { logger } from '../../helpers/logger'
9import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
10import { VideoModel } from './video'
11import { Transaction } from 'sequelize'
12
13@Table({
14 tableName: 'storyboard',
15 indexes: [
16 {
17 fields: [ 'videoId' ],
18 unique: true
19 },
20 {
21 fields: [ 'filename' ],
22 unique: true
23 }
24 ]
25})
26export class StoryboardModel extends Model<Partial<AttributesOnly<StoryboardModel>>> {
27
28 @AllowNull(false)
29 @Column
30 filename: string
31
32 @AllowNull(false)
33 @Column
34 totalHeight: number
35
36 @AllowNull(false)
37 @Column
38 totalWidth: number
39
40 @AllowNull(false)
41 @Column
42 spriteHeight: number
43
44 @AllowNull(false)
45 @Column
46 spriteWidth: number
47
48 @AllowNull(false)
49 @Column
50 spriteDuration: number
51
52 @AllowNull(true)
53 @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
54 fileUrl: string
55
56 @ForeignKey(() => VideoModel)
57 @Column
58 videoId: number
59
60 @BelongsTo(() => VideoModel, {
61 foreignKey: {
62 allowNull: true
63 },
64 onDelete: 'CASCADE'
65 })
66 Video: VideoModel
67
68 @CreatedAt
69 createdAt: Date
70
71 @UpdatedAt
72 updatedAt: Date
73
74 @AfterDestroy
75 static removeInstanceFile (instance: StoryboardModel) {
76 logger.info('Removing storyboard file %s.', instance.filename)
77
78 // Don't block the transaction
79 instance.removeFile()
80 .catch(err => logger.error('Cannot remove storyboard file %s.', instance.filename, { err }))
81 }
82
83 static loadByVideo (videoId: number, transaction?: Transaction): Promise<MStoryboard> {
84 const query = {
85 where: {
86 videoId
87 },
88 transaction
89 }
90
91 return StoryboardModel.findOne(query)
92 }
93
94 static loadByFilename (filename: string): Promise<MStoryboard> {
95 const query = {
96 where: {
97 filename
98 }
99 }
100
101 return StoryboardModel.findOne(query)
102 }
103
104 static loadWithVideoByFilename (filename: string): Promise<MStoryboardVideo> {
105 const query = {
106 where: {
107 filename
108 },
109 include: [
110 {
111 model: VideoModel.unscoped(),
112 required: true
113 }
114 ]
115 }
116
117 return StoryboardModel.findOne(query)
118 }
119
120 // ---------------------------------------------------------------------------
121
122 static async listStoryboardsOf (video: MVideo): Promise<MStoryboardVideo[]> {
123 const query = {
124 where: {
125 videoId: video.id
126 }
127 }
128
129 const storyboards = await StoryboardModel.findAll<MStoryboard>(query)
130
131 return storyboards.map(s => Object.assign(s, { Video: video }))
132 }
133
134 // ---------------------------------------------------------------------------
135
136 getOriginFileUrl (video: MVideo) {
137 if (video.isOwned()) {
138 return WEBSERVER.URL + this.getLocalStaticPath()
139 }
140
141 return this.fileUrl
142 }
143
144 getLocalStaticPath () {
145 return LAZY_STATIC_PATHS.STORYBOARDS + this.filename
146 }
147
148 getPath () {
149 return join(CONFIG.STORAGE.STORYBOARDS_DIR, this.filename)
150 }
151
152 removeFile () {
153 return remove(this.getPath())
154 }
155
156 toFormattedJSON (this: MStoryboardVideo): Storyboard {
157 return {
158 storyboardPath: this.getLocalStaticPath(),
159
160 totalHeight: this.totalHeight,
161 totalWidth: this.totalWidth,
162
163 spriteWidth: this.spriteWidth,
164 spriteHeight: this.spriteHeight,
165
166 spriteDuration: this.spriteDuration
167 }
168 }
169}
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 1fb1cae82..dd4cefd65 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -15,7 +15,7 @@ import {
15 Table, 15 Table,
16 UpdatedAt 16 UpdatedAt
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' 18import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionLanguageUrl, MVideoCaptionVideo } from '@server/types/models'
19import { buildUUID } from '@shared/extra-utils' 19import { buildUUID } from '@shared/extra-utils'
20import { AttributesOnly } from '@shared/typescript-utils' 20import { AttributesOnly } from '@shared/typescript-utils'
21import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 21import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
@@ -225,7 +225,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption
225 } 225 }
226 } 226 }
227 227
228 getCaptionStaticPath (this: MVideoCaption) { 228 getCaptionStaticPath (this: MVideoCaptionLanguageUrl) {
229 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) 229 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
230 } 230 }
231 231
@@ -233,9 +233,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption
233 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) 233 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
234 } 234 }
235 235
236 getFileUrl (video: MVideo) { 236 getFileUrl (this: MVideoCaptionLanguageUrl, video: MVideo) {
237 if (!this.Video) this.Video = video as VideoModel
238
239 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() 237 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
240 238
241 return this.fileUrl 239 return this.fileUrl
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f90f2b7f6..0e9a84426 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -58,7 +58,7 @@ import {
58import { AttributesOnly } from '@shared/typescript-utils' 58import { AttributesOnly } from '@shared/typescript-utils'
59import { peertubeTruncate } from '../../helpers/core-utils' 59import { peertubeTruncate } from '../../helpers/core-utils'
60import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 60import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
61import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' 61import { exists, isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc'
62import { 62import {
63 isVideoDescriptionValid, 63 isVideoDescriptionValid,
64 isVideoDurationValid, 64 isVideoDurationValid,
@@ -75,6 +75,7 @@ import {
75 MChannel, 75 MChannel,
76 MChannelAccountDefault, 76 MChannelAccountDefault,
77 MChannelId, 77 MChannelId,
78 MStoryboard,
78 MStreamingPlaylist, 79 MStreamingPlaylist,
79 MStreamingPlaylistFilesVideo, 80 MStreamingPlaylistFilesVideo,
80 MUserAccountId, 81 MUserAccountId,
@@ -83,6 +84,8 @@ import {
83 MVideoAccountLight, 84 MVideoAccountLight,
84 MVideoAccountLightBlacklistAllFiles, 85 MVideoAccountLightBlacklistAllFiles,
85 MVideoAP, 86 MVideoAP,
87 MVideoAPLight,
88 MVideoCaptionLanguageUrl,
86 MVideoDetails, 89 MVideoDetails,
87 MVideoFileVideo, 90 MVideoFileVideo,
88 MVideoFormattable, 91 MVideoFormattable,
@@ -126,6 +129,7 @@ import {
126 VideosIdListQueryBuilder, 129 VideosIdListQueryBuilder,
127 VideosModelListQueryBuilder 130 VideosModelListQueryBuilder
128} from './sql/video' 131} from './sql/video'
132import { StoryboardModel } from './storyboard'
129import { TagModel } from './tag' 133import { TagModel } from './tag'
130import { ThumbnailModel } from './thumbnail' 134import { ThumbnailModel } from './thumbnail'
131import { VideoBlacklistModel } from './video-blacklist' 135import { VideoBlacklistModel } from './video-blacklist'
@@ -753,6 +757,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
753 }) 757 })
754 VideoJobInfo: VideoJobInfoModel 758 VideoJobInfo: VideoJobInfoModel
755 759
760 @HasOne(() => StoryboardModel, {
761 foreignKey: {
762 name: 'videoId',
763 allowNull: false
764 },
765 onDelete: 'cascade'
766 })
767 Storyboard: StoryboardModel
768
756 @AfterCreate 769 @AfterCreate
757 static notifyCreate (video: MVideo) { 770 static notifyCreate (video: MVideo) {
758 InternalEventEmitter.Instance.emit('video-created', { video }) 771 InternalEventEmitter.Instance.emit('video-created', { video })
@@ -904,6 +917,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
904 required: false 917 required: false
905 }, 918 },
906 { 919 {
920 model: StoryboardModel.unscoped(),
921 required: false
922 },
923 {
907 attributes: [ 'id', 'url' ], 924 attributes: [ 'id', 'url' ],
908 model: VideoShareModel.unscoped(), 925 model: VideoShareModel.unscoped(),
909 required: false, 926 required: false,
@@ -1768,6 +1785,32 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1768 ) 1785 )
1769 } 1786 }
1770 1787
1788 async lightAPToFullAP (this: MVideoAPLight, transaction: Transaction): Promise<MVideoAP> {
1789 const videoAP = this as MVideoAP
1790
1791 const getCaptions = () => {
1792 if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions
1793
1794 return this.$get('VideoCaptions', {
1795 attributes: [ 'filename', 'language', 'fileUrl' ],
1796 transaction
1797 }) as Promise<MVideoCaptionLanguageUrl[]>
1798 }
1799
1800 const getStoryboard = () => {
1801 if (videoAP.Storyboard) return videoAP.Storyboard
1802
1803 return this.$get('Storyboard', { transaction }) as Promise<MStoryboard>
1804 }
1805
1806 const [ captions, storyboard ] = await Promise.all([ getCaptions(), getStoryboard() ])
1807
1808 return Object.assign(this, {
1809 VideoCaptions: captions,
1810 Storyboard: storyboard
1811 })
1812 }
1813
1771 getTruncatedDescription () { 1814 getTruncatedDescription () {
1772 if (!this.description) return null 1815 if (!this.description) return null
1773 1816