diff options
author | Chocobozzz <me@florianbigard.com> | 2023-04-21 14:55:10 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2023-05-09 08:57:34 +0200 |
commit | 0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch) | |
tree | 226d3dd1565b0bb56588897af3b8530e6216e96b /server/models | |
parent | 6bcb854cdea8688a32240bc5719c7d139806e00b (diff) | |
download | PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.gz PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.zst PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.zip |
Implement remote runner jobs in server
Move ffmpeg functions to @shared
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/runner/runner-job.ts | 347 | ||||
-rw-r--r-- | server/models/runner/runner-registration-token.ts | 103 | ||||
-rw-r--r-- | server/models/runner/runner.ts | 112 | ||||
-rw-r--r-- | server/models/shared/update.ts | 28 | ||||
-rw-r--r-- | server/models/video/video-job-info.ts | 16 | ||||
-rw-r--r-- | server/models/video/video-live-session.ts | 13 | ||||
-rw-r--r-- | server/models/video/video.ts | 10 |
7 files changed, 607 insertions, 22 deletions
diff --git a/server/models/runner/runner-job.ts b/server/models/runner/runner-job.ts new file mode 100644 index 000000000..add6f9a43 --- /dev/null +++ b/server/models/runner/runner-job.ts | |||
@@ -0,0 +1,347 @@ | |||
1 | import { FindOptions, Op, Transaction } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | IsUUID, | ||
11 | Model, | ||
12 | Scopes, | ||
13 | Table, | ||
14 | UpdatedAt | ||
15 | } from 'sequelize-typescript' | ||
16 | import { isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
17 | import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants' | ||
18 | import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners' | ||
19 | import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models' | ||
20 | import { AttributesOnly } from '@shared/typescript-utils' | ||
21 | import { getSort, searchAttribute } from '../shared' | ||
22 | import { RunnerModel } from './runner' | ||
23 | |||
24 | enum ScopeNames { | ||
25 | WITH_RUNNER = 'WITH_RUNNER', | ||
26 | WITH_PARENT = 'WITH_PARENT' | ||
27 | } | ||
28 | |||
29 | @Scopes(() => ({ | ||
30 | [ScopeNames.WITH_RUNNER]: { | ||
31 | include: [ | ||
32 | { | ||
33 | model: RunnerModel.unscoped(), | ||
34 | required: false | ||
35 | } | ||
36 | ] | ||
37 | }, | ||
38 | [ScopeNames.WITH_PARENT]: { | ||
39 | include: [ | ||
40 | { | ||
41 | model: RunnerJobModel.unscoped(), | ||
42 | required: false | ||
43 | } | ||
44 | ] | ||
45 | } | ||
46 | })) | ||
47 | @Table({ | ||
48 | tableName: 'runnerJob', | ||
49 | indexes: [ | ||
50 | { | ||
51 | fields: [ 'uuid' ], | ||
52 | unique: true | ||
53 | }, | ||
54 | { | ||
55 | fields: [ 'processingJobToken' ], | ||
56 | unique: true | ||
57 | }, | ||
58 | { | ||
59 | fields: [ 'runnerId' ] | ||
60 | } | ||
61 | ] | ||
62 | }) | ||
63 | export class RunnerJobModel extends Model<Partial<AttributesOnly<RunnerJobModel>>> { | ||
64 | |||
65 | @AllowNull(false) | ||
66 | @IsUUID(4) | ||
67 | @Column(DataType.UUID) | ||
68 | uuid: string | ||
69 | |||
70 | @AllowNull(false) | ||
71 | @Column | ||
72 | type: RunnerJobType | ||
73 | |||
74 | @AllowNull(false) | ||
75 | @Column(DataType.JSONB) | ||
76 | payload: RunnerJobPayload | ||
77 | |||
78 | @AllowNull(false) | ||
79 | @Column(DataType.JSONB) | ||
80 | privatePayload: RunnerJobPrivatePayload | ||
81 | |||
82 | @AllowNull(false) | ||
83 | @Column | ||
84 | state: RunnerJobState | ||
85 | |||
86 | @AllowNull(false) | ||
87 | @Default(0) | ||
88 | @Column | ||
89 | failures: number | ||
90 | |||
91 | @AllowNull(true) | ||
92 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.RUNNER_JOBS.ERROR_MESSAGE.max)) | ||
93 | error: string | ||
94 | |||
95 | // Less has priority | ||
96 | @AllowNull(false) | ||
97 | @Column | ||
98 | priority: number | ||
99 | |||
100 | // Used to fetch the appropriate job when the runner wants to post the result | ||
101 | @AllowNull(true) | ||
102 | @Column | ||
103 | processingJobToken: string | ||
104 | |||
105 | @AllowNull(true) | ||
106 | @Column | ||
107 | progress: number | ||
108 | |||
109 | @AllowNull(true) | ||
110 | @Column | ||
111 | startedAt: Date | ||
112 | |||
113 | @AllowNull(true) | ||
114 | @Column | ||
115 | finishedAt: Date | ||
116 | |||
117 | @CreatedAt | ||
118 | createdAt: Date | ||
119 | |||
120 | @UpdatedAt | ||
121 | updatedAt: Date | ||
122 | |||
123 | @ForeignKey(() => RunnerJobModel) | ||
124 | @Column | ||
125 | dependsOnRunnerJobId: number | ||
126 | |||
127 | @BelongsTo(() => RunnerJobModel, { | ||
128 | foreignKey: { | ||
129 | name: 'dependsOnRunnerJobId', | ||
130 | allowNull: true | ||
131 | }, | ||
132 | onDelete: 'cascade' | ||
133 | }) | ||
134 | DependsOnRunnerJob: RunnerJobModel | ||
135 | |||
136 | @ForeignKey(() => RunnerModel) | ||
137 | @Column | ||
138 | runnerId: number | ||
139 | |||
140 | @BelongsTo(() => RunnerModel, { | ||
141 | foreignKey: { | ||
142 | name: 'runnerId', | ||
143 | allowNull: true | ||
144 | }, | ||
145 | onDelete: 'SET NULL' | ||
146 | }) | ||
147 | Runner: RunnerModel | ||
148 | |||
149 | // --------------------------------------------------------------------------- | ||
150 | |||
151 | static loadWithRunner (uuid: string) { | ||
152 | const query = { | ||
153 | where: { uuid } | ||
154 | } | ||
155 | |||
156 | return RunnerJobModel.scope(ScopeNames.WITH_RUNNER).findOne<MRunnerJobRunner>(query) | ||
157 | } | ||
158 | |||
159 | static loadByRunnerAndJobTokensWithRunner (options: { | ||
160 | uuid: string | ||
161 | runnerToken: string | ||
162 | jobToken: string | ||
163 | }) { | ||
164 | const { uuid, runnerToken, jobToken } = options | ||
165 | |||
166 | const query = { | ||
167 | where: { | ||
168 | uuid, | ||
169 | processingJobToken: jobToken | ||
170 | }, | ||
171 | include: { | ||
172 | model: RunnerModel.unscoped(), | ||
173 | required: true, | ||
174 | where: { | ||
175 | runnerToken | ||
176 | } | ||
177 | } | ||
178 | } | ||
179 | |||
180 | return RunnerJobModel.findOne<MRunnerJobRunner>(query) | ||
181 | } | ||
182 | |||
183 | static listAvailableJobs () { | ||
184 | const query = { | ||
185 | limit: 10, | ||
186 | order: getSort('priority'), | ||
187 | where: { | ||
188 | state: RunnerJobState.PENDING | ||
189 | } | ||
190 | } | ||
191 | |||
192 | return RunnerJobModel.findAll<MRunnerJob>(query) | ||
193 | } | ||
194 | |||
195 | static listStalledJobs (options: { | ||
196 | staleTimeMS: number | ||
197 | types: RunnerJobType[] | ||
198 | }) { | ||
199 | const before = new Date(Date.now() - options.staleTimeMS) | ||
200 | |||
201 | return RunnerJobModel.findAll<MRunnerJob>({ | ||
202 | where: { | ||
203 | type: { | ||
204 | [Op.in]: options.types | ||
205 | }, | ||
206 | state: RunnerJobState.PROCESSING, | ||
207 | updatedAt: { | ||
208 | [Op.lt]: before | ||
209 | } | ||
210 | } | ||
211 | }) | ||
212 | } | ||
213 | |||
214 | static listChildrenOf (job: MRunnerJob, transaction?: Transaction) { | ||
215 | const query = { | ||
216 | where: { | ||
217 | dependsOnRunnerJobId: job.id | ||
218 | }, | ||
219 | transaction | ||
220 | } | ||
221 | |||
222 | return RunnerJobModel.findAll<MRunnerJob>(query) | ||
223 | } | ||
224 | |||
225 | static listForApi (options: { | ||
226 | start: number | ||
227 | count: number | ||
228 | sort: string | ||
229 | search?: string | ||
230 | }) { | ||
231 | const { start, count, sort, search } = options | ||
232 | |||
233 | const query: FindOptions = { | ||
234 | offset: start, | ||
235 | limit: count, | ||
236 | order: getSort(sort) | ||
237 | } | ||
238 | |||
239 | if (search) { | ||
240 | if (isUUIDValid(search)) { | ||
241 | query.where = { uuid: search } | ||
242 | } else { | ||
243 | query.where = { | ||
244 | [Op.or]: [ | ||
245 | searchAttribute(search, 'type'), | ||
246 | searchAttribute(search, '$Runner.name$') | ||
247 | ] | ||
248 | } | ||
249 | } | ||
250 | } | ||
251 | |||
252 | return Promise.all([ | ||
253 | RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query), | ||
254 | RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query) | ||
255 | ]).then(([ total, data ]) => ({ total, data })) | ||
256 | } | ||
257 | |||
258 | static updateDependantJobsOf (runnerJob: MRunnerJob) { | ||
259 | const where = { | ||
260 | dependsOnRunnerJobId: runnerJob.id | ||
261 | } | ||
262 | |||
263 | return RunnerJobModel.update({ state: RunnerJobState.PENDING }, { where }) | ||
264 | } | ||
265 | |||
266 | static cancelAllJobs (options: { type: RunnerJobType }) { | ||
267 | const where = { | ||
268 | type: options.type | ||
269 | } | ||
270 | |||
271 | return RunnerJobModel.update({ state: RunnerJobState.CANCELLED }, { where }) | ||
272 | } | ||
273 | |||
274 | // --------------------------------------------------------------------------- | ||
275 | |||
276 | resetToPending () { | ||
277 | this.state = RunnerJobState.PENDING | ||
278 | this.processingJobToken = null | ||
279 | this.progress = null | ||
280 | this.startedAt = null | ||
281 | this.runnerId = null | ||
282 | } | ||
283 | |||
284 | setToErrorOrCancel ( | ||
285 | state: RunnerJobState.PARENT_ERRORED | RunnerJobState.ERRORED | RunnerJobState.CANCELLED | RunnerJobState.PARENT_CANCELLED | ||
286 | ) { | ||
287 | this.state = state | ||
288 | this.processingJobToken = null | ||
289 | this.finishedAt = new Date() | ||
290 | } | ||
291 | |||
292 | toFormattedJSON (this: MRunnerJobRunnerParent): RunnerJob { | ||
293 | const runner = this.Runner | ||
294 | ? { | ||
295 | id: this.Runner.id, | ||
296 | name: this.Runner.name, | ||
297 | description: this.Runner.description | ||
298 | } | ||
299 | : null | ||
300 | |||
301 | const parent = this.DependsOnRunnerJob | ||
302 | ? { | ||
303 | id: this.DependsOnRunnerJob.id, | ||
304 | uuid: this.DependsOnRunnerJob.uuid, | ||
305 | type: this.DependsOnRunnerJob.type, | ||
306 | state: { | ||
307 | id: this.DependsOnRunnerJob.state, | ||
308 | label: RUNNER_JOB_STATES[this.DependsOnRunnerJob.state] | ||
309 | } | ||
310 | } | ||
311 | : undefined | ||
312 | |||
313 | return { | ||
314 | uuid: this.uuid, | ||
315 | type: this.type, | ||
316 | |||
317 | state: { | ||
318 | id: this.state, | ||
319 | label: RUNNER_JOB_STATES[this.state] | ||
320 | }, | ||
321 | |||
322 | progress: this.progress, | ||
323 | priority: this.priority, | ||
324 | failures: this.failures, | ||
325 | error: this.error, | ||
326 | |||
327 | payload: this.payload, | ||
328 | |||
329 | startedAt: this.startedAt?.toISOString(), | ||
330 | finishedAt: this.finishedAt?.toISOString(), | ||
331 | |||
332 | createdAt: this.createdAt.toISOString(), | ||
333 | updatedAt: this.updatedAt.toISOString(), | ||
334 | |||
335 | parent, | ||
336 | runner | ||
337 | } | ||
338 | } | ||
339 | |||
340 | toFormattedAdminJSON (this: MRunnerJobRunnerParent): RunnerJobAdmin { | ||
341 | return { | ||
342 | ...this.toFormattedJSON(), | ||
343 | |||
344 | privatePayload: this.privatePayload | ||
345 | } | ||
346 | } | ||
347 | } | ||
diff --git a/server/models/runner/runner-registration-token.ts b/server/models/runner/runner-registration-token.ts new file mode 100644 index 000000000..b2ae6c9eb --- /dev/null +++ b/server/models/runner/runner-registration-token.ts | |||
@@ -0,0 +1,103 @@ | |||
1 | import { FindOptions, literal } from 'sequelize' | ||
2 | import { AllowNull, Column, CreatedAt, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { MRunnerRegistrationToken } from '@server/types/models/runners' | ||
4 | import { RunnerRegistrationToken } from '@shared/models' | ||
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { getSort } from '../shared' | ||
7 | import { RunnerModel } from './runner' | ||
8 | |||
9 | /** | ||
10 | * | ||
11 | * Tokens used by PeerTube runners to register themselves to the PeerTube instance | ||
12 | * | ||
13 | */ | ||
14 | |||
15 | @Table({ | ||
16 | tableName: 'runnerRegistrationToken', | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'registrationToken' ], | ||
20 | unique: true | ||
21 | } | ||
22 | ] | ||
23 | }) | ||
24 | export class RunnerRegistrationTokenModel extends Model<Partial<AttributesOnly<RunnerRegistrationTokenModel>>> { | ||
25 | |||
26 | @AllowNull(false) | ||
27 | @Column | ||
28 | registrationToken: string | ||
29 | |||
30 | @CreatedAt | ||
31 | createdAt: Date | ||
32 | |||
33 | @UpdatedAt | ||
34 | updatedAt: Date | ||
35 | |||
36 | @HasMany(() => RunnerModel, { | ||
37 | foreignKey: { | ||
38 | allowNull: true | ||
39 | }, | ||
40 | onDelete: 'cascade' | ||
41 | }) | ||
42 | Runners: RunnerModel[] | ||
43 | |||
44 | static load (id: number) { | ||
45 | return RunnerRegistrationTokenModel.findByPk(id) | ||
46 | } | ||
47 | |||
48 | static loadByRegistrationToken (registrationToken: string) { | ||
49 | const query = { | ||
50 | where: { registrationToken } | ||
51 | } | ||
52 | |||
53 | return RunnerRegistrationTokenModel.findOne(query) | ||
54 | } | ||
55 | |||
56 | static countTotal () { | ||
57 | return RunnerRegistrationTokenModel.unscoped().count() | ||
58 | } | ||
59 | |||
60 | static listForApi (options: { | ||
61 | start: number | ||
62 | count: number | ||
63 | sort: string | ||
64 | }) { | ||
65 | const { start, count, sort } = options | ||
66 | |||
67 | const query: FindOptions = { | ||
68 | attributes: { | ||
69 | include: [ | ||
70 | [ | ||
71 | literal('(SELECT COUNT(*) FROM "runner" WHERE "runner"."runnerRegistrationTokenId" = "RunnerRegistrationTokenModel"."id")'), | ||
72 | 'registeredRunnersCount' | ||
73 | ] | ||
74 | ] | ||
75 | }, | ||
76 | offset: start, | ||
77 | limit: count, | ||
78 | order: getSort(sort) | ||
79 | } | ||
80 | |||
81 | return Promise.all([ | ||
82 | RunnerRegistrationTokenModel.count(query), | ||
83 | RunnerRegistrationTokenModel.findAll<MRunnerRegistrationToken>(query) | ||
84 | ]).then(([ total, data ]) => ({ total, data })) | ||
85 | } | ||
86 | |||
87 | // --------------------------------------------------------------------------- | ||
88 | |||
89 | toFormattedJSON (this: MRunnerRegistrationToken): RunnerRegistrationToken { | ||
90 | const registeredRunnersCount = this.get('registeredRunnersCount') as number | ||
91 | |||
92 | return { | ||
93 | id: this.id, | ||
94 | |||
95 | registrationToken: this.registrationToken, | ||
96 | |||
97 | createdAt: this.createdAt, | ||
98 | updatedAt: this.updatedAt, | ||
99 | |||
100 | registeredRunnersCount | ||
101 | } | ||
102 | } | ||
103 | } | ||
diff --git a/server/models/runner/runner.ts b/server/models/runner/runner.ts new file mode 100644 index 000000000..1ef0018b4 --- /dev/null +++ b/server/models/runner/runner.ts | |||
@@ -0,0 +1,112 @@ | |||
1 | import { FindOptions } from 'sequelize' | ||
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { MRunner } from '@server/types/models/runners' | ||
4 | import { Runner } from '@shared/models' | ||
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { getSort } from '../shared' | ||
7 | import { RunnerRegistrationTokenModel } from './runner-registration-token' | ||
8 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
9 | |||
10 | @Table({ | ||
11 | tableName: 'runner', | ||
12 | indexes: [ | ||
13 | { | ||
14 | fields: [ 'runnerToken' ], | ||
15 | unique: true | ||
16 | }, | ||
17 | { | ||
18 | fields: [ 'runnerRegistrationTokenId' ] | ||
19 | } | ||
20 | ] | ||
21 | }) | ||
22 | export class RunnerModel extends Model<Partial<AttributesOnly<RunnerModel>>> { | ||
23 | |||
24 | // Used to identify the appropriate runner when it uses the runner REST API | ||
25 | @AllowNull(false) | ||
26 | @Column | ||
27 | runnerToken: string | ||
28 | |||
29 | @AllowNull(false) | ||
30 | @Column | ||
31 | name: string | ||
32 | |||
33 | @AllowNull(true) | ||
34 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.RUNNERS.DESCRIPTION.max)) | ||
35 | description: string | ||
36 | |||
37 | @AllowNull(false) | ||
38 | @Column | ||
39 | lastContact: Date | ||
40 | |||
41 | @AllowNull(false) | ||
42 | @Column | ||
43 | ip: string | ||
44 | |||
45 | @CreatedAt | ||
46 | createdAt: Date | ||
47 | |||
48 | @UpdatedAt | ||
49 | updatedAt: Date | ||
50 | |||
51 | @ForeignKey(() => RunnerRegistrationTokenModel) | ||
52 | @Column | ||
53 | runnerRegistrationTokenId: number | ||
54 | |||
55 | @BelongsTo(() => RunnerRegistrationTokenModel, { | ||
56 | foreignKey: { | ||
57 | allowNull: false | ||
58 | }, | ||
59 | onDelete: 'cascade' | ||
60 | }) | ||
61 | RunnerRegistrationToken: RunnerRegistrationTokenModel | ||
62 | |||
63 | // --------------------------------------------------------------------------- | ||
64 | |||
65 | static load (id: number) { | ||
66 | return RunnerModel.findByPk(id) | ||
67 | } | ||
68 | |||
69 | static loadByToken (runnerToken: string) { | ||
70 | const query = { | ||
71 | where: { runnerToken } | ||
72 | } | ||
73 | |||
74 | return RunnerModel.findOne(query) | ||
75 | } | ||
76 | |||
77 | static listForApi (options: { | ||
78 | start: number | ||
79 | count: number | ||
80 | sort: string | ||
81 | }) { | ||
82 | const { start, count, sort } = options | ||
83 | |||
84 | const query: FindOptions = { | ||
85 | offset: start, | ||
86 | limit: count, | ||
87 | order: getSort(sort) | ||
88 | } | ||
89 | |||
90 | return Promise.all([ | ||
91 | RunnerModel.count(query), | ||
92 | RunnerModel.findAll<MRunner>(query) | ||
93 | ]).then(([ total, data ]) => ({ total, data })) | ||
94 | } | ||
95 | |||
96 | // --------------------------------------------------------------------------- | ||
97 | |||
98 | toFormattedJSON (this: MRunner): Runner { | ||
99 | return { | ||
100 | id: this.id, | ||
101 | |||
102 | name: this.name, | ||
103 | description: this.description, | ||
104 | |||
105 | ip: this.ip, | ||
106 | lastContact: this.lastContact, | ||
107 | |||
108 | createdAt: this.createdAt, | ||
109 | updatedAt: this.updatedAt | ||
110 | } | ||
111 | } | ||
112 | } | ||
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts index d02c4535d..96db43730 100644 --- a/server/models/shared/update.ts +++ b/server/models/shared/update.ts | |||
@@ -1,22 +1,32 @@ | |||
1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' | 1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | 2 | ||
3 | const updating = new Set<string>() | ||
4 | |||
3 | // Sequelize always skip the update if we only update updatedAt field | 5 | // Sequelize always skip the update if we only update updatedAt field |
4 | function setAsUpdated (options: { | 6 | async function setAsUpdated (options: { |
5 | sequelize: Sequelize | 7 | sequelize: Sequelize |
6 | table: string | 8 | table: string |
7 | id: number | 9 | id: number |
8 | transaction?: Transaction | 10 | transaction?: Transaction |
9 | }) { | 11 | }) { |
10 | const { sequelize, table, id, transaction } = options | 12 | const { sequelize, table, id, transaction } = options |
13 | const key = table + '-' + id | ||
14 | |||
15 | if (updating.has(key)) return | ||
16 | updating.add(key) | ||
11 | 17 | ||
12 | return sequelize.query( | 18 | try { |
13 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | 19 | await sequelize.query( |
14 | { | 20 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, |
15 | replacements: { table, id, updatedAt: new Date() }, | 21 | { |
16 | type: QueryTypes.UPDATE, | 22 | replacements: { table, id, updatedAt: new Date() }, |
17 | transaction | 23 | type: QueryTypes.UPDATE, |
18 | } | 24 | transaction |
19 | ) | 25 | } |
26 | ) | ||
27 | } finally { | ||
28 | updating.delete(key) | ||
29 | } | ||
20 | } | 30 | } |
21 | 31 | ||
22 | export { | 32 | export { |
diff --git a/server/models/video/video-job-info.ts b/server/models/video/video-job-info.ts index 740f6b5c6..5845b8c74 100644 --- a/server/models/video/video-job-info.ts +++ b/server/models/video/video-job-info.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Op, QueryTypes, Transaction } from 'sequelize' | 1 | import { Op, QueryTypes, Transaction } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Model, Table, Unique, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Model, Table, Unique, UpdatedAt } from 'sequelize-typescript' |
3 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
4 | import { VideoModel } from './video' | 5 | import { VideoModel } from './video' |
5 | 6 | ||
@@ -59,32 +60,33 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo | |||
59 | return VideoJobInfoModel.findOne({ where, transaction }) | 60 | return VideoJobInfoModel.findOne({ where, transaction }) |
60 | } | 61 | } |
61 | 62 | ||
62 | static async increaseOrCreate (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> { | 63 | static async increaseOrCreate (videoUUID: string, column: VideoJobInfoColumnType, amountArg = 1): Promise<number> { |
63 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } } | 64 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } } |
65 | const amount = forceNumber(amountArg) | ||
64 | 66 | ||
65 | const [ { pendingMove } ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(` | 67 | const [ result ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(` |
66 | INSERT INTO "videoJobInfo" ("videoId", "${column}", "createdAt", "updatedAt") | 68 | INSERT INTO "videoJobInfo" ("videoId", "${column}", "createdAt", "updatedAt") |
67 | SELECT | 69 | SELECT |
68 | "video"."id" AS "videoId", 1, NOW(), NOW() | 70 | "video"."id" AS "videoId", ${amount}, NOW(), NOW() |
69 | FROM | 71 | FROM |
70 | "video" | 72 | "video" |
71 | WHERE | 73 | WHERE |
72 | "video"."uuid" = $videoUUID | 74 | "video"."uuid" = $videoUUID |
73 | ON CONFLICT ("videoId") DO UPDATE | 75 | ON CONFLICT ("videoId") DO UPDATE |
74 | SET | 76 | SET |
75 | "${column}" = "videoJobInfo"."${column}" + 1, | 77 | "${column}" = "videoJobInfo"."${column}" + ${amount}, |
76 | "updatedAt" = NOW() | 78 | "updatedAt" = NOW() |
77 | RETURNING | 79 | RETURNING |
78 | "${column}" | 80 | "${column}" |
79 | `, options) | 81 | `, options) |
80 | 82 | ||
81 | return pendingMove | 83 | return result[column] |
82 | } | 84 | } |
83 | 85 | ||
84 | static async decrease (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> { | 86 | static async decrease (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> { |
85 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } } | 87 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } } |
86 | 88 | ||
87 | const result = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(` | 89 | const result = await VideoJobInfoModel.sequelize.query(` |
88 | UPDATE | 90 | UPDATE |
89 | "videoJobInfo" | 91 | "videoJobInfo" |
90 | SET | 92 | SET |
@@ -99,7 +101,7 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo | |||
99 | 101 | ||
100 | if (result.length === 0) return undefined | 102 | if (result.length === 0) return undefined |
101 | 103 | ||
102 | return result[0].pendingMove | 104 | return result[0][column] |
103 | } | 105 | } |
104 | 106 | ||
105 | static async abortAllTasks (videoUUID: string, column: VideoJobInfoColumnType): Promise<void> { | 107 | static async abortAllTasks (videoUUID: string, column: VideoJobInfoColumnType): Promise<void> { |
diff --git a/server/models/video/video-live-session.ts b/server/models/video/video-live-session.ts index dcded7872..9426f5d11 100644 --- a/server/models/video/video-live-session.ts +++ b/server/models/video/video-live-session.ts | |||
@@ -147,12 +147,21 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv | |||
147 | return VideoLiveSessionModel.scope(ScopeNames.WITH_REPLAY).findOne(query) | 147 | return VideoLiveSessionModel.scope(ScopeNames.WITH_REPLAY).findOne(query) |
148 | } | 148 | } |
149 | 149 | ||
150 | static findCurrentSessionOf (videoId: number) { | 150 | static findCurrentSessionOf (videoUUID: string) { |
151 | return VideoLiveSessionModel.findOne({ | 151 | return VideoLiveSessionModel.findOne({ |
152 | where: { | 152 | where: { |
153 | liveVideoId: videoId, | ||
154 | endDate: null | 153 | endDate: null |
155 | }, | 154 | }, |
155 | include: [ | ||
156 | { | ||
157 | model: VideoModel.unscoped(), | ||
158 | as: 'LiveVideo', | ||
159 | required: true, | ||
160 | where: { | ||
161 | uuid: videoUUID | ||
162 | } | ||
163 | } | ||
164 | ], | ||
156 | order: [ [ 'startDate', 'DESC' ] ] | 165 | order: [ [ 'startDate', 'DESC' ] ] |
157 | }) | 166 | }) |
158 | } | 167 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index f817c4a33..baa8c120a 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -29,12 +29,14 @@ import { LiveManager } from '@server/lib/live/live-manager' | |||
29 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' | 29 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' |
30 | import { tracer } from '@server/lib/opentelemetry/tracing' | 30 | import { tracer } from '@server/lib/opentelemetry/tracing' |
31 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 31 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
32 | import { Hooks } from '@server/lib/plugins/hooks' | ||
32 | import { VideoPathManager } from '@server/lib/video-path-manager' | 33 | import { VideoPathManager } from '@server/lib/video-path-manager' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 34 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
34 | import { getServerActor } from '@server/models/application/application' | 35 | import { getServerActor } from '@server/models/application/application' |
35 | import { ModelCache } from '@server/models/shared/model-cache' | 36 | import { ModelCache } from '@server/models/shared/model-cache' |
36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' | 37 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' |
37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' | 38 | import { uuidToShort } from '@shared/extra-utils' |
39 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@shared/ffmpeg' | ||
38 | import { | 40 | import { |
39 | ResultList, | 41 | ResultList, |
40 | ThumbnailType, | 42 | ThumbnailType, |
@@ -62,7 +64,6 @@ import { | |||
62 | isVideoStateValid, | 64 | isVideoStateValid, |
63 | isVideoSupportValid | 65 | isVideoSupportValid |
64 | } from '../../helpers/custom-validators/videos' | 66 | } from '../../helpers/custom-validators/videos' |
65 | import { getVideoStreamDimensionsInfo } from '../../helpers/ffmpeg' | ||
66 | import { logger } from '../../helpers/logger' | 67 | import { logger } from '../../helpers/logger' |
67 | import { CONFIG } from '../../initializers/config' | 68 | import { CONFIG } from '../../initializers/config' |
68 | import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | 69 | import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' |
@@ -137,7 +138,6 @@ import { VideoShareModel } from './video-share' | |||
137 | import { VideoSourceModel } from './video-source' | 138 | import { VideoSourceModel } from './video-source' |
138 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 139 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
139 | import { VideoTagModel } from './video-tag' | 140 | import { VideoTagModel } from './video-tag' |
140 | import { Hooks } from '@server/lib/plugins/hooks' | ||
141 | 141 | ||
142 | export enum ScopeNames { | 142 | export enum ScopeNames { |
143 | FOR_API = 'FOR_API', | 143 | FOR_API = 'FOR_API', |
@@ -798,7 +798,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
798 | 798 | ||
799 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) | 799 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) |
800 | 800 | ||
801 | LiveManager.Instance.stopSessionOf(instance.id, null) | 801 | LiveManager.Instance.stopSessionOf(instance.uuid, null) |
802 | } | 802 | } |
803 | 803 | ||
804 | @BeforeDestroy | 804 | @BeforeDestroy |
@@ -1763,10 +1763,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1763 | 1763 | ||
1764 | const { audioStream } = await getAudioStream(originalFilePath, probe) | 1764 | const { audioStream } = await getAudioStream(originalFilePath, probe) |
1765 | const hasAudio = await hasAudioStream(originalFilePath, probe) | 1765 | const hasAudio = await hasAudioStream(originalFilePath, probe) |
1766 | const fps = await getVideoStreamFPS(originalFilePath, probe) | ||
1766 | 1767 | ||
1767 | return { | 1768 | return { |
1768 | audioStream, | 1769 | audioStream, |
1769 | hasAudio, | 1770 | hasAudio, |
1771 | fps, | ||
1770 | 1772 | ||
1771 | ...await getVideoStreamDimensionsInfo(originalFilePath, probe) | 1773 | ...await getVideoStreamDimensionsInfo(originalFilePath, probe) |
1772 | } | 1774 | } |