]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-caption.ts
Generate a name for caption files
[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 { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
19 import { MVideoAccountLight, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
20 import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
21 import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
22 import { logger } from '../../helpers/logger'
23 import { CONFIG } from '../../initializers/config'
24 import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
25 import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
26 import { VideoModel } from './video'
27 import { v4 as uuidv4 } from 'uuid'
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 {
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) {
95 if (!instance.Video) {
96 instance.Video = await instance.$get('Video')
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): 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 }
127
128 return VideoCaptionModel.findOne(query)
129 }
130
131 static loadWithVideoByFilename (filename: string): Promise<MVideoCaptionVideo> {
132 const query = {
133 where: {
134 filename
135 },
136 include: [
137 {
138 model: VideoModel.unscoped(),
139 attributes: [ 'id', 'remote', 'uuid' ]
140 }
141 ]
142 }
143
144 return VideoCaptionModel.findOne(query)
145 }
146
147 static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) {
148 const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language)
149 // Delete existing file
150 if (existing) await existing.destroy({ transaction })
151
152 return caption.save({ transaction })
153 }
154
155 static listVideoCaptions (videoId: number): Promise<MVideoCaptionVideo[]> {
156 const query = {
157 order: [ [ 'language', 'ASC' ] ] as OrderItem[],
158 where: {
159 videoId
160 }
161 }
162
163 return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query)
164 }
165
166 static getLanguageLabel (language: string) {
167 return VIDEO_LANGUAGES[language] || 'Unknown'
168 }
169
170 static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Transaction) {
171 const query = {
172 where: {
173 videoId
174 },
175 transaction
176 }
177
178 return VideoCaptionModel.destroy(query)
179 }
180
181 static generateCaptionName (language: string) {
182 return `${uuidv4()}-${language}.vtt`
183 }
184
185 isOwned () {
186 return this.Video.remote === false
187 }
188
189 toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
190 return {
191 language: {
192 id: this.language,
193 label: VideoCaptionModel.getLanguageLabel(this.language)
194 },
195 captionPath: this.getCaptionStaticPath()
196 }
197 }
198
199 getCaptionStaticPath (this: MVideoCaption) {
200 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
201 }
202
203 removeCaptionFile (this: MVideoCaption) {
204 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
205 }
206
207 getFileUrl (video: MVideoAccountLight) {
208 if (!this.Video) this.Video = video as VideoModel
209
210 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
211 if (this.fileUrl) return this.fileUrl
212
213 // Fallback if we don't have a file URL
214 return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
215 }
216 }