]>
Commit | Line | Data |
---|---|---|
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 | } |