59d3e105085697fd7e228f450c11706a86a1a911
[oweals/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 }