Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
7.2 kB · 238 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238import type { PickKeys } from "ts-essentials"import type { CompareOpKey, ComponentMinimalSelect } from "../backend/common/database/drizzle/drizzleTypes.ts"import type { ClientType, ComponentAuthType, DeepReplaceValue, MonitoringStatus, QueueContext } from "./Atomic.ts"import type { SourceType } from "./Atomic.ts"import type { ComponentType, DateLike, ErrorLike, JsonPlayObject, PlayState, QueueName, Replace, SOURCE_SOT_TYPES, SourcePlayerJson } from "./Atomic.ts"import type { Dayjs } from "dayjs"import type { ErrorIsh } from "./ErrorUtils.ts"import type { PlayEvent } from "./PlayEvent.ts"import * as z from "zod"
export interface PlayApiCommon { uid: string componentId: number state: PlayState play: JsonPlayObject compacted: boolean playedAt: string seenAt: string updatedAt: string parentUid?: string // TODO add parent source type/name?}
export interface PlayInputApi { id: number data?: object play?: JsonPlayObject createdAt: string}
export interface QueueStateApi { id: number queueName: string queueStatus: string retries: number error?: ErrorLike updatedAt: string createdAt: string context?: QueueContext}
export interface PlayApiCommonDetailed extends PlayApiCommon { error?: ErrorIsh input?: PlayInputApi queueStates: QueueStateApi[] events: PlayEvent<string>[]}
export type ComponentState = 1 | 2 | 3 | 4 | 5 | 6 | 7;export const COMPONENT_STATE = { RUNNING: 1, IGNORED: 2, IDLE: 3, STOPPED: 4, INITIALIZING: 5, NOT_READY: 6, ERROR: 7,} as const satisfies Record<string, ComponentState>;
export const componentStateToFriendly = (state: ComponentState) => { switch(state) { case 1: return 'Running'; case 2: return 'Ignored'; case 3: return 'Idle'; case 4: return 'Stopped'; case 5: return 'Initializing'; case 6: return 'Not Ready'; case 7: return 'Error'; }}
export type ComponentCommonApi = { type: SourceType | ClientType name: string /** General state of the component like Idle, Stopped, Running, Error */ state: ComponentState /** More specific, live activity state like "sleeping", "hydrating historical scrobbles", "processing dead scrobbles", etc... */ status?: string players: Record<string, SourcePlayerJson> errors?: ErrorIsh[] warnings?: ErrorIsh[] monitoringStatus?: MonitoringStatus deadLetterPlays: number deadLetterPlaysTotal: number queued: number} & Omit<ComponentMinimalSelect, 'type'>
export type ComponentCommonApiJson = Replace<ComponentCommonApi, PickKeys<ComponentCommonApi, Dayjs>, string>;
export type ComponentHistoricalApi = { synced: boolean syncedReason: string | undefined syncError: ErrorIsh | undefined lastImport: Dayjs lastImportSuccess: Dayjs}
export type ComponentDetailedApi = ComponentCommonApi & { hasAuth: boolean; hasAuthInteraction: boolean; authed: boolean authType: ComponentAuthType initialized: boolean}
export type ComponentCientApiBase = { tracksScrobbled: number supportsNowPlaying: boolean players: Record<string, SourcePlayerJson & {expiration?: string}>}
export type ComponentClientApi = ComponentDetailedApi & ComponentCientApiBase & Partial<ComponentHistoricalApi>;export type ComponentClientApiJson = Replace<ComponentClientApi, PickKeys<ComponentClientApi, Dayjs>, string>;
export type ComponentSourceApiBase = { sot: SOURCE_SOT_TYPES supportsUpstreamRecentlyPlayed: boolean; tracksDiscovered: number; wakeAt?: string sleeping: boolean}
export type ComponentSourceApi = ComponentDetailedApi & ComponentSourceApiBase & Partial<ComponentHistoricalApi>;export type ComponentSourceApiJson = Replace<ComponentSourceApi, PickKeys<ComponentSourceApi, Dayjs>, string>;
export type SubsonicSourceApiJson = ComponentSourceApiJson & { playbackReporting?: boolean }
export type ComponentsApiJson = ComponentSourceApiJson | ComponentClientApiJson;
export const isComponentSourceApiJson = (data: ComponentCommonApiJson): data is ComponentSourceApiJson => { return data.mode === 'source';}export const isComponentClientApiJson = (data: ComponentCommonApiJson): data is ComponentClientApiJson => { return data.mode === 'client';}
export type MsSseEventPayload<T extends object = Record<string, any>> = { type: SourceType | ClientType name: string componentId: number from: ComponentType data: T event: string}
export type MsSseEvent<T extends object = Record<string, any>> = { playerUpdate: MsSseEventPayload<SourcePlayerJson> discovered: MsSseEventPayload scrobbleQueued: MsSseEventPayload scrobbleDequeued: MsSseEventPayload client: T componentUpdate: MsSseEventPayload<ComponentCommonApiJson> playInsert: MsSseEventPayload<PlayApiCommonDetailed> playUpdate: MsSseEventPayload<Partial<PlayApiCommonDetailed>>}
export type SortPlaysBy = 'played' | 'seen';export interface SortPlaysByProps { sortBy: SortPlaysBy}
export type PlayStateUI = PlayState | 'failed TBR';
export type QueryPlaysOptsJson = { sort?: "playedAt" | "seenAt"; order?: "asc" | "desc"; with?: ("input" | "parent" | "parent-input" | "queues")[]; limit?: number; offset?: number; state?: ("queued" | "discovered" | "discarded" | "scrobbled" | "failed" | "duped")[]; stateNot?: ("queued" | "discovered" | "discarded" | "scrobbled" | "failed" | "duped")[]; componentId?: number; seenAt?: { type: "eq" | "ne" | "gt" | "gte" | "lt" | "lte"; date: string; } | { type: "between"; range: [string, string]; inclusive?: boolean; }; playedAt?: { type: "eq" | "ne" | "gt" | "gte" | "lt" | "lte"; date: string; } | { type: "between"; range: [string, string]; inclusive?: boolean; };queues?: { queueName: QueueName; queueStatus: ('queued' | 'failed' | 'completed')[] | ('queued' | 'failed' | 'completed'); }[]; uid?: string[]; text?: string[];}export interface PaginatedQueryResponse { limit: number; offset: number; total?: number;}export interface PaginatedResponse<T> { data: T[]; meta: PaginatedQueryResponse;}
export type CompareDateBetween<D extends DateLike = Dayjs> = { type: 'between'; range: [D, D]; inclusive?: boolean;};
export type CompareDateSingle<D extends DateLike = Dayjs> = { type: CompareOpKey<D>; date: D;};
export type CacheClearType = 'external-api' | 'transforms';
export const componentStateBodySchema = z.object({ state: z.enum(["stop","start","restart","ignore","monitor"]), reason: z.string().optional()});
export type ComponentStateBody = z.infer<typeof componentStateBodySchema>;
export const playStateBodySchema = z.object({ state: z.enum(["discarded"]), reason: z.string().optional()});
export type PlayStateBody = z.infer<typeof playStateBodySchema>;