]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-file.ts
Add infohash cache
[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 } from 'sequelize-typescript'
15 import {
16 isVideoFileExtnameValid,
17 isVideoFileInfoHashValid,
18 isVideoFileResolutionValid,
19 isVideoFileSizeValid,
20 isVideoFPSResolutionValid
21 } from '../../helpers/custom-validators/videos'
22 import { parseAggregateResult, throwIfNotValid } from '../utils'
23 import { VideoModel } from './video'
24 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
25 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26 import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
27 import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
28 import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
29 import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
30 import * as memoizee from 'memoizee'
31
32 @Table({
33 tableName: 'videoFile',
34 indexes: [
35 {
36 fields: [ 'videoId' ],
37 where: {
38 videoId: {
39 [Op.ne]: null
40 }
41 }
42 },
43 {
44 fields: [ 'videoStreamingPlaylistId' ],
45 where: {
46 videoStreamingPlaylistId: {
47 [Op.ne]: null
48 }
49 }
50 },
51
52 {
53 fields: [ 'infoHash' ]
54 },
55
56 {
57 fields: [ 'videoId', 'resolution', 'fps' ],
58 unique: true,
59 where: {
60 videoId: {
61 [Op.ne]: null
62 }
63 }
64 },
65 {
66 fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
67 unique: true,
68 where: {
69 videoStreamingPlaylistId: {
70 [Op.ne]: null
71 }
72 }
73 }
74 ]
75 })
76 export class VideoFileModel extends Model<VideoFileModel> {
77 @CreatedAt
78 createdAt: Date
79
80 @UpdatedAt
81 updatedAt: Date
82
83 @AllowNull(false)
84 @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
85 @Column
86 resolution: number
87
88 @AllowNull(false)
89 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
90 @Column(DataType.BIGINT)
91 size: number
92
93 @AllowNull(false)
94 @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
95 @Column
96 extname: string
97
98 @AllowNull(false)
99 @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
100 @Column
101 infoHash: string
102
103 @AllowNull(false)
104 @Default(-1)
105 @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
106 @Column
107 fps: number
108
109 @ForeignKey(() => VideoModel)
110 @Column
111 videoId: number
112
113 @BelongsTo(() => VideoModel, {
114 foreignKey: {
115 allowNull: true
116 },
117 onDelete: 'CASCADE'
118 })
119 Video: VideoModel
120
121 @ForeignKey(() => VideoStreamingPlaylistModel)
122 @Column
123 videoStreamingPlaylistId: number
124
125 @BelongsTo(() => VideoStreamingPlaylistModel, {
126 foreignKey: {
127 allowNull: true
128 },
129 onDelete: 'CASCADE'
130 })
131 VideoStreamingPlaylist: VideoStreamingPlaylistModel
132
133 @HasMany(() => VideoRedundancyModel, {
134 foreignKey: {
135 allowNull: true
136 },
137 onDelete: 'CASCADE',
138 hooks: true
139 })
140 RedundancyVideos: VideoRedundancyModel[]
141
142 static doesInfohashExistCached = memoizee(VideoFileModel.doesInfohashExist, {
143 promise: true,
144 max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
145 maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
146 })
147
148 static doesInfohashExist (infoHash: string) {
149 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
150 const options = {
151 type: QueryTypes.SELECT as QueryTypes.SELECT,
152 bind: { infoHash },
153 raw: true
154 }
155
156 return VideoModel.sequelize.query(query, options)
157 .then(results => results.length === 1)
158 }
159
160 static loadWithVideo (id: number) {
161 const options = {
162 include: [
163 {
164 model: VideoModel.unscoped(),
165 required: true
166 }
167 ]
168 }
169
170 return VideoFileModel.findByPk(id, options)
171 }
172
173 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
174 const query = {
175 include: [
176 {
177 model: VideoModel.unscoped(),
178 required: true,
179 include: [
180 {
181 model: VideoStreamingPlaylistModel.unscoped(),
182 required: true,
183 where: {
184 id: streamingPlaylistId
185 }
186 }
187 ]
188 }
189 ],
190 transaction
191 }
192
193 return VideoFileModel.findAll(query)
194 }
195
196 static getStats () {
197 const query: FindOptions = {
198 include: [
199 {
200 attributes: [],
201 model: VideoModel.unscoped(),
202 where: {
203 remote: false
204 }
205 }
206 ]
207 }
208
209 return VideoFileModel.aggregate('size', 'SUM', query)
210 .then(result => ({
211 totalLocalVideoFilesSize: parseAggregateResult(result)
212 }))
213 }
214
215 // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
216 static async customUpsert (
217 videoFile: MVideoFile,
218 mode: 'streaming-playlist' | 'video',
219 transaction: Transaction
220 ) {
221 const baseWhere = {
222 fps: videoFile.fps,
223 resolution: videoFile.resolution
224 }
225
226 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
227 else Object.assign(baseWhere, { videoId: videoFile.videoId })
228
229 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
230 if (!element) return videoFile.save({ transaction })
231
232 for (const k of Object.keys(videoFile.toJSON())) {
233 element[k] = videoFile[k]
234 }
235
236 return element.save({ transaction })
237 }
238
239 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
240 if (this.videoId) return (this as MVideoFileVideo).Video
241
242 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
243 }
244
245 isAudio () {
246 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
247 }
248
249 hasSameUniqueKeysThan (other: MVideoFile) {
250 return this.fps === other.fps &&
251 this.resolution === other.resolution &&
252 (
253 (this.videoId !== null && this.videoId === other.videoId) ||
254 (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
255 )
256 }
257 }