aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-07-12 19:02:00 +0200
committerChocobozzz <me@florianbigard.com>2018-07-16 11:50:08 +0200
commit40e87e9ecc54e3513fb586928330a7855eb192c6 (patch)
treeaf1111ecba85f9cd8286811ff332a67cf21be2f6 /server/models/video
parentd4557fd3ecc8d4ed4fb0e5c868929bc36c959ed2 (diff)
downloadPeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.gz
PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.zst
PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.zip
Implement captions/subtitles
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-caption.ts173
-rw-r--r--server/models/video/video.ts40
2 files changed, 206 insertions, 7 deletions
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
new file mode 100644
index 000000000..9920dfc7c
--- /dev/null
+++ b/server/models/video/video-caption.ts
@@ -0,0 +1,173 @@
1import * as Sequelize from 'sequelize'
2import {
3 AllowNull,
4 BeforeDestroy,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 ForeignKey,
9 Is,
10 Model,
11 Scopes,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { throwIfNotValid } from '../utils'
16import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/video-caption.model'
19import { CONFIG, STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers'
20import { join } from 'path'
21import { logger } from '../../helpers/logger'
22import { unlinkPromise } from '../../helpers/core-utils'
23
24export enum ScopeNames {
25 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
26}
27
28@Scopes({
29 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
30 include: [
31 {
32 attributes: [ 'uuid', 'remote' ],
33 model: () => VideoModel.unscoped(),
34 required: true
35 }
36 ]
37 }
38})
39
40@Table({
41 tableName: 'videoCaption',
42 indexes: [
43 {
44 fields: [ 'videoId' ]
45 },
46 {
47 fields: [ 'videoId', 'language' ],
48 unique: true
49 }
50 ]
51})
52export class VideoCaptionModel extends Model<VideoCaptionModel> {
53 @CreatedAt
54 createdAt: Date
55
56 @UpdatedAt
57 updatedAt: Date
58
59 @AllowNull(false)
60 @Is('VideoCaptionLanguage', value => throwIfNotValid(value, isVideoCaptionLanguageValid, 'language'))
61 @Column
62 language: string
63
64 @ForeignKey(() => VideoModel)
65 @Column
66 videoId: number
67
68 @BelongsTo(() => VideoModel, {
69 foreignKey: {
70 allowNull: false
71 },
72 onDelete: 'CASCADE'
73 })
74 Video: VideoModel
75
76 @BeforeDestroy
77 static async removeFiles (instance: VideoCaptionModel) {
78
79 if (instance.isOwned()) {
80 if (!instance.Video) {
81 instance.Video = await instance.$get('Video') as VideoModel
82 }
83
84 logger.debug('Removing captions %s of video %s.', instance.Video.uuid, instance.language)
85 return instance.removeCaptionFile()
86 }
87
88 return undefined
89 }
90
91 static loadByVideoIdAndLanguage (videoId: string | number, language: string) {
92 const videoInclude = {
93 model: VideoModel.unscoped(),
94 attributes: [ 'id', 'remote', 'uuid' ],
95 where: { }
96 }
97
98 if (typeof videoId === 'string') videoInclude.where['uuid'] = videoId
99 else videoInclude.where['id'] = videoId
100
101 const query = {
102 where: {
103 language
104 },
105 include: [
106 videoInclude
107 ]
108 }
109
110 return VideoCaptionModel.findOne(query)
111 }
112
113 static insertOrReplaceLanguage (videoId: number, language: string, transaction: Sequelize.Transaction) {
114 const values = {
115 videoId,
116 language
117 }
118
119 return VideoCaptionModel.upsert(values, { transaction })
120 }
121
122 static listVideoCaptions (videoId: number) {
123 const query = {
124 order: [ [ 'language', 'ASC' ] ],
125 where: {
126 videoId
127 }
128 }
129
130 return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query)
131 }
132
133 static getLanguageLabel (language: string) {
134 return VIDEO_LANGUAGES[language] || 'Unknown'
135 }
136
137 static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Sequelize.Transaction) {
138 const query = {
139 where: {
140 videoId
141 },
142 transaction
143 }
144
145 return VideoCaptionModel.destroy(query)
146 }
147
148 isOwned () {
149 return this.Video.remote === false
150 }
151
152 toFormattedJSON (): VideoCaption {
153 return {
154 language: {
155 id: this.language,
156 label: VideoCaptionModel.getLanguageLabel(this.language)
157 },
158 captionPath: this.getCaptionStaticPath()
159 }
160 }
161
162 getCaptionStaticPath () {
163 return join(STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
164 }
165
166 getCaptionName () {
167 return `${this.Video.uuid}-${this.language}.vtt`
168 }
169
170 removeCaptionFile () {
171 return unlinkPromise(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
172 }
173}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index ab33b7c99..74a3a5d05 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -92,6 +92,7 @@ import { VideoFileModel } from './video-file'
92import { VideoShareModel } from './video-share' 92import { VideoShareModel } from './video-share'
93import { VideoTagModel } from './video-tag' 93import { VideoTagModel } from './video-tag'
94import { ScheduleVideoUpdateModel } from './schedule-video-update' 94import { ScheduleVideoUpdateModel } from './schedule-video-update'
95import { VideoCaptionModel } from './video-caption'
95 96
96export enum ScopeNames { 97export enum ScopeNames {
97 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 98 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -526,6 +527,17 @@ export class VideoModel extends Model<VideoModel> {
526 }) 527 })
527 ScheduleVideoUpdate: ScheduleVideoUpdateModel 528 ScheduleVideoUpdate: ScheduleVideoUpdateModel
528 529
530 @HasMany(() => VideoCaptionModel, {
531 foreignKey: {
532 name: 'videoId',
533 allowNull: false
534 },
535 onDelete: 'cascade',
536 hooks: true,
537 ['separate' as any]: true
538 })
539 VideoCaptions: VideoCaptionModel[]
540
529 @BeforeDestroy 541 @BeforeDestroy
530 static async sendDelete (instance: VideoModel, options) { 542 static async sendDelete (instance: VideoModel, options) {
531 if (instance.isOwned()) { 543 if (instance.isOwned()) {
@@ -550,7 +562,7 @@ export class VideoModel extends Model<VideoModel> {
550 } 562 }
551 563
552 @BeforeDestroy 564 @BeforeDestroy
553 static async removeFilesAndSendDelete (instance: VideoModel) { 565 static async removeFiles (instance: VideoModel) {
554 const tasks: Promise<any>[] = [] 566 const tasks: Promise<any>[] = []
555 567
556 logger.debug('Removing files of video %s.', instance.url) 568 logger.debug('Removing files of video %s.', instance.url)
@@ -616,6 +628,11 @@ export class VideoModel extends Model<VideoModel> {
616 }, 628 },
617 include: [ 629 include: [
618 { 630 {
631 attributes: [ 'language' ],
632 model: VideoCaptionModel.unscoped(),
633 required: false
634 },
635 {
619 attributes: [ 'id', 'url' ], 636 attributes: [ 'id', 'url' ],
620 model: VideoShareModel.unscoped(), 637 model: VideoShareModel.unscoped(),
621 required: false, 638 required: false,
@@ -1028,15 +1045,15 @@ export class VideoModel extends Model<VideoModel> {
1028 videoFile.infoHash = parsedTorrent.infoHash 1045 videoFile.infoHash = parsedTorrent.infoHash
1029 } 1046 }
1030 1047
1031 getEmbedPath () { 1048 getEmbedStaticPath () {
1032 return '/videos/embed/' + this.uuid 1049 return '/videos/embed/' + this.uuid
1033 } 1050 }
1034 1051
1035 getThumbnailPath () { 1052 getThumbnailStaticPath () {
1036 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) 1053 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
1037 } 1054 }
1038 1055
1039 getPreviewPath () { 1056 getPreviewStaticPath () {
1040 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 1057 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
1041 } 1058 }
1042 1059
@@ -1077,9 +1094,9 @@ export class VideoModel extends Model<VideoModel> {
1077 views: this.views, 1094 views: this.views,
1078 likes: this.likes, 1095 likes: this.likes,
1079 dislikes: this.dislikes, 1096 dislikes: this.dislikes,
1080 thumbnailPath: this.getThumbnailPath(), 1097 thumbnailPath: this.getThumbnailStaticPath(),
1081 previewPath: this.getPreviewPath(), 1098 previewPath: this.getPreviewStaticPath(),
1082 embedPath: this.getEmbedPath(), 1099 embedPath: this.getEmbedStaticPath(),
1083 createdAt: this.createdAt, 1100 createdAt: this.createdAt,
1084 updatedAt: this.updatedAt, 1101 updatedAt: this.updatedAt,
1085 publishedAt: this.publishedAt, 1102 publishedAt: this.publishedAt,
@@ -1247,6 +1264,14 @@ export class VideoModel extends Model<VideoModel> {
1247 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid 1264 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
1248 }) 1265 })
1249 1266
1267 const subtitleLanguage = []
1268 for (const caption of this.VideoCaptions) {
1269 subtitleLanguage.push({
1270 identifier: caption.language,
1271 name: VideoCaptionModel.getLanguageLabel(caption.language)
1272 })
1273 }
1274
1250 return { 1275 return {
1251 type: 'Video' as 'Video', 1276 type: 'Video' as 'Video',
1252 id: this.url, 1277 id: this.url,
@@ -1267,6 +1292,7 @@ export class VideoModel extends Model<VideoModel> {
1267 mediaType: 'text/markdown', 1292 mediaType: 'text/markdown',
1268 content: this.getTruncatedDescription(), 1293 content: this.getTruncatedDescription(),
1269 support: this.support, 1294 support: this.support,
1295 subtitleLanguage,
1270 icon: { 1296 icon: {
1271 type: 'Image', 1297 type: 'Image',
1272 url: this.getThumbnailUrl(baseUrlHttp), 1298 url: this.getThumbnailUrl(baseUrlHttp),