aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-04-21 14:55:10 +0200
committerChocobozzz <chocobozzz@cpy.re>2023-05-09 08:57:34 +0200
commit0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch)
tree226d3dd1565b0bb56588897af3b8530e6216e96b /server/models
parent6bcb854cdea8688a32240bc5719c7d139806e00b (diff)
downloadPeerTube-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.ts347
-rw-r--r--server/models/runner/runner-registration-token.ts103
-rw-r--r--server/models/runner/runner.ts112
-rw-r--r--server/models/shared/update.ts28
-rw-r--r--server/models/video/video-job-info.ts16
-rw-r--r--server/models/video/video-live-session.ts13
-rw-r--r--server/models/video/video.ts10
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 @@
1import { FindOptions, Op, Transaction } from 'sequelize'
2import {
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'
16import { isUUIDValid } from '@server/helpers/custom-validators/misc'
17import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
18import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners'
19import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models'
20import { AttributesOnly } from '@shared/typescript-utils'
21import { getSort, searchAttribute } from '../shared'
22import { RunnerModel } from './runner'
23
24enum 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})
63export 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 @@
1import { FindOptions, literal } from 'sequelize'
2import { AllowNull, Column, CreatedAt, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { MRunnerRegistrationToken } from '@server/types/models/runners'
4import { RunnerRegistrationToken } from '@shared/models'
5import { AttributesOnly } from '@shared/typescript-utils'
6import { getSort } from '../shared'
7import { 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})
24export 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 @@
1import { FindOptions } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { MRunner } from '@server/types/models/runners'
4import { Runner } from '@shared/models'
5import { AttributesOnly } from '@shared/typescript-utils'
6import { getSort } from '../shared'
7import { RunnerRegistrationTokenModel } from './runner-registration-token'
8import { 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})
22export 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 @@
1import { QueryTypes, Sequelize, Transaction } from 'sequelize' 1import { QueryTypes, Sequelize, Transaction } from 'sequelize'
2 2
3const 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
4function setAsUpdated (options: { 6async 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
22export { 32export {
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 @@
1import { Op, QueryTypes, Transaction } from 'sequelize' 1import { Op, QueryTypes, Transaction } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Model, Table, Unique, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Model, Table, Unique, UpdatedAt } from 'sequelize-typescript'
3import { forceNumber } from '@shared/core-utils'
3import { AttributesOnly } from '@shared/typescript-utils' 4import { AttributesOnly } from '@shared/typescript-utils'
4import { VideoModel } from './video' 5import { 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'
29import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' 29import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage'
30import { tracer } from '@server/lib/opentelemetry/tracing' 30import { tracer } from '@server/lib/opentelemetry/tracing'
31import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' 31import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths'
32import { Hooks } from '@server/lib/plugins/hooks'
32import { VideoPathManager } from '@server/lib/video-path-manager' 33import { VideoPathManager } from '@server/lib/video-path-manager'
33import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' 34import { isVideoInPrivateDirectory } from '@server/lib/video-privacy'
34import { getServerActor } from '@server/models/application/application' 35import { getServerActor } from '@server/models/application/application'
35import { ModelCache } from '@server/models/shared/model-cache' 36import { ModelCache } from '@server/models/shared/model-cache'
36import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' 37import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
37import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' 38import { uuidToShort } from '@shared/extra-utils'
39import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@shared/ffmpeg'
38import { 40import {
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'
65import { getVideoStreamDimensionsInfo } from '../../helpers/ffmpeg'
66import { logger } from '../../helpers/logger' 67import { logger } from '../../helpers/logger'
67import { CONFIG } from '../../initializers/config' 68import { CONFIG } from '../../initializers/config'
68import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' 69import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
@@ -137,7 +138,6 @@ import { VideoShareModel } from './video-share'
137import { VideoSourceModel } from './video-source' 138import { VideoSourceModel } from './video-source'
138import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 139import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
139import { VideoTagModel } from './video-tag' 140import { VideoTagModel } from './video-tag'
140import { Hooks } from '@server/lib/plugins/hooks'
141 141
142export enum ScopeNames { 142export 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 }