Merge branch 'release/1.4.0' into develop
[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   ForeignKey,
9   Is,
10   Model,
11   Scopes,
12   Table,
13   UpdatedAt
14 } from 'sequelize-typescript'
15 import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
16 import { VideoModel } from './video'
17 import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18 import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
19 import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants'
20 import { join } from 'path'
21 import { logger } from '../../helpers/logger'
22 import { remove } from 'fs-extra'
23 import { CONFIG } from '../../initializers/config'
24 import * as Bluebird from 'bluebird'
25 import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
26
27 export enum ScopeNames {
28   WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
29 }
30
31 @Scopes(() => ({
32   [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
33     include: [
34       {
35         attributes: [ 'id', 'uuid', 'remote' ],
36         model: VideoModel.unscoped(),
37         required: true
38       }
39     ]
40   }
41 }))
42
43 @Table({
44   tableName: 'videoCaption',
45   indexes: [
46     {
47       fields: [ 'videoId' ]
48     },
49     {
50       fields: [ 'videoId', 'language' ],
51       unique: true
52     }
53   ]
54 })
55 export class VideoCaptionModel extends Model<VideoCaptionModel> {
56   @CreatedAt
57   createdAt: Date
58
59   @UpdatedAt
60   updatedAt: Date
61
62   @AllowNull(false)
63   @Is('VideoCaptionLanguage', value => throwIfNotValid(value, isVideoCaptionLanguageValid, 'language'))
64   @Column
65   language: string
66
67   @ForeignKey(() => VideoModel)
68   @Column
69   videoId: number
70
71   @BelongsTo(() => VideoModel, {
72     foreignKey: {
73       allowNull: false
74     },
75     onDelete: 'CASCADE'
76   })
77   Video: VideoModel
78
79   @BeforeDestroy
80   static async removeFiles (instance: VideoCaptionModel) {
81     if (!instance.Video) {
82       instance.Video = await instance.$get('Video') as VideoModel
83     }
84
85     if (instance.isOwned()) {
86       logger.info('Removing captions %s of video %s.', instance.Video.uuid, instance.language)
87
88       try {
89         await instance.removeCaptionFile()
90       } catch (err) {
91         logger.error('Cannot remove caption file of video %s.', instance.Video.uuid)
92       }
93     }
94
95     return undefined
96   }
97
98   static loadByVideoIdAndLanguage (videoId: string | number, language: string): Bluebird<MVideoCaptionVideo> {
99     const videoInclude = {
100       model: VideoModel.unscoped(),
101       attributes: [ 'id', 'remote', 'uuid' ],
102       where: buildWhereIdOrUUID(videoId)
103     }
104
105     const query = {
106       where: {
107         language
108       },
109       include: [
110         videoInclude
111       ]
112     }
113
114     return VideoCaptionModel.findOne(query)
115   }
116
117   static insertOrReplaceLanguage (videoId: number, language: string, transaction: Transaction) {
118     const values = {
119       videoId,
120       language
121     }
122
123     return (VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) as any) // FIXME: typings
124       .then(([ caption ]) => caption)
125   }
126
127   static listVideoCaptions (videoId: number): Bluebird<MVideoCaptionVideo[]> {
128     const query = {
129       order: [ [ 'language', 'ASC' ] ] as OrderItem[],
130       where: {
131         videoId
132       }
133     }
134
135     return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query)
136   }
137
138   static getLanguageLabel (language: string) {
139     return VIDEO_LANGUAGES[language] || 'Unknown'
140   }
141
142   static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Transaction) {
143     const query = {
144       where: {
145         videoId
146       },
147       transaction
148     }
149
150     return VideoCaptionModel.destroy(query)
151   }
152
153   isOwned () {
154     return this.Video.remote === false
155   }
156
157   toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
158     return {
159       language: {
160         id: this.language,
161         label: VideoCaptionModel.getLanguageLabel(this.language)
162       },
163       captionPath: this.getCaptionStaticPath()
164     }
165   }
166
167   getCaptionStaticPath (this: MVideoCaptionFormattable) {
168     return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
169   }
170
171   getCaptionName (this: MVideoCaptionFormattable) {
172     return `${this.Video.uuid}-${this.language}.vtt`
173   }
174
175   removeCaptionFile (this: MVideoCaptionFormattable) {
176     return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
177   }
178 }