61dbb1abd2
Implement the A8 upload screen against the existing /upload contract:
- UploadResponse type ({track_id, title, already_exists}) + mutation typed to it
- buildUploadFormData helper (single file under field `file`, per FastAPI)
- UploadPage: drag-and-drop + file picker, client-side queue with
concurrency cap (3), per-file status badges, retry on error,
already_exists -> "Already in library", deep-link to A7 metadata editor
- i18n upload.* section (en/ru) incl. "metadata pending" hint
Indeterminate spinner per file; percent progress is a follow-up
(needs an XHR baseQuery — fetchBaseQuery gives no upload progress).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
2.5 KiB
TypeScript
139 lines
2.5 KiB
TypeScript
export type TrackAvailability = 'server' | 'downloading' | 'error' | 'missing';
|
|
|
|
export interface Track {
|
|
id: string;
|
|
title: string;
|
|
artistId: string;
|
|
artistName: string;
|
|
albumId: string;
|
|
albumTitle: string;
|
|
albumArtUrl?: string;
|
|
durationMs: number;
|
|
trackNumber?: number;
|
|
discNumber?: number;
|
|
year?: number;
|
|
genre?: string;
|
|
availability: TrackAvailability;
|
|
fileSize?: number;
|
|
format?: string;
|
|
bitrate?: number;
|
|
liked: boolean;
|
|
}
|
|
|
|
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 StorageStats {
|
|
totalBytes: number;
|
|
usedBytes: number;
|
|
trackCount: number;
|
|
albumCount: number;
|
|
artistCount: number;
|
|
}
|
|
|
|
export interface User {
|
|
id: string;
|
|
username: string;
|
|
email?: string;
|
|
role: 'admin' | 'user';
|
|
createdAt: string;
|
|
lastActiveAt?: string;
|
|
}
|
|
|
|
export interface AuthTokens {
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
expiresIn: number;
|
|
}
|
|
|
|
export interface LoginRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
user: User;
|
|
tokens: AuthTokens;
|
|
}
|
|
|
|
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;
|
|
liked?: boolean;
|
|
page?: number;
|
|
pageSize?: number;
|
|
sortBy?: 'title' | 'artist' | 'album' | 'year' | 'dateAdded';
|
|
sortOrder?: 'asc' | 'desc';
|
|
}
|
|
|
|
export interface ApiError {
|
|
status: number;
|
|
message: string;
|
|
code?: string;
|
|
}
|