]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-caption.ts
e8e883dd012a7a7cfd177b07dcaf139ccec13199
[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, 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: [ 'videoId' ]
49 },
50 {
51 fields: [ 'videoId', 'language' ],
52 unique: true
53 }
54 ]
55 })
56 export class VideoCaptionModel extends Model {
57 @CreatedAt
58 createdAt: Date
59
60 @UpdatedAt
61 updatedAt: Date
62
63 @AllowNull(false)
64 @Is('VideoCaptionLanguage', value => throwIfNotValid(value, isVideoCaptionLanguageValid, 'language'))
65 @Column
66 language: string
67
68 @AllowNull(true)
69 @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
70 fileUrl: string
71
72 @ForeignKey(() => VideoModel)
73 @Column
74 videoId: number
75
76 @BelongsTo(() => VideoModel, {
77 foreignKey: {
78 allowNull: false
79 },
80 onDelete: 'CASCADE'
81 })
82 Video: VideoModel
83
84 @BeforeDestroy
85 static async removeFiles (instance: VideoCaptionModel) {
86 if (!instance.Video) {
87 instance.Video = await instance.$get('Video')
88 }
89
90 if (instance.isOwned()) {
91 logger.info('Removing captions %s of video %s.', instance.Video.uuid, instance.language)
92
93 try {
94 await instance.removeCaptionFile()
95 } catch (err) {
96 logger.error('Cannot remove caption file of video %s.', instance.Video.uuid)
97 }
98 }
99
100 return undefined
101 }
102
103 static loadByVideoIdAndLanguage (videoId: string | number, language: string): Promise<MVideoCaptionVideo> {
104 const videoInclude = {
105 model: VideoModel.unscoped(),
106 attributes: [ 'id', 'remote', 'uuid' ],
107 where: buildWhereIdOrUUID(videoId)
108 }
109
110 const query = {
111 where: {
112 language
113 },
114 include: [
115 videoInclude
116 ]
117 }
118
119 return VideoCaptionModel.findOne(query)
120 }
121
122 static insertOrReplaceLanguage (videoId: number, language: string, fileUrl: string, transaction: Transaction) {
123 const values = {
124 videoId,
125 language,
126 fileUrl
127 }
128
129 return VideoCaptionModel.upsert(values, { transaction, returning: true })
130 .then(([ caption ]) => caption)
131 }
132
133 static listVideoCaptions (videoId: number): Promise<MVideoCaptionVideo[]> {
134 const query = {
135 order: [ [ 'language', 'ASC' ] ] as OrderItem[],
136 where: {
137 videoId
138 }
139 }
140
141 return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query)
142 }
143
144 static getLanguageLabel (language: string) {
145 return VIDEO_LANGUAGES[language] || 'Unknown'
146 }
147
148 static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Transaction) {
149 const query = {
150 where: {
151 videoId
152 },
153 transaction
154 }
155
156 return VideoCaptionModel.destroy(query)
157 }
158
159 isOwned () {
160 return this.Video.remote === false
161 }
162
163 toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
164 return {
165 language: {
166 id: this.language,
167 label: VideoCaptionModel.getLanguageLabel(this.language)
168 },
169 captionPath: this.getCaptionStaticPath()
170 }
171 }
172
173 getCaptionStaticPath (this: MVideoCaptionFormattable) {
174 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
175 }
176
177 getCaptionName (this: MVideoCaptionFormattable) {
178 return `${this.Video.uuid}-${this.language}.vtt`
179 }
180
181 removeCaptionFile (this: MVideoCaptionFormattable) {
182 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
183 }
184
185 getFileUrl (video: MVideoAccountLight) {
186 if (!this.Video) this.Video = video as VideoModel
187
188 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
189 if (this.fileUrl) return this.fileUrl
190
191 // Fallback if we don't have a file URL
192 return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
193 }
194 }