@tracearr/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +7 -0
- package/dist/catalogLetters.d.ts +3 -0
- package/dist/catalogLetters.d.ts.map +1 -0
- package/dist/catalogLetters.js +3 -0
- package/dist/catalogLetters.js.map +1 -0
- package/dist/constants.d.ts +475 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +847 -0
- package/dist/constants.js.map +1 -0
- package/dist/dynamicRange.d.ts +16 -0
- package/dist/dynamicRange.d.ts.map +1 -0
- package/dist/dynamicRange.js +46 -0
- package/dist/dynamicRange.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/media.d.ts +16 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +18 -0
- package/dist/media.js.map +1 -0
- package/dist/resolution.d.ts +56 -0
- package/dist/resolution.d.ts.map +1 -0
- package/dist/resolution.js +130 -0
- package/dist/resolution.js.map +1 -0
- package/dist/schemas.d.ts +1702 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +1057 -0
- package/dist/schemas.js.map +1 -0
- package/dist/serverScope.d.ts +27 -0
- package/dist/serverScope.d.ts.map +1 -0
- package/dist/serverScope.js +36 -0
- package/dist/serverScope.js.map +1 -0
- package/dist/types.d.ts +2484 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +57 -0
- package/dist/types.js.map +1 -0
- package/dist/violations.d.ts +37 -0
- package/dist/violations.d.ts.map +1 -0
- package/dist/violations.js +377 -0
- package/dist/violations.js.map +1 -0
- package/package.json +46 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,2484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for Tracearr
|
|
3
|
+
*/
|
|
4
|
+
import type { webhookFormatSchema, sessionTargetSchema, statPeriodSchema } from './schemas.js';
|
|
5
|
+
import type { z } from 'zod';
|
|
6
|
+
type SessionTarget = z.infer<typeof sessionTargetSchema>;
|
|
7
|
+
export type UserRole = 'owner' | 'admin' | 'viewer' | 'member' | 'disabled' | 'pending';
|
|
8
|
+
export declare const ROLE_PERMISSIONS: Record<UserRole, number>;
|
|
9
|
+
export declare const LOGIN_ROLES: UserRole[];
|
|
10
|
+
export declare const canLogin: (role: UserRole) => boolean;
|
|
11
|
+
export declare const hasMinRole: (userRole: UserRole, required: "owner" | "admin" | "viewer") => boolean;
|
|
12
|
+
export declare const isOwner: (role: UserRole) => boolean;
|
|
13
|
+
export declare const isActive: (role: UserRole) => boolean;
|
|
14
|
+
export type ServerType = 'plex' | 'jellyfin' | 'emby';
|
|
15
|
+
export interface Server {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: ServerType;
|
|
19
|
+
url: string;
|
|
20
|
+
displayOrder?: number;
|
|
21
|
+
color?: string | null;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
updatedAt: Date;
|
|
24
|
+
}
|
|
25
|
+
export interface User {
|
|
26
|
+
id: string;
|
|
27
|
+
username: string;
|
|
28
|
+
name: string | null;
|
|
29
|
+
thumbnail: string | null;
|
|
30
|
+
email: string | null;
|
|
31
|
+
role: UserRole;
|
|
32
|
+
aggregateTrustScore: number;
|
|
33
|
+
totalViolations: number;
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
updatedAt: Date;
|
|
36
|
+
}
|
|
37
|
+
export interface ServerUser {
|
|
38
|
+
id: string;
|
|
39
|
+
userId: string;
|
|
40
|
+
serverId: string;
|
|
41
|
+
externalId: string;
|
|
42
|
+
username: string;
|
|
43
|
+
email: string | null;
|
|
44
|
+
thumbUrl: string | null;
|
|
45
|
+
isServerAdmin: boolean;
|
|
46
|
+
trustScore: number;
|
|
47
|
+
joinedAt: Date | null;
|
|
48
|
+
lastActivityAt: Date | null;
|
|
49
|
+
removedAt: Date | null;
|
|
50
|
+
createdAt: Date;
|
|
51
|
+
updatedAt: Date;
|
|
52
|
+
identityName?: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface ServerUserWithIdentity extends ServerUser {
|
|
55
|
+
serverName: string;
|
|
56
|
+
identityName: string | null;
|
|
57
|
+
role: UserRole;
|
|
58
|
+
identityServers?: {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
serverUserId?: string;
|
|
62
|
+
removedAt?: string | null;
|
|
63
|
+
}[];
|
|
64
|
+
identityTrustScore?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface ServerUserDetail extends ServerUserWithIdentity {
|
|
67
|
+
stats: {
|
|
68
|
+
totalSessions: number;
|
|
69
|
+
totalWatchTime: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface ViolationSummary {
|
|
73
|
+
id: string;
|
|
74
|
+
ruleId: string;
|
|
75
|
+
rule: {
|
|
76
|
+
name: string;
|
|
77
|
+
type: string;
|
|
78
|
+
};
|
|
79
|
+
serverUserId: string;
|
|
80
|
+
serverId: string;
|
|
81
|
+
serverName: string;
|
|
82
|
+
sessionId: string | null;
|
|
83
|
+
mediaTitle: string | null;
|
|
84
|
+
severity: string;
|
|
85
|
+
data: Record<string, unknown>;
|
|
86
|
+
createdAt: Date;
|
|
87
|
+
acknowledgedAt: Date | null;
|
|
88
|
+
}
|
|
89
|
+
export interface ServerUserFullDetail {
|
|
90
|
+
user: ServerUserDetail;
|
|
91
|
+
identity: {
|
|
92
|
+
userId: string;
|
|
93
|
+
aggregateTrustScore: number;
|
|
94
|
+
totalViolations: number;
|
|
95
|
+
serverUsers: {
|
|
96
|
+
id: string;
|
|
97
|
+
serverId: string;
|
|
98
|
+
serverName: string;
|
|
99
|
+
serverType: string;
|
|
100
|
+
username: string;
|
|
101
|
+
thumbUrl: string | null;
|
|
102
|
+
trustScore: number;
|
|
103
|
+
sessionCount: number;
|
|
104
|
+
removedAt: Date | null;
|
|
105
|
+
}[];
|
|
106
|
+
stats: {
|
|
107
|
+
totalSessions: number;
|
|
108
|
+
totalWatchTime: number;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
sessions: {
|
|
112
|
+
data: Session[];
|
|
113
|
+
total: number;
|
|
114
|
+
hasMore: boolean;
|
|
115
|
+
};
|
|
116
|
+
locations: UserLocation[];
|
|
117
|
+
devices: UserDevice[];
|
|
118
|
+
violations: {
|
|
119
|
+
data: ViolationSummary[];
|
|
120
|
+
total: number;
|
|
121
|
+
hasMore: boolean;
|
|
122
|
+
};
|
|
123
|
+
terminations: {
|
|
124
|
+
data: TerminationLogWithDetails[];
|
|
125
|
+
total: number;
|
|
126
|
+
hasMore: boolean;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export interface AuthUser {
|
|
130
|
+
userId: string;
|
|
131
|
+
username: string;
|
|
132
|
+
role: UserRole;
|
|
133
|
+
serverIds: string[];
|
|
134
|
+
mobile?: boolean;
|
|
135
|
+
deviceId?: string;
|
|
136
|
+
}
|
|
137
|
+
export interface UserMergeResult {
|
|
138
|
+
targetUserId: string;
|
|
139
|
+
auditId: string;
|
|
140
|
+
movedServerUserIds: string[];
|
|
141
|
+
combinedServerUsers: {
|
|
142
|
+
sourceServerUserId: string;
|
|
143
|
+
targetServerUserId: string;
|
|
144
|
+
serverId: string;
|
|
145
|
+
}[];
|
|
146
|
+
wasSameServerCombine: boolean;
|
|
147
|
+
droppedRuleNames: string[];
|
|
148
|
+
}
|
|
149
|
+
export interface ServerUserSplitResult {
|
|
150
|
+
newUserId: string;
|
|
151
|
+
serverUserId: string;
|
|
152
|
+
}
|
|
153
|
+
export interface MergeSuggestionIdentity {
|
|
154
|
+
userId: string;
|
|
155
|
+
username: string;
|
|
156
|
+
name: string | null;
|
|
157
|
+
email: string | null;
|
|
158
|
+
role: UserRole;
|
|
159
|
+
loginCapable: boolean;
|
|
160
|
+
serverUsers: {
|
|
161
|
+
id: string;
|
|
162
|
+
serverId: string;
|
|
163
|
+
serverName: string;
|
|
164
|
+
username: string;
|
|
165
|
+
email: string | null;
|
|
166
|
+
removedAt: string | null;
|
|
167
|
+
}[];
|
|
168
|
+
}
|
|
169
|
+
export interface MergeSuggestion {
|
|
170
|
+
matchType: 'email' | 'username';
|
|
171
|
+
matchValue: string;
|
|
172
|
+
users: [MergeSuggestionIdentity, MergeSuggestionIdentity];
|
|
173
|
+
requiredTargetUserId: string | null;
|
|
174
|
+
wouldCombineSameServer: boolean;
|
|
175
|
+
}
|
|
176
|
+
export interface SetupStatus {
|
|
177
|
+
needsSetup: boolean;
|
|
178
|
+
requiresClaimCode: boolean;
|
|
179
|
+
hasServers: boolean;
|
|
180
|
+
hasJellyfinServers: boolean;
|
|
181
|
+
hasPasswordAuth: boolean;
|
|
182
|
+
authMethods: {
|
|
183
|
+
local: boolean;
|
|
184
|
+
plex: boolean;
|
|
185
|
+
oidc: boolean;
|
|
186
|
+
oidcProviderName: string | null;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export type SessionState = 'playing' | 'paused' | 'stopped';
|
|
190
|
+
/** Supported media types */
|
|
191
|
+
export declare const MEDIA_TYPES: readonly ["movie", "episode", "track", "live", "photo", "trailer", "unknown"];
|
|
192
|
+
export type MediaType = (typeof MEDIA_TYPES)[number];
|
|
193
|
+
/** Source video details from original media file */
|
|
194
|
+
export interface SourceVideoDetails {
|
|
195
|
+
bitrate?: number;
|
|
196
|
+
framerate?: string;
|
|
197
|
+
dynamicRange?: string;
|
|
198
|
+
aspectRatio?: number;
|
|
199
|
+
profile?: string;
|
|
200
|
+
level?: string;
|
|
201
|
+
colorSpace?: string;
|
|
202
|
+
colorDepth?: number;
|
|
203
|
+
}
|
|
204
|
+
/** Source audio details from original media file */
|
|
205
|
+
export interface SourceAudioDetails {
|
|
206
|
+
bitrate?: number;
|
|
207
|
+
channelLayout?: string;
|
|
208
|
+
language?: string;
|
|
209
|
+
sampleRate?: number;
|
|
210
|
+
}
|
|
211
|
+
/** Stream video details after transcode */
|
|
212
|
+
export interface StreamVideoDetails {
|
|
213
|
+
bitrate?: number;
|
|
214
|
+
width?: number;
|
|
215
|
+
height?: number;
|
|
216
|
+
framerate?: string;
|
|
217
|
+
dynamicRange?: string;
|
|
218
|
+
}
|
|
219
|
+
/** Stream audio details after transcode */
|
|
220
|
+
export interface StreamAudioDetails {
|
|
221
|
+
bitrate?: number;
|
|
222
|
+
channels?: number;
|
|
223
|
+
language?: string;
|
|
224
|
+
}
|
|
225
|
+
/** Transcode processing information */
|
|
226
|
+
export interface TranscodeInfo {
|
|
227
|
+
containerDecision?: string;
|
|
228
|
+
sourceContainer?: string;
|
|
229
|
+
streamContainer?: string;
|
|
230
|
+
hwRequested?: boolean;
|
|
231
|
+
hwDecoding?: string;
|
|
232
|
+
hwEncoding?: string;
|
|
233
|
+
speed?: number;
|
|
234
|
+
throttled?: boolean;
|
|
235
|
+
reasons?: string[];
|
|
236
|
+
}
|
|
237
|
+
/** Subtitle stream information */
|
|
238
|
+
export interface SubtitleInfo {
|
|
239
|
+
decision?: string;
|
|
240
|
+
codec?: string;
|
|
241
|
+
language?: string;
|
|
242
|
+
forced?: boolean;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Common fields for stream metadata tracking.
|
|
246
|
+
* Used by Session, ProcessedSession, MediaSession.quality, and test fixtures.
|
|
247
|
+
* All fields are nullable for backwards compatibility with existing sessions.
|
|
248
|
+
*/
|
|
249
|
+
export interface StreamDetailFields {
|
|
250
|
+
sourceVideoCodec: string | null;
|
|
251
|
+
sourceAudioCodec: string | null;
|
|
252
|
+
sourceAudioChannels: number | null;
|
|
253
|
+
sourceVideoWidth: number | null;
|
|
254
|
+
sourceVideoHeight: number | null;
|
|
255
|
+
sourceVideoDetails: SourceVideoDetails | null;
|
|
256
|
+
sourceAudioDetails: SourceAudioDetails | null;
|
|
257
|
+
streamVideoCodec: string | null;
|
|
258
|
+
streamAudioCodec: string | null;
|
|
259
|
+
streamVideoDetails: StreamVideoDetails | null;
|
|
260
|
+
streamAudioDetails: StreamAudioDetails | null;
|
|
261
|
+
transcodeInfo: TranscodeInfo | null;
|
|
262
|
+
subtitleInfo: SubtitleInfo | null;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Default values for stream detail fields (all null).
|
|
266
|
+
* Use with spread operator for test fixtures and initial values.
|
|
267
|
+
*/
|
|
268
|
+
export declare const DEFAULT_STREAM_DETAILS: StreamDetailFields;
|
|
269
|
+
export interface Session extends StreamDetailFields {
|
|
270
|
+
id: string;
|
|
271
|
+
serverId: string;
|
|
272
|
+
serverName?: string;
|
|
273
|
+
serverUserId: string;
|
|
274
|
+
sessionKey: string;
|
|
275
|
+
state: SessionState;
|
|
276
|
+
mediaType: MediaType;
|
|
277
|
+
mediaTitle: string;
|
|
278
|
+
grandparentTitle: string | null;
|
|
279
|
+
seasonNumber: number | null;
|
|
280
|
+
episodeNumber: number | null;
|
|
281
|
+
year: number | null;
|
|
282
|
+
thumbPath: string | null;
|
|
283
|
+
ratingKey: string | null;
|
|
284
|
+
serverVersionKey: string | null;
|
|
285
|
+
parentRatingKey: string | null;
|
|
286
|
+
grandparentRatingKey: string | null;
|
|
287
|
+
mediaId: string | null;
|
|
288
|
+
showMediaId: string | null;
|
|
289
|
+
imdbId: string | null;
|
|
290
|
+
tmdbId: number | null;
|
|
291
|
+
tvdbId: number | null;
|
|
292
|
+
externalSessionId: string | null;
|
|
293
|
+
startedAt: Date;
|
|
294
|
+
stoppedAt: Date | null;
|
|
295
|
+
durationMs: number | null;
|
|
296
|
+
totalDurationMs: number | null;
|
|
297
|
+
progressMs: number | null;
|
|
298
|
+
lastPausedAt: Date | null;
|
|
299
|
+
pausedDurationMs: number;
|
|
300
|
+
referenceId: string | null;
|
|
301
|
+
watched: boolean;
|
|
302
|
+
ipAddress: string;
|
|
303
|
+
geoCity: string | null;
|
|
304
|
+
geoRegion: string | null;
|
|
305
|
+
geoCountry: string | null;
|
|
306
|
+
geoContinent: string | null;
|
|
307
|
+
geoPostal: string | null;
|
|
308
|
+
geoLat: number | null;
|
|
309
|
+
geoLon: number | null;
|
|
310
|
+
geoAsnNumber: number | null;
|
|
311
|
+
geoAsnOrganization: string | null;
|
|
312
|
+
playerName: string | null;
|
|
313
|
+
deviceId: string | null;
|
|
314
|
+
product: string | null;
|
|
315
|
+
device: string | null;
|
|
316
|
+
platform: string | null;
|
|
317
|
+
quality: string | null;
|
|
318
|
+
isTranscode: boolean;
|
|
319
|
+
videoDecision: string | null;
|
|
320
|
+
audioDecision: string | null;
|
|
321
|
+
bitrate: number | null;
|
|
322
|
+
channelTitle: string | null;
|
|
323
|
+
channelIdentifier: string | null;
|
|
324
|
+
channelThumb: string | null;
|
|
325
|
+
artistName: string | null;
|
|
326
|
+
albumName: string | null;
|
|
327
|
+
trackNumber: number | null;
|
|
328
|
+
discNumber: number | null;
|
|
329
|
+
}
|
|
330
|
+
export interface ActiveSession extends Session {
|
|
331
|
+
user: Pick<ServerUser, 'id' | 'username' | 'thumbUrl'> & {
|
|
332
|
+
identityName: string | null;
|
|
333
|
+
};
|
|
334
|
+
server: Pick<Server, 'id' | 'name' | 'type'>;
|
|
335
|
+
/** Whether this session can be terminated (some clients like Plexamp don't support termination) */
|
|
336
|
+
canTerminate: boolean;
|
|
337
|
+
/** True while the session is an unconfirmed pending entry; absent once confirmed. */
|
|
338
|
+
pending?: boolean;
|
|
339
|
+
}
|
|
340
|
+
export interface SessionSegment {
|
|
341
|
+
startedAt: string;
|
|
342
|
+
stoppedAt: string | null;
|
|
343
|
+
durationMs: number | null;
|
|
344
|
+
pausedDurationMs: number;
|
|
345
|
+
}
|
|
346
|
+
export interface SessionWithDetails extends Omit<Session, 'ratingKey' | 'externalSessionId'> {
|
|
347
|
+
user: Pick<ServerUser, 'id' | 'username' | 'thumbUrl'> & {
|
|
348
|
+
identityName: string | null;
|
|
349
|
+
};
|
|
350
|
+
server: Pick<Server, 'id' | 'name' | 'type'>;
|
|
351
|
+
segmentCount?: number;
|
|
352
|
+
segments?: SessionSegment[];
|
|
353
|
+
}
|
|
354
|
+
export type RuleType = 'impossible_travel' | 'simultaneous_locations' | 'device_velocity' | 'concurrent_streams' | 'geo_restriction' | 'account_inactivity';
|
|
355
|
+
export interface ImpossibleTravelParams {
|
|
356
|
+
maxSpeedKmh: number;
|
|
357
|
+
ignoreVpnRanges?: boolean;
|
|
358
|
+
/** When true, exclude sessions from private/local network IPs from comparison */
|
|
359
|
+
excludePrivateIps?: boolean;
|
|
360
|
+
}
|
|
361
|
+
export interface SimultaneousLocationsParams {
|
|
362
|
+
minDistanceKm: number;
|
|
363
|
+
/** When true, exclude sessions from private/local network IPs from comparison */
|
|
364
|
+
excludePrivateIps?: boolean;
|
|
365
|
+
}
|
|
366
|
+
export interface DeviceVelocityParams {
|
|
367
|
+
maxIps: number;
|
|
368
|
+
windowHours: number;
|
|
369
|
+
/** When true, exclude private/local network IPs (192.168.x.x, 10.x.x.x, etc.) from unique IP count */
|
|
370
|
+
excludePrivateIps?: boolean;
|
|
371
|
+
/** When true, count by deviceId instead of IP - same device with different IPs counts as 1 */
|
|
372
|
+
groupByDevice?: boolean;
|
|
373
|
+
}
|
|
374
|
+
export interface ConcurrentStreamsParams {
|
|
375
|
+
maxStreams: number;
|
|
376
|
+
/** When true, exclude sessions from private/local network IPs from stream count */
|
|
377
|
+
excludePrivateIps?: boolean;
|
|
378
|
+
}
|
|
379
|
+
export type GeoRestrictionMode = 'blocklist' | 'allowlist';
|
|
380
|
+
export interface GeoRestrictionParams {
|
|
381
|
+
mode: GeoRestrictionMode;
|
|
382
|
+
countries: string[];
|
|
383
|
+
/** When true, always allow sessions from private/local network IPs (default behavior, explicit option) */
|
|
384
|
+
excludePrivateIps?: boolean;
|
|
385
|
+
}
|
|
386
|
+
/** Time unit for inactivity threshold */
|
|
387
|
+
export type AccountInactivityUnit = 'days' | 'weeks' | 'months';
|
|
388
|
+
export interface AccountInactivityParams {
|
|
389
|
+
/** Inactivity threshold value (e.g., 30, 7, 3) */
|
|
390
|
+
inactivityValue: number;
|
|
391
|
+
/** Time unit for the inactivity threshold */
|
|
392
|
+
inactivityUnit: AccountInactivityUnit;
|
|
393
|
+
}
|
|
394
|
+
export type RuleParams = ImpossibleTravelParams | SimultaneousLocationsParams | DeviceVelocityParams | ConcurrentStreamsParams | GeoRestrictionParams | AccountInactivityParams;
|
|
395
|
+
export interface Rule {
|
|
396
|
+
id: string;
|
|
397
|
+
name: string;
|
|
398
|
+
type: RuleType | null;
|
|
399
|
+
params: RuleParams | null;
|
|
400
|
+
description?: string | null;
|
|
401
|
+
severity?: ViolationSeverity;
|
|
402
|
+
conditions?: RuleConditions | null;
|
|
403
|
+
actions?: RuleActions | null;
|
|
404
|
+
serverId?: string | null;
|
|
405
|
+
userId?: string | null;
|
|
406
|
+
identityName?: string | null;
|
|
407
|
+
enforceAcrossServers?: boolean;
|
|
408
|
+
serverUserId: string | null;
|
|
409
|
+
isActive: boolean;
|
|
410
|
+
createdAt: Date;
|
|
411
|
+
updatedAt: Date;
|
|
412
|
+
}
|
|
413
|
+
export type ComparisonOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte';
|
|
414
|
+
export type ArrayOperator = 'in' | 'not_in';
|
|
415
|
+
export type StringOperator = 'contains' | 'not_contains';
|
|
416
|
+
export type Operator = ComparisonOperator | ArrayOperator | StringOperator;
|
|
417
|
+
export type SessionBehaviorField = 'concurrent_streams' | 'active_session_distance_km' | 'travel_speed_kmh' | 'unique_ips_in_window' | 'unique_devices_in_window' | 'inactive_days' | 'current_pause_minutes' | 'total_pause_minutes';
|
|
418
|
+
export type StreamQualityField = 'source_resolution' | 'output_resolution' | 'is_transcoding' | 'is_transcode_downgrade' | 'source_bitrate_mbps';
|
|
419
|
+
export type TranscodingConditionValue = 'video' | 'audio' | 'video_or_audio' | 'neither';
|
|
420
|
+
export type UserAttributeField = 'user_id' | 'trust_score' | 'account_age_days';
|
|
421
|
+
export type DeviceClientField = 'device_type' | 'client_name' | 'platform';
|
|
422
|
+
export type NetworkLocationField = 'is_local_network' | 'country' | 'ip_in_range';
|
|
423
|
+
export type ScopeField = 'server_id' | 'media_type';
|
|
424
|
+
export type ConditionField = SessionBehaviorField | StreamQualityField | UserAttributeField | DeviceClientField | NetworkLocationField | ScopeField;
|
|
425
|
+
export type VideoResolution = '4K' | '1080p' | '720p' | '480p' | 'SD' | 'unknown';
|
|
426
|
+
export type DeviceType = 'mobile' | 'tablet' | 'tv' | 'desktop' | 'browser' | 'unknown';
|
|
427
|
+
export type Platform = 'ios' | 'android' | 'windows' | 'macos' | 'linux' | 'tvos' | 'androidtv' | 'roku' | 'webos' | 'tizen' | 'unknown';
|
|
428
|
+
export type MediaTypeEnum = 'movie' | 'episode' | 'track' | 'photo' | 'live' | 'trailer';
|
|
429
|
+
export type ConditionValue = string | number | boolean | string[] | number[];
|
|
430
|
+
export interface ConditionEvidence {
|
|
431
|
+
field: ConditionField;
|
|
432
|
+
operator: Operator;
|
|
433
|
+
threshold: ConditionValue;
|
|
434
|
+
actual: unknown;
|
|
435
|
+
matched: boolean;
|
|
436
|
+
relatedSessionIds?: string[];
|
|
437
|
+
details?: Record<string, unknown>;
|
|
438
|
+
}
|
|
439
|
+
export interface GroupEvidence {
|
|
440
|
+
groupIndex: number;
|
|
441
|
+
matched: boolean;
|
|
442
|
+
conditions: ConditionEvidence[];
|
|
443
|
+
}
|
|
444
|
+
export interface Condition {
|
|
445
|
+
field: ConditionField;
|
|
446
|
+
operator: Operator;
|
|
447
|
+
value: ConditionValue;
|
|
448
|
+
params?: {
|
|
449
|
+
window_hours?: number;
|
|
450
|
+
exclude_same_device?: boolean;
|
|
451
|
+
exclude_same_ip?: boolean;
|
|
452
|
+
count_device_types?: DeviceType[];
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
export interface ConditionGroup {
|
|
456
|
+
conditions: Condition[];
|
|
457
|
+
}
|
|
458
|
+
export interface RuleConditions {
|
|
459
|
+
groups: ConditionGroup[];
|
|
460
|
+
}
|
|
461
|
+
export type ActionType = 'log_only' | 'notify' | 'adjust_trust' | 'set_trust' | 'reset_trust' | 'kill_stream' | 'message_client';
|
|
462
|
+
export type NotificationChannelV2 = 'push' | 'discord' | 'email' | 'webhook';
|
|
463
|
+
export interface LogOnlyAction {
|
|
464
|
+
type: 'log_only';
|
|
465
|
+
message?: string;
|
|
466
|
+
}
|
|
467
|
+
export interface NotifyAction {
|
|
468
|
+
type: 'notify';
|
|
469
|
+
channels: NotificationChannelV2[];
|
|
470
|
+
cooldown_minutes?: number;
|
|
471
|
+
}
|
|
472
|
+
export interface AdjustTrustAction {
|
|
473
|
+
type: 'adjust_trust';
|
|
474
|
+
amount: number;
|
|
475
|
+
}
|
|
476
|
+
export interface SetTrustAction {
|
|
477
|
+
type: 'set_trust';
|
|
478
|
+
value: number;
|
|
479
|
+
}
|
|
480
|
+
export interface ResetTrustAction {
|
|
481
|
+
type: 'reset_trust';
|
|
482
|
+
}
|
|
483
|
+
export interface KillStreamAction {
|
|
484
|
+
type: 'kill_stream';
|
|
485
|
+
/** Seconds to wait before killing. The kill only fires if the rule condition still holds after the wait; 0 (default) still re-checks once before killing. */
|
|
486
|
+
delay_seconds?: number;
|
|
487
|
+
require_confirmation?: boolean;
|
|
488
|
+
cooldown_minutes?: number;
|
|
489
|
+
/** Message to display to user before termination. If omitted, terminates silently. */
|
|
490
|
+
message?: string;
|
|
491
|
+
target?: SessionTarget;
|
|
492
|
+
}
|
|
493
|
+
export interface MessageClientAction {
|
|
494
|
+
type: 'message_client';
|
|
495
|
+
message: string;
|
|
496
|
+
target?: SessionTarget;
|
|
497
|
+
}
|
|
498
|
+
export type Action = LogOnlyAction | NotifyAction | AdjustTrustAction | SetTrustAction | ResetTrustAction | KillStreamAction | MessageClientAction;
|
|
499
|
+
export interface RuleActions {
|
|
500
|
+
actions: Action[];
|
|
501
|
+
}
|
|
502
|
+
export interface RuleV2 {
|
|
503
|
+
id: string;
|
|
504
|
+
name: string;
|
|
505
|
+
description: string | null;
|
|
506
|
+
serverId: string | null;
|
|
507
|
+
serverUserId: string | null;
|
|
508
|
+
userId: string | null;
|
|
509
|
+
enforceAcrossServers: boolean;
|
|
510
|
+
isActive: boolean;
|
|
511
|
+
severity: ViolationSeverity;
|
|
512
|
+
conditions: RuleConditions;
|
|
513
|
+
actions: RuleActions;
|
|
514
|
+
createdAt: Date;
|
|
515
|
+
updatedAt: Date;
|
|
516
|
+
}
|
|
517
|
+
export interface ActionResult {
|
|
518
|
+
actionType: string;
|
|
519
|
+
success: boolean;
|
|
520
|
+
skipped?: boolean;
|
|
521
|
+
skipReason?: string;
|
|
522
|
+
errorMessage?: string;
|
|
523
|
+
executedAt?: string;
|
|
524
|
+
}
|
|
525
|
+
export type ViolationSeverity = 'low' | 'warning' | 'high';
|
|
526
|
+
export interface Violation {
|
|
527
|
+
id: string;
|
|
528
|
+
ruleId: string;
|
|
529
|
+
serverUserId: string;
|
|
530
|
+
sessionId: string | null;
|
|
531
|
+
severity: ViolationSeverity;
|
|
532
|
+
data: Record<string, unknown>;
|
|
533
|
+
createdAt: Date;
|
|
534
|
+
acknowledgedAt: Date | null;
|
|
535
|
+
}
|
|
536
|
+
export interface ViolationSessionInfo {
|
|
537
|
+
id: string;
|
|
538
|
+
mediaTitle: string;
|
|
539
|
+
mediaType: MediaType;
|
|
540
|
+
grandparentTitle: string | null;
|
|
541
|
+
seasonNumber: number | null;
|
|
542
|
+
episodeNumber: number | null;
|
|
543
|
+
year: number | null;
|
|
544
|
+
ipAddress: string;
|
|
545
|
+
geoCity: string | null;
|
|
546
|
+
geoRegion: string | null;
|
|
547
|
+
geoCountry: string | null;
|
|
548
|
+
geoContinent: string | null;
|
|
549
|
+
geoPostal: string | null;
|
|
550
|
+
geoLat: number | null;
|
|
551
|
+
geoLon: number | null;
|
|
552
|
+
playerName: string | null;
|
|
553
|
+
device: string | null;
|
|
554
|
+
deviceId: string | null;
|
|
555
|
+
platform: string | null;
|
|
556
|
+
product: string | null;
|
|
557
|
+
quality: string | null;
|
|
558
|
+
startedAt: Date;
|
|
559
|
+
}
|
|
560
|
+
export interface ViolationWithDetails extends Violation {
|
|
561
|
+
rule: Pick<Rule, 'id' | 'name'> & {
|
|
562
|
+
type: RuleType | null;
|
|
563
|
+
};
|
|
564
|
+
user: Pick<ServerUser, 'id' | 'username' | 'thumbUrl' | 'serverId'> & {
|
|
565
|
+
identityName: string | null;
|
|
566
|
+
userId?: string;
|
|
567
|
+
};
|
|
568
|
+
server?: Pick<Server, 'id' | 'name' | 'type'>;
|
|
569
|
+
session?: ViolationSessionInfo;
|
|
570
|
+
relatedSessions?: ViolationSessionInfo[];
|
|
571
|
+
userHistory?: {
|
|
572
|
+
previousIPs: string[];
|
|
573
|
+
previousDevices: string[];
|
|
574
|
+
previousLocations: Array<{
|
|
575
|
+
city: string | null;
|
|
576
|
+
country: string | null;
|
|
577
|
+
ip: string;
|
|
578
|
+
}>;
|
|
579
|
+
};
|
|
580
|
+
/** Action results from V2 rule execution */
|
|
581
|
+
actionResults?: ActionResult[];
|
|
582
|
+
/** Condition evidence from V2 rule evaluation */
|
|
583
|
+
evidence?: GroupEvidence[];
|
|
584
|
+
/** Display names for server-user IDs found in evidence conditions (detail endpoint only) */
|
|
585
|
+
userNames?: Record<string, string>;
|
|
586
|
+
}
|
|
587
|
+
export interface DashboardStats {
|
|
588
|
+
activeStreams: number;
|
|
589
|
+
todayPlays: number;
|
|
590
|
+
todaySessions: number;
|
|
591
|
+
watchTimeHours: number;
|
|
592
|
+
alertsLast24h: number;
|
|
593
|
+
activeUsersToday: number;
|
|
594
|
+
}
|
|
595
|
+
export interface PlayStats {
|
|
596
|
+
date: string;
|
|
597
|
+
count: number;
|
|
598
|
+
serverId: string;
|
|
599
|
+
}
|
|
600
|
+
export interface UserStats {
|
|
601
|
+
serverUserId: string;
|
|
602
|
+
username: string;
|
|
603
|
+
thumbUrl: string | null;
|
|
604
|
+
playCount: number;
|
|
605
|
+
watchTimeHours: number;
|
|
606
|
+
}
|
|
607
|
+
export interface LocationUserInfo {
|
|
608
|
+
id: string;
|
|
609
|
+
username: string;
|
|
610
|
+
thumbUrl: string | null;
|
|
611
|
+
}
|
|
612
|
+
export interface LocationStats {
|
|
613
|
+
city: string | null;
|
|
614
|
+
region: string | null;
|
|
615
|
+
country: string | null;
|
|
616
|
+
lat: number;
|
|
617
|
+
lon: number;
|
|
618
|
+
count: number;
|
|
619
|
+
lastActivity?: Date;
|
|
620
|
+
firstActivity?: Date;
|
|
621
|
+
users?: LocationUserInfo[];
|
|
622
|
+
deviceCount?: number;
|
|
623
|
+
servers?: {
|
|
624
|
+
serverId: string;
|
|
625
|
+
count: number;
|
|
626
|
+
}[];
|
|
627
|
+
}
|
|
628
|
+
export interface LocationStatsSummary {
|
|
629
|
+
totalStreams: number;
|
|
630
|
+
uniqueLocations: number;
|
|
631
|
+
topCity: string | null;
|
|
632
|
+
}
|
|
633
|
+
export interface LocationFilterOptions {
|
|
634
|
+
users: {
|
|
635
|
+
id: string;
|
|
636
|
+
username: string;
|
|
637
|
+
identityName: string | null;
|
|
638
|
+
serverUserIds: string[];
|
|
639
|
+
}[];
|
|
640
|
+
servers: {
|
|
641
|
+
id: string;
|
|
642
|
+
name: string;
|
|
643
|
+
}[];
|
|
644
|
+
mediaTypes: MediaType[];
|
|
645
|
+
}
|
|
646
|
+
export interface LocationStatsResponse {
|
|
647
|
+
data: LocationStats[];
|
|
648
|
+
summary: LocationStatsSummary;
|
|
649
|
+
availableFilters: LocationFilterOptions;
|
|
650
|
+
}
|
|
651
|
+
export interface LibraryStats {
|
|
652
|
+
movies: number;
|
|
653
|
+
shows: number;
|
|
654
|
+
episodes: number;
|
|
655
|
+
tracks: number;
|
|
656
|
+
}
|
|
657
|
+
export interface DayOfWeekStats {
|
|
658
|
+
day: number;
|
|
659
|
+
name: string;
|
|
660
|
+
count: number;
|
|
661
|
+
}
|
|
662
|
+
export interface HourOfDayStats {
|
|
663
|
+
hour: number;
|
|
664
|
+
count: number;
|
|
665
|
+
}
|
|
666
|
+
export interface QualityStats {
|
|
667
|
+
directPlay: number;
|
|
668
|
+
directStream: number;
|
|
669
|
+
transcode: number;
|
|
670
|
+
total: number;
|
|
671
|
+
directPlayPercent: number;
|
|
672
|
+
directStreamPercent: number;
|
|
673
|
+
transcodePercent: number;
|
|
674
|
+
}
|
|
675
|
+
export interface TopUserStats {
|
|
676
|
+
/** Identity (person) id - plays/watch time are summed across all their accounts */
|
|
677
|
+
userId: string;
|
|
678
|
+
/** Representative account id, used for navigation (routes to /users/:id) */
|
|
679
|
+
serverUserId: string;
|
|
680
|
+
username: string;
|
|
681
|
+
identityName: string | null;
|
|
682
|
+
thumbUrl: string | null;
|
|
683
|
+
/** Representative account's server, used for avatar proxy */
|
|
684
|
+
serverId: string | null;
|
|
685
|
+
/** Identity-level aggregate trust score, not the representative account's own score */
|
|
686
|
+
trustScore: number;
|
|
687
|
+
playCount: number;
|
|
688
|
+
watchTimeHours: number;
|
|
689
|
+
topMediaType: string | null;
|
|
690
|
+
topContent: string | null;
|
|
691
|
+
/** Every server this identity has an account on, scoped to accessible servers */
|
|
692
|
+
identityServers?: {
|
|
693
|
+
id: string;
|
|
694
|
+
name: string;
|
|
695
|
+
}[];
|
|
696
|
+
}
|
|
697
|
+
export interface TopContentStats {
|
|
698
|
+
title: string;
|
|
699
|
+
type: string;
|
|
700
|
+
showTitle: string | null;
|
|
701
|
+
year: number | null;
|
|
702
|
+
playCount: number;
|
|
703
|
+
watchTimeHours: number;
|
|
704
|
+
thumbPath: string | null;
|
|
705
|
+
serverId: string | null;
|
|
706
|
+
ratingKey: string | null;
|
|
707
|
+
}
|
|
708
|
+
export interface PlatformStats {
|
|
709
|
+
platform: string | null;
|
|
710
|
+
count: number;
|
|
711
|
+
}
|
|
712
|
+
export interface ServerResourceDataPoint {
|
|
713
|
+
/** Unix timestamp */
|
|
714
|
+
at: number;
|
|
715
|
+
/** Timespan interval in seconds */
|
|
716
|
+
timespan: number;
|
|
717
|
+
/** System-wide CPU utilization percentage */
|
|
718
|
+
hostCpuUtilization: number;
|
|
719
|
+
/** Plex process CPU utilization percentage */
|
|
720
|
+
processCpuUtilization: number;
|
|
721
|
+
/** System-wide memory utilization percentage */
|
|
722
|
+
hostMemoryUtilization: number;
|
|
723
|
+
/** Plex process memory utilization percentage */
|
|
724
|
+
processMemoryUtilization: number;
|
|
725
|
+
}
|
|
726
|
+
export interface ServerResourceStats {
|
|
727
|
+
/** Server ID these stats belong to */
|
|
728
|
+
serverId: string;
|
|
729
|
+
/** Data points (newest first based on 'at' timestamp) */
|
|
730
|
+
data: ServerResourceDataPoint[];
|
|
731
|
+
/** When this data was fetched */
|
|
732
|
+
fetchedAt: Date;
|
|
733
|
+
}
|
|
734
|
+
export interface ServerBandwidthDataPoint {
|
|
735
|
+
/** Unix timestamp */
|
|
736
|
+
at: number;
|
|
737
|
+
/** Timespan interval in seconds */
|
|
738
|
+
timespan: number;
|
|
739
|
+
/** Total local (LAN) bandwidth in bytes for this interval */
|
|
740
|
+
lanBytes: number;
|
|
741
|
+
/** Total remote (WAN) bandwidth in bytes for this interval */
|
|
742
|
+
wanBytes: number;
|
|
743
|
+
}
|
|
744
|
+
export interface ServerBandwidthStats {
|
|
745
|
+
/** Server ID these stats belong to */
|
|
746
|
+
serverId: string;
|
|
747
|
+
/** Data points (newest first based on 'at' timestamp) */
|
|
748
|
+
data: ServerBandwidthDataPoint[];
|
|
749
|
+
/** When this data was fetched */
|
|
750
|
+
fetchedAt: Date;
|
|
751
|
+
}
|
|
752
|
+
export type WebhookFormat = z.infer<typeof webhookFormatSchema>;
|
|
753
|
+
export type UnitSystem = 'metric' | 'imperial';
|
|
754
|
+
export interface Settings {
|
|
755
|
+
allowGuestAccess: boolean;
|
|
756
|
+
unitSystem: UnitSystem;
|
|
757
|
+
discordWebhookUrl: string | null;
|
|
758
|
+
customWebhookUrl: string | null;
|
|
759
|
+
webhookFormat: WebhookFormat | null;
|
|
760
|
+
ntfyTopic: string | null;
|
|
761
|
+
ntfyAuthToken: string | null;
|
|
762
|
+
pushoverApiToken: string | null;
|
|
763
|
+
pushoverUserKey: string | null;
|
|
764
|
+
pollerEnabled: boolean;
|
|
765
|
+
pollerIntervalMs: number;
|
|
766
|
+
usePlexGeoip: boolean;
|
|
767
|
+
tautulliUrl: string | null;
|
|
768
|
+
tautulliApiKey: string | null;
|
|
769
|
+
externalUrl: string | null;
|
|
770
|
+
trustProxy: boolean;
|
|
771
|
+
mobileEnabled: boolean;
|
|
772
|
+
tailscaleEnabled: boolean;
|
|
773
|
+
tailscaleHostname: string | null;
|
|
774
|
+
backupScheduleType: BackupScheduleType;
|
|
775
|
+
backupScheduleTime: string;
|
|
776
|
+
backupScheduleDayOfWeek: number;
|
|
777
|
+
backupScheduleDayOfMonth: number;
|
|
778
|
+
backupRetentionCount: number;
|
|
779
|
+
pluginUpdateCheckEnabled: boolean;
|
|
780
|
+
pluginManifestUrl: string | null;
|
|
781
|
+
watchedThresholdMovie: number;
|
|
782
|
+
watchedThresholdTv: number;
|
|
783
|
+
watchedThresholdMusic: number;
|
|
784
|
+
publicApiRateLimitPerMinute: number;
|
|
785
|
+
imagePrecacheEnabled: boolean;
|
|
786
|
+
preferredPosterServerId: string | null;
|
|
787
|
+
}
|
|
788
|
+
export type TailscaleStatus = 'disabled' | 'starting' | 'awaiting_auth' | 'connected' | 'error' | 'stopping';
|
|
789
|
+
export interface TailscaleExitNode {
|
|
790
|
+
id: string;
|
|
791
|
+
hostname: string;
|
|
792
|
+
dnsName: string;
|
|
793
|
+
ip: string;
|
|
794
|
+
online: boolean;
|
|
795
|
+
active: boolean;
|
|
796
|
+
}
|
|
797
|
+
export interface TailscaleInfo {
|
|
798
|
+
status: TailscaleStatus;
|
|
799
|
+
authUrl: string | null;
|
|
800
|
+
hostname: string | null;
|
|
801
|
+
dnsName: string | null;
|
|
802
|
+
tailnetName: string | null;
|
|
803
|
+
tailnetIp: string | null;
|
|
804
|
+
tailnetUrl: string | null;
|
|
805
|
+
exitNodes: TailscaleExitNode[];
|
|
806
|
+
error: string | null;
|
|
807
|
+
available: boolean;
|
|
808
|
+
}
|
|
809
|
+
export interface HeavyOpsWaitingFor {
|
|
810
|
+
jobType: 'import' | 'maintenance';
|
|
811
|
+
description: string;
|
|
812
|
+
startedAt: string;
|
|
813
|
+
}
|
|
814
|
+
export interface TautulliImportProgress {
|
|
815
|
+
status: 'idle' | 'waiting' | 'fetching' | 'processing' | 'complete' | 'error';
|
|
816
|
+
/** Expected total from API (may differ from actual if API count is stale) */
|
|
817
|
+
totalRecords: number;
|
|
818
|
+
/** Actual records fetched from API so far */
|
|
819
|
+
fetchedRecords: number;
|
|
820
|
+
/** Records processed (looped through) */
|
|
821
|
+
processedRecords: number;
|
|
822
|
+
/** New sessions inserted */
|
|
823
|
+
importedRecords: number;
|
|
824
|
+
/** Existing sessions updated with new data */
|
|
825
|
+
updatedRecords: number;
|
|
826
|
+
/** Total skipped (sum of duplicate + unknownUser + activeSession) */
|
|
827
|
+
skippedRecords: number;
|
|
828
|
+
/** Skipped: already exists in DB or duplicate in this import */
|
|
829
|
+
duplicateRecords: number;
|
|
830
|
+
/** Skipped: user not found in Tracearr (need to sync server first) */
|
|
831
|
+
unknownUserRecords: number;
|
|
832
|
+
/** Skipped: in-progress sessions without reference_id */
|
|
833
|
+
activeSessionRecords: number;
|
|
834
|
+
/** Records that failed to process */
|
|
835
|
+
errorRecords: number;
|
|
836
|
+
currentPage: number;
|
|
837
|
+
totalPages: number;
|
|
838
|
+
message: string;
|
|
839
|
+
/** Present when status='waiting' - what this job is waiting for */
|
|
840
|
+
waitingFor?: HeavyOpsWaitingFor;
|
|
841
|
+
}
|
|
842
|
+
export interface TautulliImportResult {
|
|
843
|
+
success: boolean;
|
|
844
|
+
imported: number;
|
|
845
|
+
updated: number;
|
|
846
|
+
/** Number of sessions linked via referenceId (resume chain detection) */
|
|
847
|
+
linked: number;
|
|
848
|
+
skipped: number;
|
|
849
|
+
errors: number;
|
|
850
|
+
message: string;
|
|
851
|
+
/** Details about users that were skipped (not found in Tracearr) */
|
|
852
|
+
skippedUsers?: {
|
|
853
|
+
tautulliUserId: number;
|
|
854
|
+
username: string;
|
|
855
|
+
recordCount: number;
|
|
856
|
+
}[];
|
|
857
|
+
}
|
|
858
|
+
export interface JellystatImportProgress {
|
|
859
|
+
status: 'idle' | 'waiting' | 'parsing' | 'enriching' | 'processing' | 'complete' | 'error';
|
|
860
|
+
totalRecords: number;
|
|
861
|
+
processedRecords: number;
|
|
862
|
+
importedRecords: number;
|
|
863
|
+
skippedRecords: number;
|
|
864
|
+
/** Number of records filtered out (theme songs, theme videos, trailers, etc.) */
|
|
865
|
+
filteredRecords: number;
|
|
866
|
+
errorRecords: number;
|
|
867
|
+
/** Number of media items enriched with metadata from Jellyfin */
|
|
868
|
+
enrichedRecords: number;
|
|
869
|
+
/** Current phase message */
|
|
870
|
+
message: string;
|
|
871
|
+
/** Present when status='waiting' - what this job is waiting for */
|
|
872
|
+
waitingFor?: HeavyOpsWaitingFor;
|
|
873
|
+
}
|
|
874
|
+
export interface JellystatImportResult {
|
|
875
|
+
success: boolean;
|
|
876
|
+
imported: number;
|
|
877
|
+
updated: number;
|
|
878
|
+
skipped: number;
|
|
879
|
+
/** Number of records filtered out (theme songs, theme videos, trailers, etc.) */
|
|
880
|
+
filtered: number;
|
|
881
|
+
errors: number;
|
|
882
|
+
enriched: number;
|
|
883
|
+
message: string;
|
|
884
|
+
/** Details about users that were skipped (not found in Tracearr) */
|
|
885
|
+
skippedUsers?: {
|
|
886
|
+
jellyfinUserId: string;
|
|
887
|
+
username: string | null;
|
|
888
|
+
recordCount: number;
|
|
889
|
+
}[];
|
|
890
|
+
}
|
|
891
|
+
export interface LibrarySyncProgress {
|
|
892
|
+
serverId: string;
|
|
893
|
+
serverName: string;
|
|
894
|
+
status: 'running' | 'complete' | 'error';
|
|
895
|
+
currentLibrary?: string;
|
|
896
|
+
currentLibraryName?: string;
|
|
897
|
+
totalLibraries: number;
|
|
898
|
+
processedLibraries: number;
|
|
899
|
+
totalItems: number;
|
|
900
|
+
processedItems: number;
|
|
901
|
+
message: string;
|
|
902
|
+
startedAt: string;
|
|
903
|
+
completedAt?: string;
|
|
904
|
+
error?: string;
|
|
905
|
+
}
|
|
906
|
+
export interface ServerToClientEvents {
|
|
907
|
+
'session:started': (session: ActiveSession) => void;
|
|
908
|
+
'session:stopped': (sessionId: string) => void;
|
|
909
|
+
'session:updated': (session: ActiveSession) => void;
|
|
910
|
+
'violation:new': (violation: ViolationWithDetails) => void;
|
|
911
|
+
'stats:updated': (stats: DashboardStats) => void;
|
|
912
|
+
'import:progress': (progress: TautulliImportProgress) => void;
|
|
913
|
+
'import:jellystat:progress': (progress: JellystatImportProgress) => void;
|
|
914
|
+
'maintenance:progress': (progress: MaintenanceJobProgress) => void;
|
|
915
|
+
'library:sync:progress': (progress: LibrarySyncProgress) => void;
|
|
916
|
+
'tasks:updated': (tasks: RunningTask[]) => void;
|
|
917
|
+
'version:update': (data: {
|
|
918
|
+
current: string;
|
|
919
|
+
latest: string;
|
|
920
|
+
releaseUrl: string;
|
|
921
|
+
}) => void;
|
|
922
|
+
'server:down': (data: {
|
|
923
|
+
serverId: string;
|
|
924
|
+
serverName: string;
|
|
925
|
+
}) => void;
|
|
926
|
+
'server:up': (data: {
|
|
927
|
+
serverId: string;
|
|
928
|
+
serverName: string;
|
|
929
|
+
}) => void;
|
|
930
|
+
'server:connection': (status: ServerConnectionStatus) => void;
|
|
931
|
+
}
|
|
932
|
+
export interface ClientToServerEvents {
|
|
933
|
+
'subscribe:sessions': () => void;
|
|
934
|
+
'unsubscribe:sessions': () => void;
|
|
935
|
+
}
|
|
936
|
+
export interface UserLocation {
|
|
937
|
+
city: string | null;
|
|
938
|
+
region: string | null;
|
|
939
|
+
country: string | null;
|
|
940
|
+
lat: number | null;
|
|
941
|
+
lon: number | null;
|
|
942
|
+
sessionCount: number;
|
|
943
|
+
lastSeenAt: Date;
|
|
944
|
+
ipAddresses: string[];
|
|
945
|
+
}
|
|
946
|
+
export interface DeviceLocation {
|
|
947
|
+
city: string | null;
|
|
948
|
+
region: string | null;
|
|
949
|
+
country: string | null;
|
|
950
|
+
sessionCount: number;
|
|
951
|
+
lastSeenAt: Date;
|
|
952
|
+
}
|
|
953
|
+
export interface UserDevice {
|
|
954
|
+
deviceId: string | null;
|
|
955
|
+
playerName: string | null;
|
|
956
|
+
product: string | null;
|
|
957
|
+
device: string | null;
|
|
958
|
+
platform: string | null;
|
|
959
|
+
sessionCount: number;
|
|
960
|
+
lastSeenAt: Date;
|
|
961
|
+
locations: DeviceLocation[];
|
|
962
|
+
}
|
|
963
|
+
export interface PaginatedResponse<T> {
|
|
964
|
+
data: T[];
|
|
965
|
+
total: number;
|
|
966
|
+
page: number;
|
|
967
|
+
pageSize: number;
|
|
968
|
+
totalPages: number;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Aggregate stats returned with history query results.
|
|
972
|
+
* These are computed across the entire filtered result set (not just current page).
|
|
973
|
+
*/
|
|
974
|
+
export interface HistoryAggregates {
|
|
975
|
+
/** Total watch time in milliseconds across all matching sessions */
|
|
976
|
+
totalWatchTimeMs: number;
|
|
977
|
+
/** Count of unique plays (grouped by reference_id) */
|
|
978
|
+
playCount: number;
|
|
979
|
+
/** Count of unique users in the result set */
|
|
980
|
+
uniqueUsers: number;
|
|
981
|
+
/** Count of unique content items watched */
|
|
982
|
+
uniqueContent: number;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Response shape for history/sessions queries with cursor-based pagination.
|
|
986
|
+
* Aggregate stats are fetched separately via /sessions/history/aggregates.
|
|
987
|
+
*/
|
|
988
|
+
export interface HistorySessionResponse {
|
|
989
|
+
data: SessionWithDetails[];
|
|
990
|
+
/** Cursor for fetching the next page (undefined if no more results) */
|
|
991
|
+
nextCursor?: string;
|
|
992
|
+
/** Whether more results exist beyond the current page */
|
|
993
|
+
hasMore: boolean;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Option item with count for filter dropdowns.
|
|
997
|
+
* Count represents number of plays with this value.
|
|
998
|
+
*/
|
|
999
|
+
export interface FilterOptionItem {
|
|
1000
|
+
value: string;
|
|
1001
|
+
count: number;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* User option for user filter dropdown.
|
|
1005
|
+
*/
|
|
1006
|
+
export interface UserFilterOption {
|
|
1007
|
+
id: string;
|
|
1008
|
+
username: string;
|
|
1009
|
+
thumbUrl: string | null;
|
|
1010
|
+
serverId: string;
|
|
1011
|
+
identityName: string | null;
|
|
1012
|
+
/** All server account ids belonging to this person (identity), access-scoped. */
|
|
1013
|
+
serverUserIds: string[];
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Server option for server filter dropdown.
|
|
1017
|
+
*/
|
|
1018
|
+
export interface ServerFilterOption {
|
|
1019
|
+
id: string;
|
|
1020
|
+
name: string;
|
|
1021
|
+
type: 'plex' | 'jellyfin' | 'emby';
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Country option with session activity indicator.
|
|
1025
|
+
* Used when includeAllCountries=true for rules builder.
|
|
1026
|
+
*/
|
|
1027
|
+
export interface CountryOption {
|
|
1028
|
+
code: string;
|
|
1029
|
+
name: string;
|
|
1030
|
+
hasSessions: boolean;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Available filter options for the history page.
|
|
1034
|
+
* Returned by GET /sessions/filter-options to populate dropdowns.
|
|
1035
|
+
*/
|
|
1036
|
+
export interface HistoryFilterOptions {
|
|
1037
|
+
/** Available platforms (Windows, macOS, iOS, etc.) */
|
|
1038
|
+
platforms: FilterOptionItem[];
|
|
1039
|
+
/** Available products/apps (Plex for Windows, etc.) */
|
|
1040
|
+
products: FilterOptionItem[];
|
|
1041
|
+
/** Available device types (iPhone, Android TV, etc.) */
|
|
1042
|
+
devices: FilterOptionItem[];
|
|
1043
|
+
/** Available countries (codes with session count) */
|
|
1044
|
+
countries: FilterOptionItem[];
|
|
1045
|
+
/** Available cities */
|
|
1046
|
+
cities: FilterOptionItem[];
|
|
1047
|
+
/** Available users (with avatar info) */
|
|
1048
|
+
users: UserFilterOption[];
|
|
1049
|
+
/** Available servers (optional, included when requested) */
|
|
1050
|
+
servers?: ServerFilterOption[];
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Extended filter options for rules builder.
|
|
1054
|
+
* Includes all countries with session indicators and servers.
|
|
1055
|
+
*/
|
|
1056
|
+
export interface RulesFilterOptions extends Omit<HistoryFilterOptions, 'countries'> {
|
|
1057
|
+
/** All countries with session activity indicator */
|
|
1058
|
+
countries: CountryOption[];
|
|
1059
|
+
/** Available servers */
|
|
1060
|
+
servers: ServerFilterOption[];
|
|
1061
|
+
}
|
|
1062
|
+
export interface ApiError {
|
|
1063
|
+
statusCode: number;
|
|
1064
|
+
error: string;
|
|
1065
|
+
message: string;
|
|
1066
|
+
}
|
|
1067
|
+
export interface MobileToken {
|
|
1068
|
+
id: string;
|
|
1069
|
+
expiresAt: Date;
|
|
1070
|
+
createdAt: Date;
|
|
1071
|
+
usedAt: Date | null;
|
|
1072
|
+
}
|
|
1073
|
+
export interface MobilePairTokenResponse {
|
|
1074
|
+
token: string;
|
|
1075
|
+
expiresAt: Date;
|
|
1076
|
+
}
|
|
1077
|
+
export interface MobileSession {
|
|
1078
|
+
id: string;
|
|
1079
|
+
deviceName: string;
|
|
1080
|
+
deviceId: string;
|
|
1081
|
+
platform: 'ios' | 'android';
|
|
1082
|
+
expoPushToken: string | null;
|
|
1083
|
+
lastSeenAt: Date;
|
|
1084
|
+
createdAt: Date;
|
|
1085
|
+
}
|
|
1086
|
+
export interface MobileConfig {
|
|
1087
|
+
isEnabled: boolean;
|
|
1088
|
+
sessions: MobileSession[];
|
|
1089
|
+
serverName: string;
|
|
1090
|
+
pendingTokens: number;
|
|
1091
|
+
maxDevices: number;
|
|
1092
|
+
}
|
|
1093
|
+
export interface MobilePairRequest {
|
|
1094
|
+
token: string;
|
|
1095
|
+
deviceName: string;
|
|
1096
|
+
deviceId: string;
|
|
1097
|
+
platform: 'ios' | 'android';
|
|
1098
|
+
}
|
|
1099
|
+
export interface MobilePairResponse {
|
|
1100
|
+
accessToken: string;
|
|
1101
|
+
refreshToken: string;
|
|
1102
|
+
server: {
|
|
1103
|
+
id: string;
|
|
1104
|
+
name: string;
|
|
1105
|
+
type: 'plex' | 'jellyfin' | 'emby';
|
|
1106
|
+
};
|
|
1107
|
+
user: {
|
|
1108
|
+
userId: string;
|
|
1109
|
+
username: string;
|
|
1110
|
+
role: 'owner';
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
export interface MobileQRPayload {
|
|
1114
|
+
url: string;
|
|
1115
|
+
token: string;
|
|
1116
|
+
name: string;
|
|
1117
|
+
}
|
|
1118
|
+
export type NotificationEventType = 'violation_detected' | 'stream_started' | 'stream_stopped' | 'concurrent_streams' | 'new_device' | 'trust_score_changed' | 'server_down' | 'server_up' | 'plugin_update_available';
|
|
1119
|
+
export interface NotificationPreferences {
|
|
1120
|
+
id: string;
|
|
1121
|
+
mobileSessionId: string;
|
|
1122
|
+
pushEnabled: boolean;
|
|
1123
|
+
onViolationDetected: boolean;
|
|
1124
|
+
onStreamStarted: boolean;
|
|
1125
|
+
onStreamStopped: boolean;
|
|
1126
|
+
onConcurrentStreams: boolean;
|
|
1127
|
+
onNewDevice: boolean;
|
|
1128
|
+
onTrustScoreChanged: boolean;
|
|
1129
|
+
onServerDown: boolean;
|
|
1130
|
+
onServerUp: boolean;
|
|
1131
|
+
violationMinSeverity: number;
|
|
1132
|
+
violationRuleTypes: string[];
|
|
1133
|
+
maxPerMinute: number;
|
|
1134
|
+
maxPerHour: number;
|
|
1135
|
+
quietHoursEnabled: boolean;
|
|
1136
|
+
quietHoursStart: string | null;
|
|
1137
|
+
quietHoursEnd: string | null;
|
|
1138
|
+
quietHoursTimezone: string;
|
|
1139
|
+
quietHoursOverrideCritical: boolean;
|
|
1140
|
+
createdAt: Date;
|
|
1141
|
+
updatedAt: Date;
|
|
1142
|
+
}
|
|
1143
|
+
export interface RateLimitStatus {
|
|
1144
|
+
remainingMinute: number;
|
|
1145
|
+
remainingHour: number;
|
|
1146
|
+
resetMinuteIn: number;
|
|
1147
|
+
resetHourIn: number;
|
|
1148
|
+
}
|
|
1149
|
+
export interface NotificationPreferencesWithStatus extends NotificationPreferences {
|
|
1150
|
+
rateLimitStatus?: RateLimitStatus;
|
|
1151
|
+
}
|
|
1152
|
+
export type NotificationChannel = 'discord' | 'webhook' | 'push' | 'webToast';
|
|
1153
|
+
export interface NotificationChannelRouting {
|
|
1154
|
+
id: string;
|
|
1155
|
+
eventType: NotificationEventType;
|
|
1156
|
+
discordEnabled: boolean;
|
|
1157
|
+
webhookEnabled: boolean;
|
|
1158
|
+
pushEnabled: boolean;
|
|
1159
|
+
webToastEnabled: boolean;
|
|
1160
|
+
createdAt: Date;
|
|
1161
|
+
updatedAt: Date;
|
|
1162
|
+
}
|
|
1163
|
+
export interface EncryptedPushPayload {
|
|
1164
|
+
v: 1;
|
|
1165
|
+
iv: string;
|
|
1166
|
+
salt: string;
|
|
1167
|
+
ct: string;
|
|
1168
|
+
tag: string;
|
|
1169
|
+
}
|
|
1170
|
+
export interface PushNotificationPayload {
|
|
1171
|
+
type: NotificationEventType;
|
|
1172
|
+
title: string;
|
|
1173
|
+
body: string;
|
|
1174
|
+
data?: Record<string, unknown>;
|
|
1175
|
+
channelId?: string;
|
|
1176
|
+
badge?: number;
|
|
1177
|
+
sound?: string | boolean;
|
|
1178
|
+
priority?: 'default' | 'high';
|
|
1179
|
+
}
|
|
1180
|
+
export type SSEConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'fallback' | 'unsupported';
|
|
1181
|
+
export interface PlexSSENotification {
|
|
1182
|
+
NotificationContainer: {
|
|
1183
|
+
type: string;
|
|
1184
|
+
size: number;
|
|
1185
|
+
PlaySessionStateNotification?: PlexPlaySessionNotification[];
|
|
1186
|
+
ActivityNotification?: PlexActivityNotification[];
|
|
1187
|
+
StatusNotification?: PlexStatusNotification[];
|
|
1188
|
+
TranscodeSession?: PlexTranscodeNotification[];
|
|
1189
|
+
TimelineEntry?: PlexTimelineEntry[];
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
export interface PlexPlaySessionNotification {
|
|
1193
|
+
sessionKey: string;
|
|
1194
|
+
clientIdentifier: string;
|
|
1195
|
+
guid: string;
|
|
1196
|
+
ratingKey: string;
|
|
1197
|
+
url: string;
|
|
1198
|
+
key: string;
|
|
1199
|
+
viewOffset: number;
|
|
1200
|
+
playQueueItemID: number;
|
|
1201
|
+
state: 'playing' | 'paused' | 'stopped' | 'buffering';
|
|
1202
|
+
}
|
|
1203
|
+
export interface PlexTimelineEntry {
|
|
1204
|
+
identifier: string;
|
|
1205
|
+
sectionID?: number;
|
|
1206
|
+
itemID: number;
|
|
1207
|
+
type: number;
|
|
1208
|
+
title?: string;
|
|
1209
|
+
state: number;
|
|
1210
|
+
updatedAt?: number;
|
|
1211
|
+
}
|
|
1212
|
+
export interface PlexActivityNotification {
|
|
1213
|
+
event: string;
|
|
1214
|
+
uuid: string;
|
|
1215
|
+
Activity: {
|
|
1216
|
+
uuid: string;
|
|
1217
|
+
type: string;
|
|
1218
|
+
cancellable: boolean;
|
|
1219
|
+
userID: number;
|
|
1220
|
+
title: string;
|
|
1221
|
+
subtitle: string;
|
|
1222
|
+
progress: number;
|
|
1223
|
+
Context?: {
|
|
1224
|
+
key: string;
|
|
1225
|
+
};
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
export interface PlexStatusNotification {
|
|
1229
|
+
title: string;
|
|
1230
|
+
description: string;
|
|
1231
|
+
notificationName: string;
|
|
1232
|
+
}
|
|
1233
|
+
export interface PlexTranscodeNotification {
|
|
1234
|
+
key: string;
|
|
1235
|
+
throttled: boolean;
|
|
1236
|
+
complete: boolean;
|
|
1237
|
+
progress: number;
|
|
1238
|
+
size: number;
|
|
1239
|
+
speed: number;
|
|
1240
|
+
error: boolean;
|
|
1241
|
+
duration: number;
|
|
1242
|
+
remaining: number;
|
|
1243
|
+
context: string;
|
|
1244
|
+
sourceVideoCodec: string;
|
|
1245
|
+
sourceAudioCodec: string;
|
|
1246
|
+
videoDecision: string;
|
|
1247
|
+
audioDecision: string;
|
|
1248
|
+
subtitleDecision: string;
|
|
1249
|
+
protocol: string;
|
|
1250
|
+
container: string;
|
|
1251
|
+
videoCodec: string;
|
|
1252
|
+
audioCodec: string;
|
|
1253
|
+
audioChannels: number;
|
|
1254
|
+
transcodeHwRequested: boolean;
|
|
1255
|
+
transcodeHwDecoding: string;
|
|
1256
|
+
transcodeHwEncoding: string;
|
|
1257
|
+
transcodeHwDecodingTitle: string;
|
|
1258
|
+
transcodeHwEncodingTitle: string;
|
|
1259
|
+
}
|
|
1260
|
+
export interface SSEConnectionStatus {
|
|
1261
|
+
serverId: string;
|
|
1262
|
+
serverName: string;
|
|
1263
|
+
state: SSEConnectionState;
|
|
1264
|
+
connectedAt: Date | null;
|
|
1265
|
+
lastEventAt: Date | null;
|
|
1266
|
+
reconnectAttempts: number;
|
|
1267
|
+
error: string | null;
|
|
1268
|
+
pluginVersion?: string | null;
|
|
1269
|
+
}
|
|
1270
|
+
export type PluginIssue = 'missing' | 'blocked' | 'restart_required' | 'malfunctioned' | 'unknown';
|
|
1271
|
+
export interface ServerConnectionStatus {
|
|
1272
|
+
serverId: string;
|
|
1273
|
+
serverName: string;
|
|
1274
|
+
serverType: ServerType;
|
|
1275
|
+
mode: 'realtime' | 'polling';
|
|
1276
|
+
state: SSEConnectionState;
|
|
1277
|
+
lastEventAt: string | null;
|
|
1278
|
+
since: string | null;
|
|
1279
|
+
error: string | null;
|
|
1280
|
+
pluginVersion: string | null;
|
|
1281
|
+
pluginUpdateAvailable: boolean;
|
|
1282
|
+
pluginIssue: PluginIssue | null;
|
|
1283
|
+
}
|
|
1284
|
+
export type TerminationTrigger = 'manual' | 'rule';
|
|
1285
|
+
export interface TerminationLogWithDetails {
|
|
1286
|
+
id: string;
|
|
1287
|
+
sessionId: string;
|
|
1288
|
+
serverId: string;
|
|
1289
|
+
serverName: string | null;
|
|
1290
|
+
serverUserId: string;
|
|
1291
|
+
trigger: TerminationTrigger;
|
|
1292
|
+
triggeredByUserId: string | null;
|
|
1293
|
+
triggeredByUsername: string | null;
|
|
1294
|
+
ruleId: string | null;
|
|
1295
|
+
ruleName: string | null;
|
|
1296
|
+
violationId: string | null;
|
|
1297
|
+
reason: string | null;
|
|
1298
|
+
success: boolean;
|
|
1299
|
+
errorMessage: string | null;
|
|
1300
|
+
createdAt: Date;
|
|
1301
|
+
mediaTitle: string | null;
|
|
1302
|
+
mediaType: MediaType | null;
|
|
1303
|
+
grandparentTitle: string | null;
|
|
1304
|
+
seasonNumber: number | null;
|
|
1305
|
+
episodeNumber: number | null;
|
|
1306
|
+
year: number | null;
|
|
1307
|
+
artistName: string | null;
|
|
1308
|
+
albumName: string | null;
|
|
1309
|
+
}
|
|
1310
|
+
export type PlexConnectionErrorCode = 'timeout' | 'dns' | 'refused' | 'unreachable' | 'reset' | 'tls' | 'http' | 'unknown';
|
|
1311
|
+
export interface PlexConnectionError {
|
|
1312
|
+
code: PlexConnectionErrorCode;
|
|
1313
|
+
message: string;
|
|
1314
|
+
status?: number;
|
|
1315
|
+
}
|
|
1316
|
+
export interface PlexDiscoveredConnection {
|
|
1317
|
+
uri: string;
|
|
1318
|
+
local: boolean;
|
|
1319
|
+
address: string;
|
|
1320
|
+
port: number;
|
|
1321
|
+
reachable: boolean;
|
|
1322
|
+
latencyMs: number | null;
|
|
1323
|
+
custom?: boolean;
|
|
1324
|
+
error?: PlexConnectionError;
|
|
1325
|
+
}
|
|
1326
|
+
export interface PlexDiscoveredServer {
|
|
1327
|
+
name: string;
|
|
1328
|
+
platform: string;
|
|
1329
|
+
version: string;
|
|
1330
|
+
clientIdentifier: string;
|
|
1331
|
+
recommendedUri: string | null;
|
|
1332
|
+
connections: PlexDiscoveredConnection[];
|
|
1333
|
+
}
|
|
1334
|
+
export interface PlexAvailableServersResponse {
|
|
1335
|
+
servers: PlexDiscoveredServer[];
|
|
1336
|
+
hasPlexToken: boolean;
|
|
1337
|
+
}
|
|
1338
|
+
export interface PlexAccount {
|
|
1339
|
+
id: string;
|
|
1340
|
+
plexAccountId: string;
|
|
1341
|
+
plexUsername: string | null;
|
|
1342
|
+
plexEmail: string | null;
|
|
1343
|
+
plexThumbnail: string | null;
|
|
1344
|
+
allowLogin: boolean;
|
|
1345
|
+
serverCount: number;
|
|
1346
|
+
createdAt: Date;
|
|
1347
|
+
}
|
|
1348
|
+
export interface PlexAccountsResponse {
|
|
1349
|
+
accounts: PlexAccount[];
|
|
1350
|
+
}
|
|
1351
|
+
export interface LinkPlexAccountRequest {
|
|
1352
|
+
pin: string;
|
|
1353
|
+
}
|
|
1354
|
+
export interface LinkPlexAccountResponse {
|
|
1355
|
+
account: PlexAccount;
|
|
1356
|
+
}
|
|
1357
|
+
export interface UnlinkPlexAccountResponse {
|
|
1358
|
+
success: boolean;
|
|
1359
|
+
}
|
|
1360
|
+
export type MaintenanceJobCategory = 'normalization' | 'backfill' | 'cleanup';
|
|
1361
|
+
export type MaintenanceJobType = 'normalize_players' | 'normalize_countries' | 'fix_imported_progress' | 'rebuild_timescale_views' | 'normalize_codecs' | 'normalize_resolutions' | 'backfill_user_dates' | 'backfill_library_snapshots' | 'normalize_library_snapshots' | 'cleanup_old_chunks' | 'full_aggregate_rebuild' | 'repair_corrupted_chunks' | 'backfill_session_identity';
|
|
1362
|
+
export type MaintenanceJobStatus = 'idle' | 'waiting' | 'running' | 'complete' | 'error';
|
|
1363
|
+
export interface MaintenanceJobProgress {
|
|
1364
|
+
type: MaintenanceJobType;
|
|
1365
|
+
status: MaintenanceJobStatus;
|
|
1366
|
+
totalRecords: number;
|
|
1367
|
+
processedRecords: number;
|
|
1368
|
+
updatedRecords: number;
|
|
1369
|
+
skippedRecords: number;
|
|
1370
|
+
errorRecords: number;
|
|
1371
|
+
message: string;
|
|
1372
|
+
startedAt?: string;
|
|
1373
|
+
completedAt?: string;
|
|
1374
|
+
/** Present when status='waiting' - what this job is waiting for */
|
|
1375
|
+
waitingFor?: HeavyOpsWaitingFor;
|
|
1376
|
+
}
|
|
1377
|
+
export interface MaintenanceJobResult {
|
|
1378
|
+
success: boolean;
|
|
1379
|
+
type: MaintenanceJobType;
|
|
1380
|
+
processed: number;
|
|
1381
|
+
updated: number;
|
|
1382
|
+
skipped: number;
|
|
1383
|
+
errors: number;
|
|
1384
|
+
durationMs: number;
|
|
1385
|
+
message: string;
|
|
1386
|
+
}
|
|
1387
|
+
export type RunningTaskType = 'library_sync' | 'tautulli_import' | 'jellystat_import' | 'maintenance';
|
|
1388
|
+
export interface RunningTask {
|
|
1389
|
+
/** Unique task identifier */
|
|
1390
|
+
id: string;
|
|
1391
|
+
/** Task type for categorization */
|
|
1392
|
+
type: RunningTaskType;
|
|
1393
|
+
/** Human-readable task name */
|
|
1394
|
+
name: string;
|
|
1395
|
+
/** Current status */
|
|
1396
|
+
status: 'pending' | 'waiting' | 'running' | 'complete' | 'error';
|
|
1397
|
+
/** Progress percentage (0-100), null if indeterminate */
|
|
1398
|
+
progress: number | null;
|
|
1399
|
+
/** Current status message */
|
|
1400
|
+
message: string;
|
|
1401
|
+
/** When the task started */
|
|
1402
|
+
startedAt: string;
|
|
1403
|
+
/** Additional context (e.g., server name, job type) */
|
|
1404
|
+
context?: string;
|
|
1405
|
+
/** What the task is waiting for (only when status is 'waiting') */
|
|
1406
|
+
waitingFor?: HeavyOpsWaitingFor;
|
|
1407
|
+
}
|
|
1408
|
+
export interface RunningTasksResponse {
|
|
1409
|
+
tasks: RunningTask[];
|
|
1410
|
+
}
|
|
1411
|
+
export type EngagementTier = 'abandoned' | 'sampled' | 'engaged' | 'watched' | 'rewatched' | 'unknown';
|
|
1412
|
+
export type UserBehaviorType = 'inactive' | 'sampler' | 'casual' | 'completionist' | 'rewatcher';
|
|
1413
|
+
export interface ContentEngagement {
|
|
1414
|
+
ratingKey: string;
|
|
1415
|
+
mediaTitle: string;
|
|
1416
|
+
showTitle: string | null;
|
|
1417
|
+
mediaType: MediaType;
|
|
1418
|
+
thumbPath: string | null;
|
|
1419
|
+
serverId: string | null;
|
|
1420
|
+
year: number | null;
|
|
1421
|
+
plays: number;
|
|
1422
|
+
completionPct: number;
|
|
1423
|
+
engagementTier: EngagementTier;
|
|
1424
|
+
cumulativeWatchedMs: number;
|
|
1425
|
+
validSessions: number;
|
|
1426
|
+
totalSessions: number;
|
|
1427
|
+
firstWatchedAt: Date;
|
|
1428
|
+
lastWatchedAt: Date;
|
|
1429
|
+
}
|
|
1430
|
+
export interface TopContentEngagement {
|
|
1431
|
+
ratingKey: string;
|
|
1432
|
+
title: string;
|
|
1433
|
+
showTitle: string | null;
|
|
1434
|
+
type: MediaType;
|
|
1435
|
+
thumbPath: string | null;
|
|
1436
|
+
serverId: string | null;
|
|
1437
|
+
year: number | null;
|
|
1438
|
+
totalPlays: number;
|
|
1439
|
+
totalWatchHours: number;
|
|
1440
|
+
uniqueViewers: number;
|
|
1441
|
+
validSessions: number;
|
|
1442
|
+
totalSessions: number;
|
|
1443
|
+
completions: number;
|
|
1444
|
+
rewatches: number;
|
|
1445
|
+
abandonments: number;
|
|
1446
|
+
completionRate: number;
|
|
1447
|
+
abandonmentRate: number;
|
|
1448
|
+
}
|
|
1449
|
+
export interface ShowEngagement {
|
|
1450
|
+
showTitle: string;
|
|
1451
|
+
thumbPath: string | null;
|
|
1452
|
+
serverId: string | null;
|
|
1453
|
+
year: number | null;
|
|
1454
|
+
totalEpisodeViews: number;
|
|
1455
|
+
totalWatchHours: number;
|
|
1456
|
+
uniqueViewers: number;
|
|
1457
|
+
avgEpisodesPerViewer: number;
|
|
1458
|
+
avgCompletionRate: number;
|
|
1459
|
+
bingeScore: number;
|
|
1460
|
+
validSessions: number;
|
|
1461
|
+
totalSessions: number;
|
|
1462
|
+
}
|
|
1463
|
+
export interface UserEngagementProfile {
|
|
1464
|
+
serverUserId: string;
|
|
1465
|
+
username: string;
|
|
1466
|
+
thumbUrl: string | null;
|
|
1467
|
+
identityName: string | null;
|
|
1468
|
+
contentStarted: number;
|
|
1469
|
+
totalPlays: number;
|
|
1470
|
+
totalWatchHours: number;
|
|
1471
|
+
validSessionCount: number;
|
|
1472
|
+
totalSessionCount: number;
|
|
1473
|
+
abandonedCount: number;
|
|
1474
|
+
sampledCount: number;
|
|
1475
|
+
engagedCount: number;
|
|
1476
|
+
watchedCount: number;
|
|
1477
|
+
rewatchedCount: number;
|
|
1478
|
+
completionRate: number;
|
|
1479
|
+
behaviorType: UserBehaviorType;
|
|
1480
|
+
favoriteMediaType: MediaType | null;
|
|
1481
|
+
}
|
|
1482
|
+
export interface EngagementTierBreakdown {
|
|
1483
|
+
tier: EngagementTier;
|
|
1484
|
+
count: number;
|
|
1485
|
+
percentage: number;
|
|
1486
|
+
}
|
|
1487
|
+
export interface EngagementStats {
|
|
1488
|
+
topContent: TopContentEngagement[];
|
|
1489
|
+
topShows: ShowEngagement[];
|
|
1490
|
+
engagementBreakdown: EngagementTierBreakdown[];
|
|
1491
|
+
userProfiles: UserEngagementProfile[];
|
|
1492
|
+
summary: {
|
|
1493
|
+
totalPlays: number;
|
|
1494
|
+
totalValidSessions: number;
|
|
1495
|
+
totalAllSessions: number;
|
|
1496
|
+
sessionInflationPct: number;
|
|
1497
|
+
avgCompletionRate: number;
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
export interface ShowStatsResponse {
|
|
1501
|
+
data: ShowEngagement[];
|
|
1502
|
+
total: number;
|
|
1503
|
+
}
|
|
1504
|
+
export interface VersionInfo {
|
|
1505
|
+
current: {
|
|
1506
|
+
version: string;
|
|
1507
|
+
tag: string | null;
|
|
1508
|
+
commit: string | null;
|
|
1509
|
+
buildDate: string | null;
|
|
1510
|
+
isPrerelease: boolean;
|
|
1511
|
+
};
|
|
1512
|
+
latest: {
|
|
1513
|
+
version: string;
|
|
1514
|
+
tag: string;
|
|
1515
|
+
releaseUrl: string;
|
|
1516
|
+
publishedAt: string;
|
|
1517
|
+
isPrerelease: boolean;
|
|
1518
|
+
releaseName: string | null;
|
|
1519
|
+
releaseNotes: string | null;
|
|
1520
|
+
} | null;
|
|
1521
|
+
updateAvailable: boolean;
|
|
1522
|
+
lastChecked: string | null;
|
|
1523
|
+
}
|
|
1524
|
+
export interface DeviceCompatibilityRow {
|
|
1525
|
+
deviceType: string;
|
|
1526
|
+
videoCodec: string;
|
|
1527
|
+
audioCodec: string;
|
|
1528
|
+
sessionCount: number;
|
|
1529
|
+
videoDirectCount: number;
|
|
1530
|
+
audioDirectCount: number;
|
|
1531
|
+
fullDirectCount: number;
|
|
1532
|
+
anyTranscodeCount: number;
|
|
1533
|
+
videoDirectPct: number;
|
|
1534
|
+
audioDirectPct: number;
|
|
1535
|
+
fullDirectPct: number;
|
|
1536
|
+
}
|
|
1537
|
+
export interface DeviceCompatibilityResponse {
|
|
1538
|
+
data: DeviceCompatibilityRow[];
|
|
1539
|
+
summary: {
|
|
1540
|
+
totalSessions: number;
|
|
1541
|
+
directPlayPct: number;
|
|
1542
|
+
uniqueDevices: number;
|
|
1543
|
+
uniqueCodecs: number;
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
export interface DeviceCompatibilityMatrix {
|
|
1547
|
+
codecs: string[];
|
|
1548
|
+
devices: {
|
|
1549
|
+
device: string;
|
|
1550
|
+
codecs: Record<string, {
|
|
1551
|
+
sessions: number;
|
|
1552
|
+
directPct: number;
|
|
1553
|
+
}>;
|
|
1554
|
+
}[];
|
|
1555
|
+
}
|
|
1556
|
+
export interface DeviceHealthRow {
|
|
1557
|
+
serverId: string;
|
|
1558
|
+
device: string;
|
|
1559
|
+
sessions: number;
|
|
1560
|
+
directPlayCount: number;
|
|
1561
|
+
transcodeCount: number;
|
|
1562
|
+
directPlayPct: number;
|
|
1563
|
+
}
|
|
1564
|
+
export interface DeviceHealthResponse {
|
|
1565
|
+
data: DeviceHealthRow[];
|
|
1566
|
+
}
|
|
1567
|
+
export interface TranscodeHotspotRow {
|
|
1568
|
+
serverId: string;
|
|
1569
|
+
device: string;
|
|
1570
|
+
videoCodec: string;
|
|
1571
|
+
audioCodec: string;
|
|
1572
|
+
sessions: number;
|
|
1573
|
+
directCount: number;
|
|
1574
|
+
transcodeCount: number;
|
|
1575
|
+
directPlayPct: number;
|
|
1576
|
+
pctOfTotalTranscodes: number;
|
|
1577
|
+
}
|
|
1578
|
+
export interface TranscodeHotspotsResponse {
|
|
1579
|
+
data: TranscodeHotspotRow[];
|
|
1580
|
+
totalTranscodes: number;
|
|
1581
|
+
}
|
|
1582
|
+
export interface TopTranscodingUserRow {
|
|
1583
|
+
serverId: string;
|
|
1584
|
+
serverUserId: string;
|
|
1585
|
+
username: string;
|
|
1586
|
+
identityName: string | null;
|
|
1587
|
+
avatar: string | null;
|
|
1588
|
+
totalSessions: number;
|
|
1589
|
+
directPlayCount: number;
|
|
1590
|
+
transcodeCount: number;
|
|
1591
|
+
directPlayPct: number;
|
|
1592
|
+
pctOfTotalTranscodes: number;
|
|
1593
|
+
}
|
|
1594
|
+
export interface TopTranscodingUsersResponse {
|
|
1595
|
+
data: TopTranscodingUserRow[];
|
|
1596
|
+
totalTranscodes: number;
|
|
1597
|
+
}
|
|
1598
|
+
export interface DailyBandwidthRow {
|
|
1599
|
+
date: string;
|
|
1600
|
+
/** Server this row belongs to - required for per-server chart series */
|
|
1601
|
+
serverId: string;
|
|
1602
|
+
sessions: number;
|
|
1603
|
+
/** Total data transferred in bytes */
|
|
1604
|
+
totalBytes: number;
|
|
1605
|
+
/** Total data transferred in GB (human-readable) */
|
|
1606
|
+
totalGb: number;
|
|
1607
|
+
avgBitrate: number;
|
|
1608
|
+
peakBitrate: number;
|
|
1609
|
+
totalDurationMs: number;
|
|
1610
|
+
avgBitrateMbps: number;
|
|
1611
|
+
totalHours: number;
|
|
1612
|
+
}
|
|
1613
|
+
export interface DailyBandwidthResponse {
|
|
1614
|
+
data: DailyBandwidthRow[];
|
|
1615
|
+
usingAggregate: boolean;
|
|
1616
|
+
}
|
|
1617
|
+
export interface BandwidthTopUser {
|
|
1618
|
+
username: string;
|
|
1619
|
+
/** Display name from linked identity (null if no linked user) */
|
|
1620
|
+
identityName: string | null;
|
|
1621
|
+
/** Avatar URL from server user */
|
|
1622
|
+
thumbUrl: string | null;
|
|
1623
|
+
/** Representative account id, used for navigation (routes to /users/:id) */
|
|
1624
|
+
serverUserId: string;
|
|
1625
|
+
/** Representative account's server - a merged identity is one row, bytes summed across servers */
|
|
1626
|
+
serverId: string;
|
|
1627
|
+
/** Total data transferred in bytes */
|
|
1628
|
+
totalBytes: number;
|
|
1629
|
+
/** Total data transferred in GB (human-readable) */
|
|
1630
|
+
totalGb: number;
|
|
1631
|
+
sessions: number;
|
|
1632
|
+
avgBitrate: number;
|
|
1633
|
+
totalDurationMs: number;
|
|
1634
|
+
avgBitrateMbps: number;
|
|
1635
|
+
totalHours: number;
|
|
1636
|
+
}
|
|
1637
|
+
export interface BandwidthTopUsersResponse {
|
|
1638
|
+
data: BandwidthTopUser[];
|
|
1639
|
+
}
|
|
1640
|
+
export interface BandwidthSummaryServerKpis {
|
|
1641
|
+
totalSessions: number;
|
|
1642
|
+
totalBytes: number;
|
|
1643
|
+
totalGb: number;
|
|
1644
|
+
avgBitrate: number;
|
|
1645
|
+
peakBitrate: number;
|
|
1646
|
+
minBitrate: number;
|
|
1647
|
+
medianBitrate: number;
|
|
1648
|
+
totalDurationMs: number;
|
|
1649
|
+
uniqueUsers: number;
|
|
1650
|
+
avgBitrateMbps: number;
|
|
1651
|
+
peakBitrateMbps: number;
|
|
1652
|
+
totalHours: number;
|
|
1653
|
+
}
|
|
1654
|
+
export interface BandwidthSummary {
|
|
1655
|
+
totalSessions: number;
|
|
1656
|
+
/** Total data transferred in bytes */
|
|
1657
|
+
totalBytes: number;
|
|
1658
|
+
/** Total data transferred in GB (human-readable) */
|
|
1659
|
+
totalGb: number;
|
|
1660
|
+
avgBitrate: number;
|
|
1661
|
+
peakBitrate: number;
|
|
1662
|
+
minBitrate: number;
|
|
1663
|
+
medianBitrate: number;
|
|
1664
|
+
totalDurationMs: number;
|
|
1665
|
+
uniqueUsers: number;
|
|
1666
|
+
avgBitrateMbps: number;
|
|
1667
|
+
peakBitrateMbps: number;
|
|
1668
|
+
totalHours: number;
|
|
1669
|
+
/** Per-server KPI breakdown keyed by server ID (present when multiple servers selected) */
|
|
1670
|
+
byServer?: Record<string, BandwidthSummaryServerKpis>;
|
|
1671
|
+
}
|
|
1672
|
+
export interface LibraryStatsResponse {
|
|
1673
|
+
totalItems: number;
|
|
1674
|
+
totalSizeBytes: string;
|
|
1675
|
+
movieCount: number;
|
|
1676
|
+
episodeCount: number;
|
|
1677
|
+
showCount: number;
|
|
1678
|
+
qualityBreakdown: {
|
|
1679
|
+
count4k: number;
|
|
1680
|
+
count1080p: number;
|
|
1681
|
+
count720p: number;
|
|
1682
|
+
countSd: number;
|
|
1683
|
+
};
|
|
1684
|
+
asOf: string | null;
|
|
1685
|
+
}
|
|
1686
|
+
export interface GrowthDataPoint {
|
|
1687
|
+
day: string;
|
|
1688
|
+
total: number;
|
|
1689
|
+
additions: number;
|
|
1690
|
+
/** Server that produced this data point (present in multi-server responses) */
|
|
1691
|
+
serverId: string;
|
|
1692
|
+
}
|
|
1693
|
+
export interface LibraryGrowthResponse {
|
|
1694
|
+
period: string;
|
|
1695
|
+
movies: GrowthDataPoint[];
|
|
1696
|
+
episodes: GrowthDataPoint[];
|
|
1697
|
+
music: GrowthDataPoint[];
|
|
1698
|
+
}
|
|
1699
|
+
export interface QualityDataPoint {
|
|
1700
|
+
day: string;
|
|
1701
|
+
totalItems: number;
|
|
1702
|
+
count4k: number;
|
|
1703
|
+
count1080p: number;
|
|
1704
|
+
count720p: number;
|
|
1705
|
+
countSd: number;
|
|
1706
|
+
pct4k: number;
|
|
1707
|
+
pct1080p: number;
|
|
1708
|
+
pct720p: number;
|
|
1709
|
+
pctSd: number;
|
|
1710
|
+
hevcCount: number;
|
|
1711
|
+
h264Count: number;
|
|
1712
|
+
av1Count: number;
|
|
1713
|
+
}
|
|
1714
|
+
export interface LibraryQualityResponse {
|
|
1715
|
+
period: string;
|
|
1716
|
+
mediaType: 'all' | 'movies' | 'shows';
|
|
1717
|
+
data: QualityDataPoint[];
|
|
1718
|
+
}
|
|
1719
|
+
export interface StorageHistoryPoint {
|
|
1720
|
+
day: string;
|
|
1721
|
+
totalSizeBytes: string;
|
|
1722
|
+
}
|
|
1723
|
+
export interface StoragePrediction {
|
|
1724
|
+
predicted: string;
|
|
1725
|
+
min: string;
|
|
1726
|
+
max: string;
|
|
1727
|
+
}
|
|
1728
|
+
export interface LibraryStorageResponse {
|
|
1729
|
+
current: {
|
|
1730
|
+
totalSizeBytes: string;
|
|
1731
|
+
totalItems: number;
|
|
1732
|
+
lastUpdated: string | null;
|
|
1733
|
+
};
|
|
1734
|
+
history: StorageHistoryPoint[];
|
|
1735
|
+
growthRate: {
|
|
1736
|
+
bytesPerDay: string;
|
|
1737
|
+
bytesPerWeek: string;
|
|
1738
|
+
bytesPerMonth: string;
|
|
1739
|
+
/** Days of history behind the numbers; below minDataDays they are unusable */
|
|
1740
|
+
fitDays?: number;
|
|
1741
|
+
/**
|
|
1742
|
+
* Which side of the mediaVersionsBackfilledAt changeover the fit ran on.
|
|
1743
|
+
* 'preChangeover' means old-semantics history is standing in until the
|
|
1744
|
+
* post-changeover side accumulates minDataDays of snapshots.
|
|
1745
|
+
*/
|
|
1746
|
+
basis?: 'current' | 'preChangeover';
|
|
1747
|
+
};
|
|
1748
|
+
predictions: {
|
|
1749
|
+
day30: StoragePrediction | null;
|
|
1750
|
+
day90: StoragePrediction | null;
|
|
1751
|
+
day365: StoragePrediction | null;
|
|
1752
|
+
confidence: 'high' | 'medium' | 'low' | null;
|
|
1753
|
+
minDataDays: number;
|
|
1754
|
+
currentDataDays: number;
|
|
1755
|
+
message?: string;
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
/** 'version' groups are one title whose single library item carries several
|
|
1759
|
+
* physical files; the others group distinct items (copies) by identity. */
|
|
1760
|
+
export type MatchType = 'imdb' | 'tmdb' | 'tvdb' | 'fuzzy' | 'version';
|
|
1761
|
+
/** One physical file of a duplicate item */
|
|
1762
|
+
export interface DuplicateItemVersion {
|
|
1763
|
+
resolution: string | null;
|
|
1764
|
+
videoCodec: string | null;
|
|
1765
|
+
fileSize: number | null;
|
|
1766
|
+
filePath: string | null;
|
|
1767
|
+
/**
|
|
1768
|
+
* The same physical file already listed elsewhere in the group (equal byte
|
|
1769
|
+
* size, the codebase-wide mirror heuristic). Jellyfin merged-version
|
|
1770
|
+
* libraries list every file under every library entry; mirrors keep the
|
|
1771
|
+
* listing honest without counting the file twice.
|
|
1772
|
+
*/
|
|
1773
|
+
isMirror?: boolean;
|
|
1774
|
+
}
|
|
1775
|
+
export interface DuplicateItem {
|
|
1776
|
+
id: string;
|
|
1777
|
+
serverId: string;
|
|
1778
|
+
serverName: string;
|
|
1779
|
+
libraryId: string | null;
|
|
1780
|
+
libraryName: string | null;
|
|
1781
|
+
title: string;
|
|
1782
|
+
year: number | null;
|
|
1783
|
+
mediaType: string;
|
|
1784
|
+
fileSize: number | null;
|
|
1785
|
+
resolution: string | null;
|
|
1786
|
+
versions: DuplicateItemVersion[];
|
|
1787
|
+
}
|
|
1788
|
+
export interface DuplicateGroup {
|
|
1789
|
+
matchKey: string;
|
|
1790
|
+
matchType: MatchType;
|
|
1791
|
+
confidence: number;
|
|
1792
|
+
serverCount: number;
|
|
1793
|
+
/** All copies live on one server (cross-library copies or one item's versions) */
|
|
1794
|
+
sameServer: boolean;
|
|
1795
|
+
items: DuplicateItem[];
|
|
1796
|
+
/**
|
|
1797
|
+
* Distinct physical files in the group after mirror dedup. Optional only
|
|
1798
|
+
* because responses cached before the field existed can still be served
|
|
1799
|
+
* for up to an hour; the server always sets it.
|
|
1800
|
+
*/
|
|
1801
|
+
uniqueFileCount?: number;
|
|
1802
|
+
/** Mirror-deduped bytes: the same physical file (equal size) counts once */
|
|
1803
|
+
totalStorageBytes: number;
|
|
1804
|
+
/** Bytes freed by keeping only the best-quality file */
|
|
1805
|
+
potentialSavingsBytes: number;
|
|
1806
|
+
}
|
|
1807
|
+
export interface DuplicatesSummary {
|
|
1808
|
+
totalGroups: number;
|
|
1809
|
+
totalDuplicateItems: number;
|
|
1810
|
+
totalPotentialSavingsBytes: number;
|
|
1811
|
+
byMatchType: {
|
|
1812
|
+
imdb: number;
|
|
1813
|
+
tmdb: number;
|
|
1814
|
+
tvdb: number;
|
|
1815
|
+
fuzzy: number;
|
|
1816
|
+
version: number;
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
export interface DuplicatesResponse {
|
|
1820
|
+
duplicates: DuplicateGroup[];
|
|
1821
|
+
summary: DuplicatesSummary;
|
|
1822
|
+
pagination: {
|
|
1823
|
+
page: number;
|
|
1824
|
+
pageSize: number;
|
|
1825
|
+
total: number;
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
export type StaleCategory = 'never_watched' | 'stale';
|
|
1829
|
+
export interface StaleItem {
|
|
1830
|
+
id: string;
|
|
1831
|
+
serverId: string;
|
|
1832
|
+
serverName: string;
|
|
1833
|
+
libraryId: string;
|
|
1834
|
+
libraryName: string;
|
|
1835
|
+
title: string;
|
|
1836
|
+
mediaType: string;
|
|
1837
|
+
year: number | null;
|
|
1838
|
+
fileSize: number | null;
|
|
1839
|
+
resolution: string | null;
|
|
1840
|
+
addedAt: string;
|
|
1841
|
+
lastWatched: string | null;
|
|
1842
|
+
watchCount: number;
|
|
1843
|
+
category: StaleCategory;
|
|
1844
|
+
daysStale: number;
|
|
1845
|
+
}
|
|
1846
|
+
export interface StaleSummary {
|
|
1847
|
+
neverWatched: {
|
|
1848
|
+
count: number;
|
|
1849
|
+
sizeBytes: number;
|
|
1850
|
+
};
|
|
1851
|
+
stale: {
|
|
1852
|
+
count: number;
|
|
1853
|
+
sizeBytes: number;
|
|
1854
|
+
};
|
|
1855
|
+
total: {
|
|
1856
|
+
count: number;
|
|
1857
|
+
sizeBytes: number;
|
|
1858
|
+
};
|
|
1859
|
+
threshold: {
|
|
1860
|
+
days: number;
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
export interface StaleResponse {
|
|
1864
|
+
items: StaleItem[];
|
|
1865
|
+
summary: StaleSummary;
|
|
1866
|
+
pagination: {
|
|
1867
|
+
page: number;
|
|
1868
|
+
pageSize: number;
|
|
1869
|
+
total: number;
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* A single "most watched" row returned by the watch endpoint.
|
|
1874
|
+
* When multiple servers are in scope, the same title is collapsed
|
|
1875
|
+
* into one row (deduped by external ID match key) and serverIds
|
|
1876
|
+
* lists every server that has a copy of the title.
|
|
1877
|
+
*/
|
|
1878
|
+
export interface WatchItem {
|
|
1879
|
+
id: string;
|
|
1880
|
+
/** Primary server for this title (lowest sort value when deduped across servers). */
|
|
1881
|
+
serverId: string;
|
|
1882
|
+
serverName: string;
|
|
1883
|
+
libraryId: string;
|
|
1884
|
+
title: string;
|
|
1885
|
+
mediaType: string;
|
|
1886
|
+
year: number | null;
|
|
1887
|
+
fileSize: number | null;
|
|
1888
|
+
resolution: string | null;
|
|
1889
|
+
addedAt: string;
|
|
1890
|
+
/** Total play count summed across all servers for this title. */
|
|
1891
|
+
watchCount: number;
|
|
1892
|
+
/** Total watch duration summed across all servers for this title. */
|
|
1893
|
+
totalWatchMs: number;
|
|
1894
|
+
lastWatchedAt: string | null;
|
|
1895
|
+
/** All server IDs that own a copy of this title (for per-title color dots). */
|
|
1896
|
+
serverIds: string[];
|
|
1897
|
+
}
|
|
1898
|
+
export interface WatchSummary {
|
|
1899
|
+
/** Distinct title count (deduped by external ID). */
|
|
1900
|
+
totalItems: number;
|
|
1901
|
+
/** Distinct titles with at least one play (deduped by external ID). */
|
|
1902
|
+
watchedCount: number;
|
|
1903
|
+
unwatchedCount: number;
|
|
1904
|
+
watchedPct: number;
|
|
1905
|
+
/** Sum of all watch durations across all servers. */
|
|
1906
|
+
totalWatchMs: number;
|
|
1907
|
+
avgWatchesPerItem: number;
|
|
1908
|
+
/** Distinct titles fully completed (deduped by external ID), if available. */
|
|
1909
|
+
completedCount: number;
|
|
1910
|
+
}
|
|
1911
|
+
export interface WatchResponse {
|
|
1912
|
+
items: WatchItem[];
|
|
1913
|
+
summary: WatchSummary;
|
|
1914
|
+
pagination: {
|
|
1915
|
+
page: number;
|
|
1916
|
+
pageSize: number;
|
|
1917
|
+
total: number;
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
export type CompletionStatus = 'completed' | 'in_progress' | 'not_started';
|
|
1921
|
+
export interface CompletionItem {
|
|
1922
|
+
id: string;
|
|
1923
|
+
serverId: string;
|
|
1924
|
+
serverName: string;
|
|
1925
|
+
title: string;
|
|
1926
|
+
mediaType: string;
|
|
1927
|
+
completionPct: number;
|
|
1928
|
+
watchedMs: number;
|
|
1929
|
+
runtimeMs: number;
|
|
1930
|
+
showTitle: string | null;
|
|
1931
|
+
seasonNumber: number | null;
|
|
1932
|
+
episodeNumber: number | null;
|
|
1933
|
+
status: CompletionStatus;
|
|
1934
|
+
lastWatchedAt: string | null;
|
|
1935
|
+
}
|
|
1936
|
+
export interface SeasonCompletion {
|
|
1937
|
+
serverId: string;
|
|
1938
|
+
serverName: string;
|
|
1939
|
+
showTitle: string;
|
|
1940
|
+
seasonNumber: number;
|
|
1941
|
+
totalEpisodes: number;
|
|
1942
|
+
completedEpisodes: number;
|
|
1943
|
+
inProgressEpisodes: number;
|
|
1944
|
+
completionPct: number;
|
|
1945
|
+
status: CompletionStatus;
|
|
1946
|
+
}
|
|
1947
|
+
export interface SeriesCompletion {
|
|
1948
|
+
serverId: string;
|
|
1949
|
+
serverName: string;
|
|
1950
|
+
showTitle: string;
|
|
1951
|
+
totalSeasons: number;
|
|
1952
|
+
completedSeasons: number;
|
|
1953
|
+
totalEpisodes: number;
|
|
1954
|
+
completedEpisodes: number;
|
|
1955
|
+
avgSeasonCompletionPct: number;
|
|
1956
|
+
status: CompletionStatus;
|
|
1957
|
+
}
|
|
1958
|
+
export interface CompletionSummary {
|
|
1959
|
+
totalItems: number;
|
|
1960
|
+
completedCount: number;
|
|
1961
|
+
inProgressCount: number;
|
|
1962
|
+
notStartedCount: number;
|
|
1963
|
+
overallCompletionPct: number;
|
|
1964
|
+
}
|
|
1965
|
+
export interface CompletionPaginationInfo {
|
|
1966
|
+
page: number;
|
|
1967
|
+
pageSize: number;
|
|
1968
|
+
total: number;
|
|
1969
|
+
}
|
|
1970
|
+
export type CompletionResponse = {
|
|
1971
|
+
items: CompletionItem[];
|
|
1972
|
+
summary: CompletionSummary;
|
|
1973
|
+
pagination: CompletionPaginationInfo;
|
|
1974
|
+
} | {
|
|
1975
|
+
seasons: SeasonCompletion[];
|
|
1976
|
+
summary: CompletionSummary;
|
|
1977
|
+
pagination: CompletionPaginationInfo;
|
|
1978
|
+
} | {
|
|
1979
|
+
series: SeriesCompletion[];
|
|
1980
|
+
summary: CompletionSummary;
|
|
1981
|
+
pagination: CompletionPaginationInfo;
|
|
1982
|
+
};
|
|
1983
|
+
export type WatchedState = 'watched' | 'partial' | 'unwatched';
|
|
1984
|
+
export interface CatalogRowServerEntry {
|
|
1985
|
+
serverId: string;
|
|
1986
|
+
addedAt: string;
|
|
1987
|
+
videoResolution: string | null;
|
|
1988
|
+
fileSize: number | null;
|
|
1989
|
+
/** Active physical files of this copy (1 for single-version titles) */
|
|
1990
|
+
versionCount: number;
|
|
1991
|
+
}
|
|
1992
|
+
export interface CatalogRow {
|
|
1993
|
+
mediaId: string;
|
|
1994
|
+
mediaType: 'movie' | 'show';
|
|
1995
|
+
title: string;
|
|
1996
|
+
year: number | null;
|
|
1997
|
+
genres: string[];
|
|
1998
|
+
posterUrl: string | null;
|
|
1999
|
+
posterVersion: string | null;
|
|
2000
|
+
dominantColor: string | null;
|
|
2001
|
+
servers: CatalogRowServerEntry[];
|
|
2002
|
+
resolutionBest: string | null;
|
|
2003
|
+
watchedState: WatchedState;
|
|
2004
|
+
/** Same probe, scoped to the requesting admin's own identity instead of
|
|
2005
|
+
* the anyone-grain lens - "have I personally watched this". */
|
|
2006
|
+
watchedStateSelf: WatchedState;
|
|
2007
|
+
plays: number;
|
|
2008
|
+
viewers: number;
|
|
2009
|
+
}
|
|
2010
|
+
export interface CatalogResponseMeta {
|
|
2011
|
+
/** Absolute row offset this window starts at, echoing the request. */
|
|
2012
|
+
offset: number;
|
|
2013
|
+
pageSize: number;
|
|
2014
|
+
totalItems: number;
|
|
2015
|
+
totalFileSize: number;
|
|
2016
|
+
}
|
|
2017
|
+
export interface CatalogResponse {
|
|
2018
|
+
data: CatalogRow[];
|
|
2019
|
+
meta: CatalogResponseMeta;
|
|
2020
|
+
}
|
|
2021
|
+
export interface CatalogLetterBucket {
|
|
2022
|
+
/** '#' or 'A'..'Z'. */
|
|
2023
|
+
letter: string;
|
|
2024
|
+
count: number;
|
|
2025
|
+
}
|
|
2026
|
+
export interface CatalogLettersResponse {
|
|
2027
|
+
letters: CatalogLetterBucket[];
|
|
2028
|
+
}
|
|
2029
|
+
export type ShelfRow = Omit<CatalogRow, 'plays' | 'viewers' | 'watchedStateSelf'>;
|
|
2030
|
+
/** Same period convention as statsQuerySchema/TimeRangeValue on the frontend. */
|
|
2031
|
+
export type ShelvesPeriod = z.infer<typeof statPeriodSchema>;
|
|
2032
|
+
export interface RecentlyAddedShelfRow extends ShelfRow {
|
|
2033
|
+
/** Newly-tracked episode count for a show card; always null for movies. */
|
|
2034
|
+
newEpisodes: number | null;
|
|
2035
|
+
}
|
|
2036
|
+
export interface MostPopularShelfRow extends ShelfRow {
|
|
2037
|
+
plays: number;
|
|
2038
|
+
viewers: number;
|
|
2039
|
+
rank: number;
|
|
2040
|
+
}
|
|
2041
|
+
export interface DeadWeightRow extends ShelfRow {
|
|
2042
|
+
fileBytes: number;
|
|
2043
|
+
/** Null when the title's canonical media row has never had latest_added_at
|
|
2044
|
+
* stamped (no active library copy has ever been synced). */
|
|
2045
|
+
addedAt: string | null;
|
|
2046
|
+
}
|
|
2047
|
+
export interface ShelvesKpiWatchedInPeriod {
|
|
2048
|
+
/** Distinct canonical titles (movies + shows) with >=1 play in the window. */
|
|
2049
|
+
titlesTouched: number;
|
|
2050
|
+
/** Total canonical titles (movies + shows) in scope, all-time. */
|
|
2051
|
+
totalTitles: number;
|
|
2052
|
+
}
|
|
2053
|
+
export interface ShelvesKpiNewlyAdded {
|
|
2054
|
+
/** Canonical titles (movies + shows) added within the window. */
|
|
2055
|
+
count: number;
|
|
2056
|
+
totalBytes: number;
|
|
2057
|
+
/** Of the titles added in the window, how many have ever been played. */
|
|
2058
|
+
playedCount: number;
|
|
2059
|
+
}
|
|
2060
|
+
export interface ShelvesKpiDeadWeight {
|
|
2061
|
+
/** All-time never-watched canonical title count (not window-scoped). */
|
|
2062
|
+
count: number;
|
|
2063
|
+
totalBytes: number;
|
|
2064
|
+
}
|
|
2065
|
+
export interface ShelvesKpis {
|
|
2066
|
+
watchedInPeriod: ShelvesKpiWatchedInPeriod;
|
|
2067
|
+
/** Total watched time across the window, in seconds. */
|
|
2068
|
+
hoursWatched: number;
|
|
2069
|
+
newlyAdded: ShelvesKpiNewlyAdded;
|
|
2070
|
+
/** Omitted when the request opts out via includeDeadWeight=false. */
|
|
2071
|
+
deadWeight?: ShelvesKpiDeadWeight;
|
|
2072
|
+
}
|
|
2073
|
+
export interface ShelvesResponseMeta {
|
|
2074
|
+
movies: number;
|
|
2075
|
+
shows: number;
|
|
2076
|
+
totalFileSize: number;
|
|
2077
|
+
}
|
|
2078
|
+
export interface ShelvesResponse {
|
|
2079
|
+
period: ShelvesPeriod;
|
|
2080
|
+
recentlyAddedMovies: RecentlyAddedShelfRow[];
|
|
2081
|
+
recentlyAddedShows: RecentlyAddedShelfRow[];
|
|
2082
|
+
mostPopularMovies: MostPopularShelfRow[];
|
|
2083
|
+
mostPopularShows: MostPopularShelfRow[];
|
|
2084
|
+
deadWeight?: DeadWeightRow[];
|
|
2085
|
+
kpis: ShelvesKpis;
|
|
2086
|
+
meta: ShelvesResponseMeta;
|
|
2087
|
+
}
|
|
2088
|
+
export interface GenreRow {
|
|
2089
|
+
genre: string;
|
|
2090
|
+
itemCount: number;
|
|
2091
|
+
plays: number;
|
|
2092
|
+
watchTimeMs: number;
|
|
2093
|
+
}
|
|
2094
|
+
export interface GenresResponse {
|
|
2095
|
+
data: GenreRow[];
|
|
2096
|
+
}
|
|
2097
|
+
/** One physical file of a library copy */
|
|
2098
|
+
export interface MediaVersionEntry {
|
|
2099
|
+
resolution: string | null;
|
|
2100
|
+
videoCodec: string | null;
|
|
2101
|
+
audioCodec: string | null;
|
|
2102
|
+
dynamicRange: string | null;
|
|
2103
|
+
container: string | null;
|
|
2104
|
+
fileSize: number | null;
|
|
2105
|
+
}
|
|
2106
|
+
export interface MediaAvailabilityEntry {
|
|
2107
|
+
serverId: string;
|
|
2108
|
+
serverType: string;
|
|
2109
|
+
libraryId: string;
|
|
2110
|
+
/** Library display name, null until that server's library sync has recorded it. */
|
|
2111
|
+
libraryName: string | null;
|
|
2112
|
+
ratingKey: string;
|
|
2113
|
+
addedAt: string;
|
|
2114
|
+
removedAt: string | null;
|
|
2115
|
+
videoResolution: string | null;
|
|
2116
|
+
fileSize: number | null;
|
|
2117
|
+
/** Show rows only: summed episode file bytes for this server+library; null for movies/episodes. */
|
|
2118
|
+
episodeFileSize: number | null;
|
|
2119
|
+
/** Show rows only: distinct episode resolutions ordered by frequency desc; null for movies/episodes. */
|
|
2120
|
+
episodeResolutions: string[] | null;
|
|
2121
|
+
/** Show rows only: active episode count for this server+library; null for movies/episodes. */
|
|
2122
|
+
episodeCount: number | null;
|
|
2123
|
+
/** Physical files of this copy, largest first. Empty for containers. */
|
|
2124
|
+
versions: MediaVersionEntry[];
|
|
2125
|
+
}
|
|
2126
|
+
export interface MediaDetailResponse {
|
|
2127
|
+
id: string;
|
|
2128
|
+
mediaType: string;
|
|
2129
|
+
title: string;
|
|
2130
|
+
year: number | null;
|
|
2131
|
+
imdbId: string | null;
|
|
2132
|
+
tmdbId: number | null;
|
|
2133
|
+
tvdbId: number | null;
|
|
2134
|
+
genres: string[] | null;
|
|
2135
|
+
showMediaId: string | null;
|
|
2136
|
+
mergedIds: string[];
|
|
2137
|
+
availability: MediaAvailabilityEntry[];
|
|
2138
|
+
seasonCount: number | null;
|
|
2139
|
+
episodeCount: number | null;
|
|
2140
|
+
}
|
|
2141
|
+
export interface MediaChildEntry {
|
|
2142
|
+
id: string;
|
|
2143
|
+
mediaType: 'season' | 'episode';
|
|
2144
|
+
title: string;
|
|
2145
|
+
seasonNumber: number | null;
|
|
2146
|
+
episodeCount: number | null;
|
|
2147
|
+
episodeNumber: number | null;
|
|
2148
|
+
imdbId: string | null;
|
|
2149
|
+
tmdbId: number | null;
|
|
2150
|
+
tvdbId: number | null;
|
|
2151
|
+
showMediaId: string | null;
|
|
2152
|
+
genres: string[] | null;
|
|
2153
|
+
}
|
|
2154
|
+
export interface MediaChildrenResponse {
|
|
2155
|
+
data: MediaChildEntry[];
|
|
2156
|
+
}
|
|
2157
|
+
export interface MediaStatsMeasures {
|
|
2158
|
+
plays: number;
|
|
2159
|
+
watchTimeMs: number;
|
|
2160
|
+
uniqueUsers: number;
|
|
2161
|
+
}
|
|
2162
|
+
export interface MediaStatsWindow {
|
|
2163
|
+
combined: MediaStatsMeasures;
|
|
2164
|
+
perServer: (MediaStatsMeasures & {
|
|
2165
|
+
serverId: string;
|
|
2166
|
+
serverName: string | null;
|
|
2167
|
+
})[];
|
|
2168
|
+
}
|
|
2169
|
+
export interface MediaStatsResponse {
|
|
2170
|
+
mediaId: string;
|
|
2171
|
+
mediaType: string;
|
|
2172
|
+
windows: {
|
|
2173
|
+
all_time: MediaStatsWindow;
|
|
2174
|
+
last_30: MediaStatsWindow;
|
|
2175
|
+
last_7: MediaStatsWindow;
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
export interface MediaWatcherEntry {
|
|
2179
|
+
user: {
|
|
2180
|
+
serverUserId: string;
|
|
2181
|
+
userId: string;
|
|
2182
|
+
serverId: string;
|
|
2183
|
+
username: string | null;
|
|
2184
|
+
identityName: string | null;
|
|
2185
|
+
/** Identity thumbnail when set, else the server account's avatar. */
|
|
2186
|
+
thumb: string | null;
|
|
2187
|
+
};
|
|
2188
|
+
plays: number;
|
|
2189
|
+
watchTimeMs: number;
|
|
2190
|
+
completionPct: number | null;
|
|
2191
|
+
lastWatchedDay: string | null;
|
|
2192
|
+
distinctEpisodesWatched: number | null;
|
|
2193
|
+
}
|
|
2194
|
+
export interface MediaWatchersResponse {
|
|
2195
|
+
mediaId: string;
|
|
2196
|
+
mediaType: string;
|
|
2197
|
+
window: 'all_time' | 'last_30' | 'last_7';
|
|
2198
|
+
watchers: MediaWatcherEntry[];
|
|
2199
|
+
}
|
|
2200
|
+
export interface MediaPlatformBreakdownEntry {
|
|
2201
|
+
platform: string | null;
|
|
2202
|
+
player: string | null;
|
|
2203
|
+
plays: number;
|
|
2204
|
+
watchTimeMs: number;
|
|
2205
|
+
}
|
|
2206
|
+
export interface MediaPlatformBreakdownResponse {
|
|
2207
|
+
data: MediaPlatformBreakdownEntry[];
|
|
2208
|
+
}
|
|
2209
|
+
export interface SeasonHeatEpisode {
|
|
2210
|
+
episodeNumber: number | null;
|
|
2211
|
+
watchedState: WatchedState;
|
|
2212
|
+
}
|
|
2213
|
+
export interface SeasonHeatSeason {
|
|
2214
|
+
seasonNumber: number | null;
|
|
2215
|
+
title: string;
|
|
2216
|
+
year: number | null;
|
|
2217
|
+
episodeCount: number;
|
|
2218
|
+
watchedCount: number;
|
|
2219
|
+
watchedPct: number;
|
|
2220
|
+
episodes: SeasonHeatEpisode[];
|
|
2221
|
+
}
|
|
2222
|
+
export interface MediaSeasonHeatResponse {
|
|
2223
|
+
mediaId: string;
|
|
2224
|
+
seasons: SeasonHeatSeason[];
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* A single binge-show row.
|
|
2228
|
+
* When multiple servers are in scope, episodes of the same show across
|
|
2229
|
+
* different servers are collapsed into one row (deduped by show matchKey).
|
|
2230
|
+
*/
|
|
2231
|
+
export interface BingeShow {
|
|
2232
|
+
showTitle: string;
|
|
2233
|
+
/** Server with the most episodes watched in this binge journey. */
|
|
2234
|
+
primaryServerId: string;
|
|
2235
|
+
thumbPath: string | null;
|
|
2236
|
+
totalEpisodeWatches: number;
|
|
2237
|
+
consecutiveEpisodes: number;
|
|
2238
|
+
consecutivePct: number;
|
|
2239
|
+
avgGapMinutes: number;
|
|
2240
|
+
bingeScore: number;
|
|
2241
|
+
maxEpisodesInOneDay: number;
|
|
2242
|
+
/** All server IDs involved in this binge journey. */
|
|
2243
|
+
serverIds: string[];
|
|
2244
|
+
}
|
|
2245
|
+
export interface HourlyDistribution {
|
|
2246
|
+
hour: number;
|
|
2247
|
+
watchCount: number;
|
|
2248
|
+
totalWatchMs: number;
|
|
2249
|
+
pctOfTotal: number;
|
|
2250
|
+
}
|
|
2251
|
+
export interface MonthlyTrend {
|
|
2252
|
+
month: string;
|
|
2253
|
+
watchCount: number;
|
|
2254
|
+
totalWatchMs: number;
|
|
2255
|
+
uniqueItems: number;
|
|
2256
|
+
avgWatchesPerDay: number;
|
|
2257
|
+
}
|
|
2258
|
+
export interface PatternsResponse {
|
|
2259
|
+
bingeShows: BingeShow[];
|
|
2260
|
+
peakTimes: {
|
|
2261
|
+
hourlyDistribution: HourlyDistribution[];
|
|
2262
|
+
peakHour: number;
|
|
2263
|
+
peakDayOfWeek: number;
|
|
2264
|
+
};
|
|
2265
|
+
seasonalTrends: {
|
|
2266
|
+
monthlyTrends: MonthlyTrend[];
|
|
2267
|
+
busiestMonth: string;
|
|
2268
|
+
quietestMonth: string;
|
|
2269
|
+
};
|
|
2270
|
+
summary: {
|
|
2271
|
+
totalWatchSessions: number;
|
|
2272
|
+
avgSessionsPerDay: number;
|
|
2273
|
+
bingeSessionsPct: number;
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
export type ValueCategory = 'low_value' | 'moderate_value' | 'high_value';
|
|
2277
|
+
export interface RoiItem {
|
|
2278
|
+
id: string;
|
|
2279
|
+
serverId: string;
|
|
2280
|
+
serverName: string;
|
|
2281
|
+
title: string;
|
|
2282
|
+
mediaType: string;
|
|
2283
|
+
year: number | null;
|
|
2284
|
+
fileSizeBytes: number;
|
|
2285
|
+
fileSizeGb: number;
|
|
2286
|
+
watchCount: number;
|
|
2287
|
+
totalWatchMs: number;
|
|
2288
|
+
totalWatchHours: number;
|
|
2289
|
+
lastWatchedAt: string | null;
|
|
2290
|
+
daysSinceLastWatch: number | null;
|
|
2291
|
+
watchHoursPerGb: number;
|
|
2292
|
+
valueScore: number;
|
|
2293
|
+
valueCategory: ValueCategory;
|
|
2294
|
+
suggestDeletion: boolean;
|
|
2295
|
+
}
|
|
2296
|
+
export interface RoiSummary {
|
|
2297
|
+
totalItems: number;
|
|
2298
|
+
totalStorageGb: number;
|
|
2299
|
+
totalWatchHours: number;
|
|
2300
|
+
avgWatchHoursPerGb: number;
|
|
2301
|
+
lowValueItems: number;
|
|
2302
|
+
lowValueStorageGb: number;
|
|
2303
|
+
potentialSavingsGb: number;
|
|
2304
|
+
}
|
|
2305
|
+
export interface ValueThresholds {
|
|
2306
|
+
movie: {
|
|
2307
|
+
lowValue: number;
|
|
2308
|
+
highValue: number;
|
|
2309
|
+
};
|
|
2310
|
+
episode: {
|
|
2311
|
+
lowValue: number;
|
|
2312
|
+
highValue: number;
|
|
2313
|
+
};
|
|
2314
|
+
show: {
|
|
2315
|
+
lowValue: number;
|
|
2316
|
+
highValue: number;
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
export interface RoiResponse {
|
|
2320
|
+
items: RoiItem[];
|
|
2321
|
+
summary: RoiSummary;
|
|
2322
|
+
thresholds: ValueThresholds;
|
|
2323
|
+
pagination: {
|
|
2324
|
+
page: number;
|
|
2325
|
+
pageSize: number;
|
|
2326
|
+
total: number;
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
export interface TopMovie {
|
|
2330
|
+
ratingKey: string;
|
|
2331
|
+
title: string;
|
|
2332
|
+
year: number | null;
|
|
2333
|
+
thumbPath: string | null;
|
|
2334
|
+
serverId: string;
|
|
2335
|
+
/** All server IDs that own a copy of this title (for per-title color dots). */
|
|
2336
|
+
serverIds?: string[];
|
|
2337
|
+
totalPlays: number;
|
|
2338
|
+
totalWatchHours: number;
|
|
2339
|
+
uniqueViewers: number;
|
|
2340
|
+
completionRate: number;
|
|
2341
|
+
}
|
|
2342
|
+
export interface TopMoviesSummary {
|
|
2343
|
+
totalMovies: number;
|
|
2344
|
+
totalWatchHours: number;
|
|
2345
|
+
}
|
|
2346
|
+
export interface TopMoviesResponse {
|
|
2347
|
+
items: TopMovie[];
|
|
2348
|
+
summary: TopMoviesSummary;
|
|
2349
|
+
pagination: {
|
|
2350
|
+
page: number;
|
|
2351
|
+
pageSize: number;
|
|
2352
|
+
total: number;
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
export interface TopShow {
|
|
2356
|
+
showTitle: string;
|
|
2357
|
+
year: number | null;
|
|
2358
|
+
thumbPath: string | null;
|
|
2359
|
+
serverId: string;
|
|
2360
|
+
/** All server IDs that own a copy of this title (for per-title color dots). */
|
|
2361
|
+
serverIds?: string[];
|
|
2362
|
+
totalEpisodeViews: number;
|
|
2363
|
+
totalWatchHours: number;
|
|
2364
|
+
uniqueViewers: number;
|
|
2365
|
+
avgCompletionRate: number;
|
|
2366
|
+
bingeScore: number;
|
|
2367
|
+
}
|
|
2368
|
+
export interface TopShowsSummary {
|
|
2369
|
+
totalShows: number;
|
|
2370
|
+
totalWatchHours: number;
|
|
2371
|
+
}
|
|
2372
|
+
export interface TopShowsResponse {
|
|
2373
|
+
items: TopShow[];
|
|
2374
|
+
summary: TopShowsSummary;
|
|
2375
|
+
pagination: {
|
|
2376
|
+
page: number;
|
|
2377
|
+
pageSize: number;
|
|
2378
|
+
total: number;
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
/** Single codec entry with count and percentage */
|
|
2382
|
+
export interface CodecEntry {
|
|
2383
|
+
codec: string;
|
|
2384
|
+
count: number;
|
|
2385
|
+
percentage: number;
|
|
2386
|
+
}
|
|
2387
|
+
/** Codec breakdown for a category (video, audio, or music) */
|
|
2388
|
+
export interface CodecBreakdown {
|
|
2389
|
+
codecs: CodecEntry[];
|
|
2390
|
+
total: number;
|
|
2391
|
+
}
|
|
2392
|
+
/** Response from /library/codecs endpoint */
|
|
2393
|
+
export interface LibraryCodecsResponse {
|
|
2394
|
+
/** Video codecs for movies and episodes */
|
|
2395
|
+
video: CodecBreakdown;
|
|
2396
|
+
/** Audio codecs for movies and episodes */
|
|
2397
|
+
audio: CodecBreakdown;
|
|
2398
|
+
/** Audio channel configurations (Stereo, 5.1, 7.1, etc.) for movies and episodes */
|
|
2399
|
+
channels: CodecBreakdown;
|
|
2400
|
+
/** Audio codecs for music tracks */
|
|
2401
|
+
music: CodecBreakdown;
|
|
2402
|
+
}
|
|
2403
|
+
/** Single resolution entry with count and percentage */
|
|
2404
|
+
export interface ResolutionEntry {
|
|
2405
|
+
resolution: string;
|
|
2406
|
+
count: number;
|
|
2407
|
+
percentage: number;
|
|
2408
|
+
}
|
|
2409
|
+
/** Resolution breakdown for a media type */
|
|
2410
|
+
export interface ResolutionBreakdown {
|
|
2411
|
+
count4k: number;
|
|
2412
|
+
count1080p: number;
|
|
2413
|
+
count720p: number;
|
|
2414
|
+
countSd: number;
|
|
2415
|
+
total: number;
|
|
2416
|
+
entries: ResolutionEntry[];
|
|
2417
|
+
}
|
|
2418
|
+
/** Response from /library/resolution endpoint */
|
|
2419
|
+
export interface LibraryResolutionResponse {
|
|
2420
|
+
/** Resolution breakdown for movies */
|
|
2421
|
+
movies: ResolutionBreakdown;
|
|
2422
|
+
/** Resolution breakdown for TV episodes */
|
|
2423
|
+
tv: ResolutionBreakdown;
|
|
2424
|
+
}
|
|
2425
|
+
/** One server's library, for the catalog browse Library select. Grouped by
|
|
2426
|
+
* server on the frontend when the account has more than one. */
|
|
2427
|
+
export interface LibraryOption {
|
|
2428
|
+
serverId: string;
|
|
2429
|
+
serverName: string;
|
|
2430
|
+
libraryId: string;
|
|
2431
|
+
name: string;
|
|
2432
|
+
mediaType: string;
|
|
2433
|
+
}
|
|
2434
|
+
/** Response from /library/libraries endpoint */
|
|
2435
|
+
export interface LibrariesResponse {
|
|
2436
|
+
data: LibraryOption[];
|
|
2437
|
+
}
|
|
2438
|
+
export interface BackupMetadata {
|
|
2439
|
+
format: 1;
|
|
2440
|
+
createdAt: string;
|
|
2441
|
+
app: {
|
|
2442
|
+
version: string;
|
|
2443
|
+
commit: string;
|
|
2444
|
+
tag: string;
|
|
2445
|
+
};
|
|
2446
|
+
database: {
|
|
2447
|
+
pgVersion: string;
|
|
2448
|
+
migrationCount: number;
|
|
2449
|
+
latestMigration: string;
|
|
2450
|
+
tableCount: number;
|
|
2451
|
+
databaseSize: number;
|
|
2452
|
+
timescaleVersion: string;
|
|
2453
|
+
timescaleToolkitVersion: string | null;
|
|
2454
|
+
};
|
|
2455
|
+
counts: {
|
|
2456
|
+
sessions: number;
|
|
2457
|
+
users: number;
|
|
2458
|
+
servers: number;
|
|
2459
|
+
rules: number;
|
|
2460
|
+
libraryItems: number;
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
export type RestorePhase = 'validating' | 'creating_restore_point' | 'shutting_down' | 'restoring_database' | 'running_migrations' | 'rebuilding_aggregates' | 'restarting' | 'complete' | 'failed';
|
|
2464
|
+
/** Ordered restore phases (excludes 'failed') for progress UI rendering. */
|
|
2465
|
+
export declare const RESTORE_PHASES: Exclude<RestorePhase, 'failed'>[];
|
|
2466
|
+
export interface RestoreProgress {
|
|
2467
|
+
phase: RestorePhase;
|
|
2468
|
+
message: string;
|
|
2469
|
+
startedAt: string;
|
|
2470
|
+
error?: string;
|
|
2471
|
+
/** When phase is 'failed', indicates which phase the failure occurred in. */
|
|
2472
|
+
failedAtPhase?: Exclude<RestorePhase, 'failed'>;
|
|
2473
|
+
}
|
|
2474
|
+
export type BackupType = 'manual' | 'scheduled' | 'uploaded';
|
|
2475
|
+
export interface BackupListItem {
|
|
2476
|
+
filename: string;
|
|
2477
|
+
size: number;
|
|
2478
|
+
createdAt: string;
|
|
2479
|
+
type: BackupType;
|
|
2480
|
+
metadata: BackupMetadata;
|
|
2481
|
+
}
|
|
2482
|
+
export type BackupScheduleType = 'disabled' | 'daily' | 'weekly' | 'monthly';
|
|
2483
|
+
export {};
|
|
2484
|
+
//# sourceMappingURL=types.d.ts.map
|