feat(metadata): implement single-track metadata editor page (§A7)
Replace the placeholder with a controlled form for title/artist/album/ year/genre/track number, an AcoustID "find matches" action showing ranked candidates with confidence, a diff/apply picker, a re-enrich button, and save via PUT /metadata. Adds matches/apply API endpoints, mappers, types, and en/ru i18n strings. Batch editor remains a placeholder (deferred).
This commit is contained in:
@@ -2,10 +2,12 @@ import { api } from '../index';
|
||||
import {
|
||||
toAlbum,
|
||||
toArtist,
|
||||
toMetadataMatch,
|
||||
toPage,
|
||||
toTrack,
|
||||
type RawAlbum,
|
||||
type RawArtist,
|
||||
type RawMetadataMatch,
|
||||
type RawPaged,
|
||||
type RawTrack,
|
||||
} from '../mappers';
|
||||
@@ -13,6 +15,8 @@ import type {
|
||||
Track,
|
||||
Album,
|
||||
Artist,
|
||||
MetadataEdit,
|
||||
MetadataMatch,
|
||||
PaginatedResponse,
|
||||
LibraryFilters,
|
||||
} from '../types';
|
||||
@@ -161,6 +165,41 @@ export const libraryApi = api.injectEndpoints({
|
||||
}),
|
||||
providesTags: ['Track', 'Album', 'Artist'],
|
||||
}),
|
||||
getMetadataMatches: build.query<MetadataMatch[], string>({
|
||||
query: (trackId) => `/tracks/${trackId}/metadata/matches`,
|
||||
transformResponse: (raw: { items: RawMetadataMatch[] }) =>
|
||||
raw.items.map(toMetadataMatch),
|
||||
}),
|
||||
applyMetadata: build.mutation<
|
||||
Track,
|
||||
{ trackId: string; edit: MetadataEdit }
|
||||
>({
|
||||
query: ({ trackId, edit }) => ({
|
||||
url: `/tracks/${trackId}/metadata`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
title: edit.title,
|
||||
artist_name: edit.artistName,
|
||||
album_title: edit.albumTitle,
|
||||
year: edit.year,
|
||||
genre: edit.genre,
|
||||
track_number: edit.trackNumber,
|
||||
},
|
||||
}),
|
||||
transformResponse: (raw: RawTrack) => toTrack(raw),
|
||||
invalidatesTags: (_r, _e, { trackId }) => [
|
||||
{ type: 'Track', id: trackId },
|
||||
'Album',
|
||||
'Artist',
|
||||
],
|
||||
}),
|
||||
enrichTrack: build.mutation<{ track_id: string; job_id: string }, string>({
|
||||
query: (trackId) => ({
|
||||
url: `/tracks/${trackId}/metadata/enrich`,
|
||||
method: 'POST',
|
||||
}),
|
||||
invalidatesTags: (_r, _e, trackId) => [{ type: 'Track', id: trackId }],
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
});
|
||||
@@ -176,4 +215,7 @@ export const {
|
||||
useGetArtistAlbumsQuery,
|
||||
useGetArtistTracksQuery,
|
||||
useSearchLibraryQuery,
|
||||
useLazyGetMetadataMatchesQuery,
|
||||
useApplyMetadataMutation,
|
||||
useEnrichTrackMutation,
|
||||
} = libraryApi;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import type {
|
||||
Album,
|
||||
Artist,
|
||||
MetadataMatch,
|
||||
MetadataStatus,
|
||||
PaginatedResponse,
|
||||
Playlist,
|
||||
@@ -63,6 +64,9 @@ export interface RawTrack {
|
||||
duration_seconds: number | null;
|
||||
file_format: string;
|
||||
file_size: number;
|
||||
genre: string | null;
|
||||
year: number | null;
|
||||
track_number: number | null;
|
||||
metadata_status: string;
|
||||
metadata_error: string | null;
|
||||
enriched_at: string | null;
|
||||
@@ -71,6 +75,18 @@ export interface RawTrack {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** One AcoustID candidate, as returned by `GET /tracks/{id}/metadata/matches`. */
|
||||
export interface RawMetadataMatch {
|
||||
acoustid: string;
|
||||
score: number;
|
||||
recording_mbid: string | null;
|
||||
release_group_mbid: string | null;
|
||||
title: string | null;
|
||||
artist: string | null;
|
||||
album: string | null;
|
||||
year: number | null;
|
||||
}
|
||||
|
||||
export interface RawAlbum {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -127,6 +143,9 @@ export const toTrack = (r: RawTrack): Track => ({
|
||||
availability: 'server',
|
||||
metadataStatus: toMetadataStatus(r.metadata_status),
|
||||
metadataError: r.metadata_error ?? undefined,
|
||||
genre: r.genre ?? undefined,
|
||||
year: r.year ?? undefined,
|
||||
trackNumber: r.track_number ?? undefined,
|
||||
liked: false,
|
||||
format: r.file_format,
|
||||
fileSize: r.file_size,
|
||||
@@ -135,6 +154,17 @@ export const toTrack = (r: RawTrack): Track => ({
|
||||
enrichedAt: r.enriched_at ?? undefined,
|
||||
});
|
||||
|
||||
export const toMetadataMatch = (r: RawMetadataMatch): MetadataMatch => ({
|
||||
acoustid: r.acoustid,
|
||||
score: r.score,
|
||||
recordingMbid: r.recording_mbid ?? undefined,
|
||||
releaseGroupMbid: r.release_group_mbid ?? undefined,
|
||||
title: r.title ?? undefined,
|
||||
artist: r.artist ?? undefined,
|
||||
album: r.album ?? undefined,
|
||||
year: r.year ?? undefined,
|
||||
});
|
||||
|
||||
export const toAlbum = (r: RawAlbum): Album => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
|
||||
@@ -161,3 +161,26 @@ export interface ApiError {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user