]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/redundancy/video-redundancy.ts
Remove duplicated videos on unfollow/delete redundancy
[github/Chocobozzz/PeerTube.git] / server / models / redundancy / video-redundancy.ts
1 import {
2 AfterDestroy,
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 ForeignKey,
9 Is,
10 Model,
11 Scopes,
12 Table,
13 UpdatedAt
14 } from 'sequelize-typescript'
15 import { ActorModel } from '../activitypub/actor'
16 import { getVideoSort, throwIfNotValid } from '../utils'
17 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18 import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
19 import { VideoFileModel } from '../video/video-file'
20 import { getServerActor } from '../../helpers/utils'
21 import { VideoModel } from '../video/video'
22 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
23 import { logger } from '../../helpers/logger'
24 import { CacheFileObject, VideoPrivacy } from '../../../shared'
25 import { VideoChannelModel } from '../video/video-channel'
26 import { ServerModel } from '../server/server'
27 import { sample } from 'lodash'
28 import { isTestInstance } from '../../helpers/core-utils'
29 import * as Bluebird from 'bluebird'
30 import * as Sequelize from 'sequelize'
31
32 export enum ScopeNames {
33 WITH_VIDEO = 'WITH_VIDEO'
34 }
35
36 @Scopes({
37 [ ScopeNames.WITH_VIDEO ]: {
38 include: [
39 {
40 model: () => VideoFileModel,
41 required: true,
42 include: [
43 {
44 model: () => VideoModel,
45 required: true
46 }
47 ]
48 }
49 ]
50 }
51 })
52
53 @Table({
54 tableName: 'videoRedundancy',
55 indexes: [
56 {
57 fields: [ 'videoFileId' ]
58 },
59 {
60 fields: [ 'actorId' ]
61 },
62 {
63 fields: [ 'url' ],
64 unique: true
65 }
66 ]
67 })
68 export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
69
70 @CreatedAt
71 createdAt: Date
72
73 @UpdatedAt
74 updatedAt: Date
75
76 @AllowNull(false)
77 @Column
78 expiresOn: Date
79
80 @AllowNull(false)
81 @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
82 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
83 fileUrl: string
84
85 @AllowNull(false)
86 @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
88 url: string
89
90 @AllowNull(true)
91 @Column
92 strategy: string // Only used by us
93
94 @ForeignKey(() => VideoFileModel)
95 @Column
96 videoFileId: number
97
98 @BelongsTo(() => VideoFileModel, {
99 foreignKey: {
100 allowNull: false
101 },
102 onDelete: 'cascade'
103 })
104 VideoFile: VideoFileModel
105
106 @ForeignKey(() => ActorModel)
107 @Column
108 actorId: number
109
110 @BelongsTo(() => ActorModel, {
111 foreignKey: {
112 allowNull: false
113 },
114 onDelete: 'cascade'
115 })
116 Actor: ActorModel
117
118 @AfterDestroy
119 static removeFile (instance: VideoRedundancyModel) {
120 // Not us
121 if (!instance.strategy) return
122
123 logger.info('Removing duplicated video file %s-%s.', instance.VideoFile.Video.uuid, instance.VideoFile.resolution)
124
125 return instance.VideoFile.Video.removeFile(instance.VideoFile)
126 }
127
128 static loadByFileId (videoFileId: number) {
129 const query = {
130 where: {
131 videoFileId
132 }
133 }
134
135 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
136 }
137
138 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
139 const query = {
140 where: {
141 url
142 },
143 transaction
144 }
145
146 return VideoRedundancyModel.findOne(query)
147 }
148
149 static async getVideoSample (p: Bluebird<VideoModel[]>) {
150 const rows = await p
151 const ids = rows.map(r => r.id)
152 const id = sample(ids)
153
154 return VideoModel.loadWithFile(id, undefined, !isTestInstance())
155 }
156
157 static async findMostViewToDuplicate (randomizedFactor: number) {
158 // On VideoModel!
159 const query = {
160 attributes: [ 'id', 'views' ],
161 limit: randomizedFactor,
162 order: getVideoSort('-views'),
163 where: {
164 privacy: VideoPrivacy.PUBLIC
165 },
166 include: [
167 await VideoRedundancyModel.buildVideoFileForDuplication(),
168 VideoRedundancyModel.buildServerRedundancyInclude()
169 ]
170 }
171
172 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
173 }
174
175 static async findTrendingToDuplicate (randomizedFactor: number) {
176 // On VideoModel!
177 const query = {
178 attributes: [ 'id', 'views' ],
179 subQuery: false,
180 group: 'VideoModel.id',
181 limit: randomizedFactor,
182 order: getVideoSort('-trending'),
183 where: {
184 privacy: VideoPrivacy.PUBLIC
185 },
186 include: [
187 await VideoRedundancyModel.buildVideoFileForDuplication(),
188 VideoRedundancyModel.buildServerRedundancyInclude(),
189
190 VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
191 ]
192 }
193
194 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
195 }
196
197 static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
198 // On VideoModel!
199 const query = {
200 attributes: [ 'id', 'publishedAt' ],
201 limit: randomizedFactor,
202 order: getVideoSort('-publishedAt'),
203 where: {
204 privacy: VideoPrivacy.PUBLIC,
205 views: {
206 [ Sequelize.Op.gte ]: minViews
207 }
208 },
209 include: [
210 await VideoRedundancyModel.buildVideoFileForDuplication(),
211 VideoRedundancyModel.buildServerRedundancyInclude()
212 ]
213 }
214
215 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
216 }
217
218 static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
219 const expiredDate = new Date()
220 expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
221
222 const actor = await getServerActor()
223
224 const query = {
225 where: {
226 actorId: actor.id,
227 strategy,
228 createdAt: {
229 [ Sequelize.Op.lt ]: expiredDate
230 }
231 }
232 }
233
234 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
235 }
236
237 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
238 const actor = await getServerActor()
239
240 const options = {
241 include: [
242 {
243 attributes: [],
244 model: VideoRedundancyModel,
245 required: true,
246 where: {
247 actorId: actor.id,
248 strategy
249 }
250 }
251 ]
252 }
253
254 return VideoFileModel.sum('size', options as any) // FIXME: typings
255 }
256
257 static async listLocalExpired () {
258 const actor = await getServerActor()
259
260 const query = {
261 where: {
262 actorId: actor.id,
263 expiresOn: {
264 [ Sequelize.Op.lt ]: new Date()
265 }
266 }
267 }
268
269 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
270 }
271
272 static async listRemoteExpired () {
273 const actor = await getServerActor()
274
275 const query = {
276 where: {
277 actorId: {
278 [Sequelize.Op.ne]: actor.id
279 },
280 expiresOn: {
281 [ Sequelize.Op.lt ]: new Date()
282 }
283 }
284 }
285
286 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
287 }
288
289 static async listLocalOfServer (serverId: number) {
290 const actor = await getServerActor()
291
292 const query = {
293 where: {
294 actorId: actor.id
295 },
296 include: [
297 {
298 model: VideoFileModel,
299 required: true,
300 include: [
301 {
302 model: VideoModel,
303 required: true,
304 include: [
305 {
306 attributes: [],
307 model: VideoChannelModel.unscoped(),
308 required: true,
309 include: [
310 {
311 attributes: [],
312 model: ActorModel.unscoped(),
313 required: true,
314 where: {
315 serverId
316 }
317 }
318 ]
319 }
320 ]
321 }
322 ]
323 }
324 ]
325 }
326
327 return VideoRedundancyModel.findAll(query)
328 }
329
330 static async getStats (strategy: VideoRedundancyStrategy) {
331 const actor = await getServerActor()
332
333 const query = {
334 raw: true,
335 attributes: [
336 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
337 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
338 [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
339 ],
340 where: {
341 strategy,
342 actorId: actor.id
343 },
344 include: [
345 {
346 attributes: [],
347 model: VideoFileModel,
348 required: true
349 }
350 ]
351 }
352
353 return VideoRedundancyModel.find(query as any) // FIXME: typings
354 .then((r: any) => ({
355 totalUsed: parseInt(r.totalUsed.toString(), 10),
356 totalVideos: r.totalVideos,
357 totalVideoFiles: r.totalVideoFiles
358 }))
359 }
360
361 toActivityPubObject (): CacheFileObject {
362 return {
363 id: this.url,
364 type: 'CacheFile' as 'CacheFile',
365 object: this.VideoFile.Video.url,
366 expires: this.expiresOn.toISOString(),
367 url: {
368 type: 'Link',
369 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
370 href: this.fileUrl,
371 height: this.VideoFile.resolution,
372 size: this.VideoFile.size,
373 fps: this.VideoFile.fps
374 }
375 }
376 }
377
378 // Don't include video files we already duplicated
379 private static async buildVideoFileForDuplication () {
380 const actor = await getServerActor()
381
382 const notIn = Sequelize.literal(
383 '(' +
384 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` +
385 ')'
386 )
387
388 return {
389 attributes: [],
390 model: VideoFileModel.unscoped(),
391 required: true,
392 where: {
393 id: {
394 [ Sequelize.Op.notIn ]: notIn
395 }
396 }
397 }
398 }
399
400 private static buildServerRedundancyInclude () {
401 return {
402 attributes: [],
403 model: VideoChannelModel.unscoped(),
404 required: true,
405 include: [
406 {
407 attributes: [],
408 model: ActorModel.unscoped(),
409 required: true,
410 include: [
411 {
412 attributes: [],
413 model: ServerModel.unscoped(),
414 required: true,
415 where: {
416 redundancyAllowed: true
417 }
418 }
419 ]
420 }
421 ]
422 }
423 }
424 }