4aa071eeeb
The transient client-side upload queue vanished on refresh, so a just- uploaded track seemed to disappear. Add a server-backed "Recently uploaded" section (source=upload, newest first) that survives refresh and auto-refreshes after each upload (the upload mutation already invalidates the `Track` tag this query provides). - api: `source` filter on `LibraryFilters` → `GET /tracks?source=` - i18n: `upload.recent.*` (en + ru); loading/error/empty states Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
217 lines
4.9 KiB
TypeScript
217 lines
4.9 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;
|
|
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;
|
|
}
|
|
|
|
export interface DownloadJob {
|
|
id: string;
|
|
url: string;
|
|
title?: string;
|
|
artist?: string;
|
|
album?: string;
|
|
status: 'queued' | 'downloading' | 'processing' | 'done' | 'error';
|
|
progress: number;
|
|
errorMessage?: string;
|
|
trackId?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface UploadResponse {
|
|
track_id: string;
|
|
title: string;
|
|
already_exists: boolean;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface User {
|
|
id: string;
|
|
username: string;
|
|
email?: string;
|
|
role: 'admin' | 'user';
|
|
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;
|
|
}
|