diff options
author | Chocobozzz <me@florianbigard.com> | 2018-07-12 19:02:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-07-16 11:50:08 +0200 |
commit | 40e87e9ecc54e3513fb586928330a7855eb192c6 (patch) | |
tree | af1111ecba85f9cd8286811ff332a67cf21be2f6 /server/models/video/video-caption.ts | |
parent | d4557fd3ecc8d4ed4fb0e5c868929bc36c959ed2 (diff) | |
download | PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.gz PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.zst PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.zip |
Implement captions/subtitles
Diffstat (limited to 'server/models/video/video-caption.ts')
-rw-r--r-- | server/models/video/video-caption.ts | 173 |
1 files changed, 173 insertions, 0 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { | ||
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' | ||
15 | import { throwIfNotValid } from '../utils' | ||
16 | import { VideoModel } from './video' | ||
17 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | ||
18 | import { VideoCaption } from '../../../shared/models/videos/video-caption.model' | ||
19 | import { CONFIG, STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers' | ||
20 | import { join } from 'path' | ||
21 | import { logger } from '../../helpers/logger' | ||
22 | import { unlinkPromise } from '../../helpers/core-utils' | ||
23 | |||
24 | export 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 | }) | ||
52 | export 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 | } | ||