]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-caption.ts
Fix subscribe button responsive
[github/Chocobozzz/PeerTube.git] / server / models / video / video-caption.ts
1 import { remove } from 'fs-extra'
2 import { join } from 'path'
3 import { OrderItem, Transaction } from 'sequelize'
4 import {
5 AllowNull,
6 BeforeDestroy,
7 BelongsTo,
8 Column,
9 CreatedAt,
10 DataType,
11 ForeignKey,
12 Is,
13 Model,
14 Scopes,
15 Table,
16 UpdatedAt
17 } from 'sequelize-typescript'
18 import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
19 import { buildUUID } from '@shared/extra-utils'
20 import { AttributesOnly } from '@shared/typescript-utils'
21 import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
22 import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
23 import { logger } from '../../helpers/logger'
24 import { CONFIG } from '../../initializers/config'
25 import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
26 import { buildWhereIdOrUUID, throwIfNotValid } from '../shared'
27 import { VideoModel } from './video'
28
29 export enum ScopeNames {
30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
31 }
32
33 @Scopes(() => ({
34 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
35 include: [
36 {
37 attributes: [ 'id', 'uuid', 'remote' ],
38 model: VideoModel.unscoped(),
39 required: true
40 }
41 ]
42 }
43 }))
44
45 @Table({
46 tableName: 'videoCaption',
47 indexes: [
48 {
49 fields: [ 'filename' ],
50 unique: true
51 },
52 {
53 fields: [ 'videoId' ]
54 },
55 {
56 fields: [ 'videoId', 'language' ],
57 unique: true
58 }
59 ]
60 })
61 export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaptionModel>>> {
62 @CreatedAt
63 createdAt: Date
64
65 @UpdatedAt
66 updatedAt: Date
67
68 @AllowNull(false)
69 @Is('VideoCaptionLanguage', value => throwIfNotValid(value, isVideoCaptionLanguageValid, 'language'))
70 @Column
71 language: string
72
73 @AllowNull(false)
74 @Column
75 filename: string
76
77 @AllowNull(true)
78 @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
79 fileUrl: string
80
81 @ForeignKey(() => VideoModel)
82 @Column
83 videoId: number
84
85 @BelongsTo(() => VideoModel, {
86 foreignKey: {
87 allowNull: false
88 },
89 onDelete: 'CASCADE'
90 })
91 Video: VideoModel
92
93 @BeforeDestroy
94 static async removeFiles (instance: VideoCaptionModel, options) {
95 if (!instance.Video) {
96 instance.Video = await instance.$get('Video', { transaction: options.transaction })
97 }
98
99 if (instance.isOwned()) {
100 logger.info('Removing caption %s.', instance.filename)
101
102 try {
103 await instance.removeCaptionFile()
104 } catch (err) {
105 logger.error('Cannot remove caption file %s.', instance.filename)
106 }
107 }
108
109 return undefined
110 }
111
112 static loadByVideoIdAndLanguage (videoId: string | number, language: string, transaction?: Transaction): Promise<MVideoCaptionVideo> {
113 const videoInclude = {
114 model: VideoModel.unscoped(),
115 attributes: [ 'id', 'remote', 'uuid' ],
116 where: buildWhereIdOrUUID(videoId)
117 }
118
119 const query = {
120 where: {
121 language
122 },
123 include: [
124 videoInclude
125 ],
126 transaction
127 }
128
129 return VideoCaptionModel.findOne(query)
130 }
131
132 static loadWithVideoByFilename (filename: string): Promise<MVideoCaptionVideo> {
133 const query = {
134 where: {
135 filename
136 },
137 include: [
138 {
139 model: VideoModel.unscoped(),
140 attributes: [ 'id', 'remote', 'uuid' ]
141 }
142 ]
143 }
144
145 return VideoCaptionModel.findOne(query)
146 }
147
148 static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) {
149 const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language, transaction)
150
151 // Delete existing file
152 if (existing) await existing.destroy({ transaction })
153
154 return caption.save({ transaction })
155 }
156
157 static listVideoCaptions (videoId: number, transaction?: Transaction): Promise<MVideoCaptionVideo[]> {
158 const query = {
159 order: [ [ 'language', 'ASC' ] ] as OrderItem[],
160 where: {
161 videoId
162 },
163 transaction
164 }
165
166 return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query)
167 }
168
169 static getLanguageLabel (language: string) {
170 return VIDEO_LANGUAGES[language] || 'Unknown'
171 }
172
173 static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Transaction) {
174 const query = {
175 where: {
176 videoId
177 },
178 transaction
179 }
180
181 return VideoCaptionModel.destroy(query)
182 }
183
184 static generateCaptionName (language: string) {
185 return `${buildUUID()}-${language}.vtt`
186 }
187
188 isOwned () {
189 return this.Video.remote === false
190 }
191
192 toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
193 return {
194 language: {
195 id: this.language,
196 label: VideoCaptionModel.getLanguageLabel(this.language)
197 },
198 captionPath: this.getCaptionStaticPath(),
199 updatedAt: this.updatedAt.toISOString()
200 }
201 }
202
203 getCaptionStaticPath (this: MVideoCaption) {
204 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
205 }
206
207 removeCaptionFile (this: MVideoCaption) {
208 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
209 }
210
211 getFileUrl (video: MVideo) {
212 if (!this.Video) this.Video = video as VideoModel
213
214 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
215
216 return this.fileUrl
217 }
218
219 isEqual (this: MVideoCaption, other: MVideoCaption) {
220 if (this.fileUrl) return this.fileUrl === other.fileUrl
221
222 return this.filename === other.filename
223 }
224 }