Files
mcma-webui/src/api/types.ts
T
Цвылев Александр Вадимович ac6b76bf98 feat(radio): wire radio + similarity into the queue (A11/B6, §6.5)
"Start radio" in the track menu seeds the queue from a track; RadioController
(headless, in AppShell) tops it up via /radio/next as it drains. QueuePanel's
radio scaffolding is now live: the exploration slider drives familiar<->new and
each entry shows its reason ("why this is playing"). Adds the radio endpoints
(start/next/similar), radio context + actions on the queue slice, and the pick->
entry mapper. Degrades to metadata picks while ML is offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 02:40:13 +03:00

324 lines
8.2 KiB
TypeScript

export type TrackAvailability = 'server' | 'downloading' | 'error' | 'missing';
/**
* Metadata-enrichment state, distinct from file `availability`. `pending` = the
* worker hasn't finished (or hasn't started); `enriched` = identity found;
* `failed` = no match / a worker error (see `metadataError`); `manual` = user-
* edited and never auto-overwritten.
*/
export type MetadataStatus = 'pending' | 'enriched' | 'failed' | 'manual';
export interface Track {
id: string;
title: string;
artistId: string;
artistName: string;
albumId: string;
albumTitle: string;
albumArtUrl?: string;
hasCover: boolean;
durationMs: number;
trackNumber?: number;
discNumber?: number;
year?: number;
genre?: string;
availability: TrackAvailability;
metadataStatus: MetadataStatus;
/** Human-readable reason the last enrichment run set `failed`; else undefined. */
metadataError?: string;
fileSize?: number;
format?: string;
bitrate?: number;
liked: boolean;
/** Where the track entered the library (e.g. `upload`, `local_folder`). */
source?: string;
/** ISO timestamp the track was added to the library. */
createdAt?: string;
/** ISO timestamp the last successful enrichment ran; undefined if never. */
enrichedAt?: string;
}
export interface Album {
id: string;
title: string;
artistId: string;
artistName: string;
artUrl?: string;
/** Whether the album has cover art served by `GET /albums/{id}/cover`. */
hasCover: boolean;
year?: number;
trackCount: number;
totalDurationMs: number;
genre?: string;
}
export interface Artist {
id: string;
name: string;
artUrl?: string;
albumCount: number;
trackCount: number;
}
export interface Playlist {
id: string;
name: string;
description?: string;
ownerId: string;
trackCount: number;
totalDurationMs: number;
artUrl?: string;
isPublic: boolean;
createdAt: string;
updatedAt: string;
}
export interface PlaylistTrack extends Track {
position: number;
addedAt: string;
}
/** Lifecycle of a download job, mirroring the backend's `DownloadStatus`.
* `enriching` = file fetched, metadata pipeline running; `done` = imported. */
export type DownloadStatus =
| 'queued'
| 'downloading'
| 'enriching'
| 'done'
| 'failed';
export interface DownloadJob {
id: string;
/** Source backend the job pulls from (e.g. `youtube`). */
source: string;
/** Stable per-source id of the item (e.g. a YouTube videoId). */
sourceId?: string;
/** The free-text query the job was created from, for display. */
query?: string;
status: DownloadStatus;
/** Fraction complete, 0..1. */
progress: number;
errorMessage?: string;
/** Set once the download finishes and the library track exists. */
trackId?: string;
retryCount: number;
createdAt: string;
updatedAt: string;
}
/** Result of POST /downloads. Either the item is already in the library
* (`alreadyInLibrary`, `trackId` set), or a job covers it (`job`). */
export interface DownloadRequestResult {
alreadyInLibrary: boolean;
trackId?: string;
job?: DownloadJob;
}
/** One hit from an external (fetch) source — the §A4 discover screen. */
export interface ExternalSearchResult {
source: string;
sourceId: string;
title: string;
artist?: string;
album?: string;
durationMs?: number;
thumbnailUrl?: string;
}
/** A registered source backend, from GET /sources. `kind`: `indexable` (a
* mounted folder) or `fetch` (searchable + downloadable, e.g. YouTube). */
export interface SourceInfo {
name: string;
label: string;
kind: 'indexable' | 'fetch';
available: boolean;
}
export interface UploadResponse {
track_id: string;
title: string;
already_exists: boolean;
}
/** One backing-dependency probe result (GET /admin/services). `skipped` = the
* dependency is optional and not configured (e.g. ML with no ML_SERVICE_URL). */
export type ServiceStatus = 'ok' | 'down' | 'skipped';
export interface ServicesStatus {
database: ServiceStatus;
redis: ServiceStatus;
ml: ServiceStatus;
}
export interface StorageFormatBreakdown {
fileFormat: string;
trackCount: number;
totalSize: number;
}
export interface StorageGenreCount {
genre: string;
trackCount: number;
}
/** Capacity of the volume backing the media store. Absent for object-store
* backends (S3), which have no fixed disk to report. */
export interface StorageDiskUsage {
total: number;
used: number;
free: number;
}
export interface StorageStats {
totalTracks: number;
totalArtists: number;
totalAlbums: number;
/** Sum of every track's recorded file size (the library's footprint). */
totalSize: number;
totalDurationSeconds: number;
largestTrackSize: number;
earliestAdded?: string;
latestAdded?: string;
byFormat: StorageFormatBreakdown[];
byMetadataStatus: Record<string, number>;
bySource: Record<string, number>;
topGenres: StorageGenreCount[];
disk?: StorageDiskUsage;
}
/** A set of tracks sharing one acoustic fingerprint — de-dup candidates (§A6). */
export interface DuplicateGroup {
fingerprint: string;
tracks: Track[];
}
/** One radio/similarity pick with its reason code (§6.5). `reason`: `ml` |
* `similar` | `from_likes` | `discover` — localized client-side. */
export interface RadioPick {
track: Track;
reason: string;
}
/** Radio / similar response: where the picks came from + the picks. `source`:
* `ml` (recommender) or `metadata` (fallback heuristics). */
export interface RadioResult {
source: string;
picks: RadioPick[];
}
/** Cached lyrics for a track (§6.7). `not_found` is a normal state, not an
* error. `synced` is raw LRC (client parses timestamps); `plain` is fallback. */
export interface Lyrics {
trackId: string;
status: 'found' | 'not_found' | 'pending';
source?: string;
synced?: string;
plain?: string;
syncedAvailable: boolean;
}
export interface User {
id: string;
username: string;
email?: string;
role: 'admin' | 'user';
isActive: boolean;
createdAt: string;
lastActiveAt?: string;
}
export interface AuthTokens {
accessToken: string;
refreshToken: string;
// Optional: the backend's TokenResponse carries no TTL — expiry is driven by
// 401→refresh, not a client-side clock. Present only if a backend supplies it.
expiresIn?: number;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
user: User;
tokens: AuthTokens;
}
export interface RegisterRequest {
username: string;
password: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
export interface LibraryFilters {
search?: string;
genre?: string;
artistId?: string;
albumId?: string;
/** Filter by ingest origin, e.g. `upload`, `youtube`, `local`. */
source?: string;
liked?: boolean;
page?: number;
pageSize?: number;
sortBy?: 'title' | 'artist' | 'album' | 'year' | 'dateAdded';
sortOrder?: 'asc' | 'desc';
}
export interface ApiError {
status: number;
message: string;
code?: string;
}
/** One AcoustID candidate from `GET /tracks/{id}/metadata/matches` (§A7). */
export interface MetadataMatch {
acoustid: string;
/** Confidence 0..1. */
score: number;
recordingMbid?: string;
releaseGroupMbid?: string;
title?: string;
artist?: string;
album?: string;
year?: number;
}
/** Manual edits / an accepted match, sent to `PUT /tracks/{id}/metadata`. */
export interface MetadataEdit {
title?: string;
artistName?: string;
albumTitle?: string;
year?: number;
genre?: string;
trackNumber?: number;
}
// -- user settings (`/settings`) ---------------------------------------------
export type ThemePref = 'system' | 'light' | 'dark';
export type StreamQuality = 'original' | 'high' | 'medium' | 'low';
/** General per-user preferences from `GET /settings`. */
export interface AppSettings {
theme: ThemePref;
streamQuality: StreamQuality;
}
export type ScrobbleProvider = 'lastfm' | 'listenbrainz';
/** Scrobbling config from `GET /settings/scrobbling`. The session key is
* write-only server-side, so it never appears here — only `configured`. */
export interface ScrobblingConfig {
enabled: boolean;
provider: ScrobbleProvider | null;
username: string | null;
configured: boolean;
}