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