feat(web): admin removeTrack API helper for M7 #372
DELETE /api/admin/tracks/{id} with optional ?unmonitor=true. Returns
the typed envelope with optional cascaded album/artist ids and the
lidarr_unmonitor_failed flag (only set on Lidarr-side failure when
unmonitor was requested — the file + DB delete still succeeded).
Caller invalidates TanStack Query keys for any vanished entities.
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
import { removeTrack } from './tracks';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
function stubFetch(status: number, body: unknown, init: Partial<Response> = {}) {
|
||||||
|
const res = new Response(
|
||||||
|
body === null ? null : JSON.stringify(body),
|
||||||
|
{ status, headers: { 'Content-Type': 'application/json' }, ...init }
|
||||||
|
);
|
||||||
|
const spy = vi.fn().mockResolvedValue(res);
|
||||||
|
vi.stubGlobal('fetch', spy);
|
||||||
|
return spy;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('removeTrack', () => {
|
||||||
|
test('DELETEs /api/admin/tracks/:id with no query string when unmonitor unset', async () => {
|
||||||
|
const spy = stubFetch(200, {
|
||||||
|
deleted_track_id: 't-1',
|
||||||
|
deleted_album_id: 'al-1'
|
||||||
|
});
|
||||||
|
|
||||||
|
const r = await removeTrack('t-1');
|
||||||
|
expect(r.deleted_track_id).toBe('t-1');
|
||||||
|
expect(r.deleted_album_id).toBe('al-1');
|
||||||
|
expect(r.deleted_artist_id).toBeUndefined();
|
||||||
|
expect(r.lidarr_unmonitor_failed).toBeUndefined();
|
||||||
|
|
||||||
|
const call = spy.mock.calls[0];
|
||||||
|
expect(call[0]).toBe('/api/admin/tracks/t-1');
|
||||||
|
expect((call[1] as RequestInit).method).toBe('DELETE');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('appends ?unmonitor=true when option set', async () => {
|
||||||
|
const spy = stubFetch(200, {
|
||||||
|
deleted_track_id: 't-1',
|
||||||
|
lidarr_unmonitor_failed: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const r = await removeTrack('t-1', { unmonitor: true });
|
||||||
|
expect(r.lidarr_unmonitor_failed).toBe(true);
|
||||||
|
|
||||||
|
const call = spy.mock.calls[0];
|
||||||
|
expect(call[0]).toBe('/api/admin/tracks/t-1?unmonitor=true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('omits the query string when unmonitor: false', async () => {
|
||||||
|
const spy = stubFetch(200, { deleted_track_id: 't-1' });
|
||||||
|
|
||||||
|
await removeTrack('t-1', { unmonitor: false });
|
||||||
|
const call = spy.mock.calls[0];
|
||||||
|
expect(call[0]).toBe('/api/admin/tracks/t-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('surfaces not_found as ApiError', async () => {
|
||||||
|
stubFetch(404, { error: 'not_found' });
|
||||||
|
|
||||||
|
await expect(removeTrack('t-1')).rejects.toMatchObject({
|
||||||
|
code: 'not_found'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { apiFetch } from '$lib/api/client';
|
||||||
|
|
||||||
|
export type RemoveTrackResult = {
|
||||||
|
deleted_track_id: string;
|
||||||
|
deleted_album_id?: string;
|
||||||
|
deleted_artist_id?: string;
|
||||||
|
/** Only present (always `true` when set) when the operator requested
|
||||||
|
* unmonitor=true AND the Lidarr unmonitor call failed. The destructive
|
||||||
|
* file + DB delete already succeeded — surface a follow-up toast so
|
||||||
|
* the operator knows to manually unmonitor in Lidarr. */
|
||||||
|
lidarr_unmonitor_failed?: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveTrackOptions = {
|
||||||
|
/** When true, after the file + DB delete the server calls
|
||||||
|
* Lidarr.UnmonitorTrack so Lidarr won't search for a replacement.
|
||||||
|
* When false / omitted, no Lidarr call — Lidarr's monitoring will
|
||||||
|
* re-import on next scan, which is the operator's "find a replacement"
|
||||||
|
* path. */
|
||||||
|
unmonitor?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/admin/tracks/{id} — admin-only. Deletes the file from
|
||||||
|
* disk and the DB row, cascading album/artist tidy-up. Optionally
|
||||||
|
* unmonitors the track in Lidarr.
|
||||||
|
*
|
||||||
|
* On success returns the deleted track id plus optional album/artist
|
||||||
|
* ids when their parent row was tidied up by the server-side cascade.
|
||||||
|
* Caller is responsible for invalidating any TanStack Query keys the
|
||||||
|
* deleted entities backed.
|
||||||
|
*/
|
||||||
|
export async function removeTrack(
|
||||||
|
id: string,
|
||||||
|
options: RemoveTrackOptions = {}
|
||||||
|
): Promise<RemoveTrackResult> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.unmonitor) params.set('unmonitor', 'true');
|
||||||
|
const qs = params.toString();
|
||||||
|
const url = `/api/admin/tracks/${encodeURIComponent(id)}${qs ? '?' + qs : ''}`;
|
||||||
|
return (await apiFetch(url, { method: 'DELETE' })) as RemoveTrackResult;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user