]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-file.ts
48b337c681a7655e2446e374c4bdc44484138cab
[github/Chocobozzz/PeerTube.git] / server / models / video / video-file.ts
1 import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 DataType,
7 Default,
8 ForeignKey,
9 HasMany,
10 Is,
11 Model,
12 Table,
13 UpdatedAt,
14 Scopes,
15 DefaultScope
16 } from 'sequelize-typescript'
17 import {
18 isVideoFileExtnameValid,
19 isVideoFileInfoHashValid,
20 isVideoFileResolutionValid,
21 isVideoFileSizeValid,
22 isVideoFPSResolutionValid
23 } from '../../helpers/custom-validators/videos'
24 import { parseAggregateResult, throwIfNotValid } from '../utils'
25 import { VideoModel } from './video'
26 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
27 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
28 import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
29 import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
30 import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
31 import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
32 import * as memoizee from 'memoizee'
33 import validator from 'validator'
34
35 export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO',
37 WITH_METADATA = 'WITH_METADATA'
38 }
39
40 @DefaultScope(() => ({
41 attributes: {
42 exclude: [ 'metadata' ]
43 }
44 }))
45 @Scopes(() => ({
46 [ScopeNames.WITH_VIDEO]: {
47 include: [
48 {
49 model: VideoModel.unscoped(),
50 required: true
51 }
52 ]
53 },
54 [ScopeNames.WITH_METADATA]: {
55 attributes: {
56 include: [ 'metadata' ]
57 }
58 }
59 }))
60 @Table({
61 tableName: 'videoFile',
62 indexes: [
63 {
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 }
78 },
79
80 {
81 fields: [ 'infoHash' ]
82 },
83
84 {
85 fields: [ 'videoId', 'resolution', 'fps' ],
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 }
101 }
102 ]
103 })
104 export class VideoFileModel extends Model {
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)
122 @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
123 @Column
124 extname: string
125
126 @AllowNull(true)
127 @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash', true))
128 @Column
129 infoHash: string
130
131 @AllowNull(false)
132 @Default(-1)
133 @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
134 @Column
135 fps: number
136
137 @AllowNull(true)
138 @Column(DataType.JSONB)
139 metadata: any
140
141 @AllowNull(true)
142 @Column
143 metadataUrl: string
144
145 @ForeignKey(() => VideoModel)
146 @Column
147 videoId: number
148
149 @BelongsTo(() => VideoModel, {
150 foreignKey: {
151 allowNull: true
152 },
153 onDelete: 'CASCADE'
154 })
155 Video: VideoModel
156
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
169 @HasMany(() => VideoRedundancyModel, {
170 foreignKey: {
171 allowNull: true
172 },
173 onDelete: 'CASCADE',
174 hooks: true
175 })
176 RedundancyVideos: VideoRedundancyModel[]
177
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
184 static doesInfohashExist (infoHash: string) {
185 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
186 const options = {
187 type: QueryTypes.SELECT as QueryTypes.SELECT,
188 bind: { infoHash },
189 raw: true
190 }
191
192 return VideoModel.sequelize.query(query, options)
193 .then(results => results.length === 1)
194 }
195
196 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
197 const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
198
199 return !!videoFile
200 }
201
202 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
204 }
205
206 static loadWithVideo (id: number) {
207 return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id)
208 }
209
210 static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
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 }
236 ]
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 })
246 }
247
248 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
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
271 static getStats () {
272 const webtorrentFilesQuery: FindOptions = {
273 include: [
274 {
275 attributes: [],
276 required: true,
277 model: VideoModel.unscoped(),
278 where: {
279 remote: false
280 }
281 }
282 ]
283 }
284
285 const hlsFilesQuery: FindOptions = {
286 include: [
287 {
288 attributes: [],
289 required: true,
290 model: VideoStreamingPlaylistModel.unscoped(),
291 include: [
292 {
293 attributes: [],
294 model: VideoModel.unscoped(),
295 required: true,
296 where: {
297 remote: false
298 }
299 }
300 ]
301 }
302 ]
303 }
304
305 return Promise.all([
306 VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery),
307 VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery)
308 ]).then(([ webtorrentResult, hlsResult ]) => ({
309 totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult)
310 }))
311 }
312
313 // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
314 static async customUpsert (
315 videoFile: MVideoFile,
316 mode: 'streaming-playlist' | 'video',
317 transaction: Transaction
318 ) {
319 const baseWhere = {
320 fps: videoFile.fps,
321 resolution: videoFile.resolution
322 }
323
324 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
325 else Object.assign(baseWhere, { videoId: videoFile.videoId })
326
327 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
328 if (!element) return videoFile.save({ transaction })
329
330 for (const k of Object.keys(videoFile.toJSON())) {
331 element[k] = videoFile[k]
332 }
333
334 return element.save({ transaction })
335 }
336
337 static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) {
338 const options = {
339 where: { videoStreamingPlaylistId }
340 }
341
342 return VideoFileModel.destroy(options)
343 }
344
345 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
346 if (this.videoId) return (this as MVideoFileVideo).Video
347
348 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
349 }
350
351 isAudio () {
352 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
353 }
354
355 isLive () {
356 return this.size === -1
357 }
358
359 isHLS () {
360 return !!this.videoStreamingPlaylistId
361 }
362
363 hasSameUniqueKeysThan (other: MVideoFile) {
364 return this.fps === other.fps &&
365 this.resolution === other.resolution &&
366 (
367 (this.videoId !== null && this.videoId === other.videoId) ||
368 (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
369 )
370 }
371 }