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