]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-file.ts
Fix player translations
[github/Chocobozzz/PeerTube.git] / server / models / video / video-file.ts
CommitLineData
c48e82b5
C
1import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 DataType,
7 Default,
8 ForeignKey,
9 HasMany,
10 Is,
11 Model,
12 Table,
8319d6ae
RK
13 UpdatedAt,
14 Scopes,
15 DefaultScope
c48e82b5 16} from 'sequelize-typescript'
3a6f351b 17import {
14e2014a 18 isVideoFileExtnameValid,
3a6f351b
C
19 isVideoFileInfoHashValid,
20 isVideoFileResolutionValid,
21 isVideoFileSizeValid,
22 isVideoFPSResolutionValid
23} from '../../helpers/custom-validators/videos'
3acc5084 24import { parseAggregateResult, throwIfNotValid } from '../utils'
3fd3ab2d 25import { VideoModel } from './video'
c48e82b5 26import { VideoRedundancyModel } from '../redundancy/video-redundancy'
ae9bbed4 27import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
d7a25329 28import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
35f28e94 29import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
26d6bf65
C
30import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
31import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
35f28e94 32import * as memoizee from 'memoizee'
7b81edc8 33import validator from 'validator'
93e1258c 34
8319d6ae
RK
35export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO',
8319d6ae
RK
37 WITH_METADATA = 'WITH_METADATA'
38}
39
8319d6ae
RK
40@DefaultScope(() => ({
41 attributes: {
7b81edc8 42 exclude: [ 'metadata' ]
8319d6ae
RK
43 }
44}))
45@Scopes(() => ({
46 [ScopeNames.WITH_VIDEO]: {
47 include: [
48 {
49 model: VideoModel.unscoped(),
50 required: true
51 }
52 ]
53 },
8319d6ae
RK
54 [ScopeNames.WITH_METADATA]: {
55 attributes: {
7b81edc8 56 include: [ 'metadata' ]
8319d6ae
RK
57 }
58 }
59}))
3fd3ab2d
C
60@Table({
61 tableName: 'videoFile',
62 indexes: [
93e1258c 63 {
d7a25329
C
64 fields: [ 'videoId' ],
65 where: {
66 videoId: {
67 [Op.ne]: null
68 }
69 }
70 },
71 {
72 fields: [ 'videoStreamingPlaylistId' ],
73 where: {
74 videoStreamingPlaylistId: {
75 [Op.ne]: null
76 }
77 }
93e1258c 78 },
d7a25329 79
93e1258c 80 {
3fd3ab2d 81 fields: [ 'infoHash' ]
8cd72bd3 82 },
d7a25329 83
8cd72bd3
C
84 {
85 fields: [ 'videoId', 'resolution', 'fps' ],
d7a25329
C
86 unique: true,
87 where: {
88 videoId: {
89 [Op.ne]: null
90 }
91 }
92 },
93 {
94 fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
95 unique: true,
96 where: {
97 videoStreamingPlaylistId: {
98 [Op.ne]: null
99 }
100 }
93e1258c 101 }
93e1258c 102 ]
3fd3ab2d
C
103})
104export class VideoFileModel extends Model<VideoFileModel> {
105 @CreatedAt
106 createdAt: Date
107
108 @UpdatedAt
109 updatedAt: Date
110
111 @AllowNull(false)
112 @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
113 @Column
114 resolution: number
115
116 @AllowNull(false)
117 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
118 @Column(DataType.BIGINT)
119 size: number
120
121 @AllowNull(false)
14e2014a
C
122 @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
123 @Column
3fd3ab2d
C
124 extname: string
125
c6c0fa6c
C
126 @AllowNull(true)
127 @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash', true))
3fd3ab2d
C
128 @Column
129 infoHash: string
130
2e7cf5ae
C
131 @AllowNull(false)
132 @Default(-1)
3a6f351b
C
133 @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
134 @Column
135 fps: number
136
8319d6ae
RK
137 @AllowNull(true)
138 @Column(DataType.JSONB)
139 metadata: any
140
141 @AllowNull(true)
142 @Column
143 metadataUrl: string
144
3fd3ab2d
C
145 @ForeignKey(() => VideoModel)
146 @Column
147 videoId: number
148
149 @BelongsTo(() => VideoModel, {
93e1258c 150 foreignKey: {
d7a25329 151 allowNull: true
93e1258c
C
152 },
153 onDelete: 'CASCADE'
154 })
3fd3ab2d 155 Video: VideoModel
cc43831a 156
d7a25329
C
157 @ForeignKey(() => VideoStreamingPlaylistModel)
158 @Column
159 videoStreamingPlaylistId: number
160
161 @BelongsTo(() => VideoStreamingPlaylistModel, {
162 foreignKey: {
163 allowNull: true
164 },
165 onDelete: 'CASCADE'
166 })
167 VideoStreamingPlaylist: VideoStreamingPlaylistModel
168
c48e82b5
C
169 @HasMany(() => VideoRedundancyModel, {
170 foreignKey: {
09209296 171 allowNull: true
c48e82b5
C
172 },
173 onDelete: 'CASCADE',
174 hooks: true
175 })
176 RedundancyVideos: VideoRedundancyModel[]
177
35f28e94
C
178 static doesInfohashExistCached = memoizee(VideoFileModel.doesInfohashExist, {
179 promise: true,
180 max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
181 maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
182 })
183
09209296 184 static doesInfohashExist (infoHash: string) {
cc43831a
C
185 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
186 const options = {
d5d9b6d7 187 type: QueryTypes.SELECT as QueryTypes.SELECT,
cc43831a
C
188 bind: { infoHash },
189 raw: true
190 }
191
192 return VideoModel.sequelize.query(query, options)
3acc5084 193 .then(results => results.length === 1)
cc43831a 194 }
e5565833 195
8319d6ae
RK
196 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
197 const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
7b81edc8
C
198
199 return !!videoFile
8319d6ae
RK
200 }
201
202 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
204 }
205
25378bc8 206 static loadWithVideo (id: number) {
8319d6ae
RK
207 return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id)
208 }
25378bc8 209
8319d6ae 210 static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
7b81edc8
C
211 const whereVideo = validator.isUUID(videoIdOrUUID + '')
212 ? { uuid: videoIdOrUUID }
213 : { id: videoIdOrUUID }
214
215 const options = {
216 where: {
217 id
218 },
219 include: [
220 {
221 model: VideoModel.unscoped(),
222 required: false,
223 where: whereVideo
224 },
225 {
226 model: VideoStreamingPlaylistModel.unscoped(),
227 required: false,
228 include: [
229 {
230 model: VideoModel.unscoped(),
231 required: true,
232 where: whereVideo
233 }
234 ]
235 }
8319d6ae 236 ]
7b81edc8
C
237 }
238
239 return VideoFileModel.findOne(options)
240 .then(file => {
241 // We used `required: false` so check we have at least a video or a streaming playlist
242 if (!file.Video && !file.VideoStreamingPlaylist) return null
243
244 return file
245 })
25378bc8
C
246 }
247
3acc5084 248 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
ae9bbed4
C
249 const query = {
250 include: [
251 {
252 model: VideoModel.unscoped(),
253 required: true,
254 include: [
255 {
256 model: VideoStreamingPlaylistModel.unscoped(),
257 required: true,
258 where: {
259 id: streamingPlaylistId
260 }
261 }
262 ]
263 }
264 ],
265 transaction
266 }
267
268 return VideoFileModel.findAll(query)
269 }
270
3acc5084
C
271 static getStats () {
272 const query: FindOptions = {
44b9c0ba
C
273 include: [
274 {
275 attributes: [],
276 model: VideoModel.unscoped(),
277 where: {
278 remote: false
279 }
280 }
281 ]
44b9c0ba 282 }
3acc5084
C
283
284 return VideoFileModel.aggregate('size', 'SUM', query)
285 .then(result => ({
286 totalLocalVideoFilesSize: parseAggregateResult(result)
287 }))
44b9c0ba
C
288 }
289
d7a25329
C
290 // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
291 static async customUpsert (
292 videoFile: MVideoFile,
293 mode: 'streaming-playlist' | 'video',
294 transaction: Transaction
295 ) {
296 const baseWhere = {
297 fps: videoFile.fps,
298 resolution: videoFile.resolution
299 }
300
301 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
302 else Object.assign(baseWhere, { videoId: videoFile.videoId })
303
304 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
305 if (!element) return videoFile.save({ transaction })
306
307 for (const k of Object.keys(videoFile.toJSON())) {
308 element[k] = videoFile[k]
309 }
310
311 return element.save({ transaction })
312 }
313
97969c4e
C
314 static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) {
315 const options = {
316 where: { videoStreamingPlaylistId }
317 }
318
319 return VideoFileModel.destroy(options)
320 }
321
d7a25329
C
322 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
323 if (this.videoId) return (this as MVideoFileVideo).Video
324
325 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
326 }
327
536598cf
C
328 isAudio () {
329 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
330 }
331
bd54ad19
C
332 isLive () {
333 return this.size === -1
334 }
335
053aed43 336 isHLS () {
9e2b2e76 337 return !!this.videoStreamingPlaylistId
053aed43
C
338 }
339
453e83ea 340 hasSameUniqueKeysThan (other: MVideoFile) {
e5565833
C
341 return this.fps === other.fps &&
342 this.resolution === other.resolution &&
d7a25329
C
343 (
344 (this.videoId !== null && this.videoId === other.videoId) ||
345 (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
346 )
e5565833 347 }
93e1258c 348}