]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/redundancy/video-redundancy.ts
Basic video redundancy implementation
[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 Sequelize,
13 Table,
14 UpdatedAt
15 } from 'sequelize-typescript'
16 import { ActorModel } from '../activitypub/actor'
17 import { throwIfNotValid } from '../utils'
18 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
19 import { CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
20 import { VideoFileModel } from '../video/video-file'
21 import { isDateValid } from '../../helpers/custom-validators/misc'
22 import { getServerActor } from '../../helpers/utils'
23 import { VideoModel } from '../video/video'
24 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
25 import { logger } from '../../helpers/logger'
26 import { CacheFileObject } from '../../../shared'
27 import { VideoChannelModel } from '../video/video-channel'
28 import { ServerModel } from '../server/server'
29 import { sample } from 'lodash'
30 import { isTestInstance } from '../../helpers/core-utils'
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 removeFilesAndSendDelete (instance: VideoRedundancyModel) {
120 // Not us
121 if (!instance.strategy) return
122
123 logger.info('Removing video file %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) {
139 const query = {
140 where: {
141 url
142 }
143 }
144
145 return VideoRedundancyModel.findOne(query)
146 }
147
148 static async findMostViewToDuplicate (randomizedFactor: number) {
149 // On VideoModel!
150 const query = {
151 logging: !isTestInstance(),
152 limit: randomizedFactor,
153 order: [ [ 'views', 'DESC' ] ],
154 include: [
155 {
156 model: VideoFileModel.unscoped(),
157 required: true,
158 where: {
159 id: {
160 [ Sequelize.Op.notIn ]: await VideoRedundancyModel.buildExcludeIn()
161 }
162 }
163 },
164 {
165 attributes: [],
166 model: VideoChannelModel.unscoped(),
167 required: true,
168 include: [
169 {
170 attributes: [],
171 model: ActorModel.unscoped(),
172 required: true,
173 include: [
174 {
175 attributes: [],
176 model: ServerModel.unscoped(),
177 required: true,
178 where: {
179 redundancyAllowed: true
180 }
181 }
182 ]
183 }
184 ]
185 }
186 ]
187 }
188
189 const rows = await VideoModel.unscoped().findAll(query)
190
191 return sample(rows)
192 }
193
194 static async getVideoFiles (strategy: VideoRedundancyStrategy) {
195 const actor = await getServerActor()
196
197 const queryVideoFiles = {
198 logging: !isTestInstance(),
199 where: {
200 actorId: actor.id,
201 strategy
202 }
203 }
204
205 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
206 .findAll(queryVideoFiles)
207 }
208
209 static listAllExpired () {
210 const query = {
211 logging: !isTestInstance(),
212 where: {
213 expiresOn: {
214 [Sequelize.Op.lt]: new Date()
215 }
216 }
217 }
218
219 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
220 .findAll(query)
221 }
222
223 toActivityPubObject (): CacheFileObject {
224 return {
225 id: this.url,
226 type: 'CacheFile' as 'CacheFile',
227 object: this.VideoFile.Video.url,
228 expires: this.expiresOn.toISOString(),
229 url: {
230 type: 'Link',
231 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
232 href: this.fileUrl,
233 height: this.VideoFile.resolution,
234 size: this.VideoFile.size,
235 fps: this.VideoFile.fps
236 }
237 }
238 }
239
240 private static async buildExcludeIn () {
241 const actor = await getServerActor()
242
243 return Sequelize.literal(
244 '(' +
245 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` +
246 ')'
247 )
248 }
249 }