diff options
Diffstat (limited to 'server/models/redundancy')
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts new file mode 100644 index 000000000..48ec77206 --- /dev/null +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -0,0 +1,249 @@ | |||
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 | } | ||