From 93544f561b0e528b35562c01cb417f5203a08e0a Mon Sep 17 00:00:00 2001 From: Hellraiser28 <193626466+Hellraiser28@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:52:30 +0200 Subject: [PATCH] fix(songs): list single-segment folders such as /music (#61) --- src/app/api/songs/[[...path]]/route.test.ts | 58 +++++++++++++++++++ src/app/api/songs/[[...path]]/route.ts | 7 ++- .../songs/hooks/use-songs-by-folder.ts | 12 +--- 3 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 src/app/api/songs/[[...path]]/route.test.ts diff --git a/src/app/api/songs/[[...path]]/route.test.ts b/src/app/api/songs/[[...path]]/route.test.ts new file mode 100644 index 00000000..b87ea3de --- /dev/null +++ b/src/app/api/songs/[[...path]]/route.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { GET } from './route' + +const { mockGetSongsByFolder, mockCountSongsByFolder } = vi.hoisted(() => ({ + mockGetSongsByFolder: vi.fn(), + mockCountSongsByFolder: vi.fn() +})) + +vi.mock('@/features/songs/song-query.repository', () => ({ + PAGE_SIZE: 50, + getSongsByFolder: (...args: unknown[]) => mockGetSongsByFolder(...args), + countSongsByFolder: (...args: unknown[]) => mockCountSongsByFolder(...args) +})) + +function get(url: string, path?: string[]) { + return GET(new Request(url), { params: Promise.resolve({ path }) }) +} + +beforeEach(() => { + vi.clearAllMocks() + mockGetSongsByFolder.mockResolvedValue([]) + mockCountSongsByFolder.mockResolvedValue(0) +}) + +describe('GET /api/songs', () => { + it('reads a single-segment folder from the folderPath query parameter', async () => { + const res = await get('http://localhost/api/songs?folderPath=%2Fmusic') + const body = await res.json() + + expect(res.status).toBe(200) + expect(body.folderPath).toBe('/music') + expect(mockGetSongsByFolder.mock.calls[0][0]).toBe('/music') + expect(mockCountSongsByFolder.mock.calls[0][0]).toBe('/music') + }) + + it('keeps special characters of the folderPath query parameter', async () => { + const folder = '/music/Albums/Amrinder Gill/Judaa & Co #1' + await get(`http://localhost/api/songs?folderPath=${encodeURIComponent(folder)}`) + + expect(mockGetSongsByFolder.mock.calls[0][0]).toBe(folder) + }) + + it('still accepts the folder as path segments', async () => { + const res = await get('http://localhost/api/songs/music/Albums', ['music', 'Albums']) + const body = await res.json() + + expect(body.folderPath).toBe('/music/Albums') + expect(mockGetSongsByFolder.mock.calls[0][0]).toBe('/music/Albums') + }) + + it('lists all songs when no folder is given', async () => { + const res = await get('http://localhost/api/songs') + const body = await res.json() + + expect(body.folderPath).toBe('all') + expect(mockGetSongsByFolder.mock.calls[0][0]).toBeNull() + }) +}) diff --git a/src/app/api/songs/[[...path]]/route.ts b/src/app/api/songs/[[...path]]/route.ts index ce23aaad..22c7ceee 100644 --- a/src/app/api/songs/[[...path]]/route.ts +++ b/src/app/api/songs/[[...path]]/route.ts @@ -26,9 +26,12 @@ type FilesResponse = FilesSuccessResponse | FilesErrorResponse export async function GET(request: Request, { params }: RouteParams): Promise> { const { path } = await params - - const folderPath = !!path?.length ? '/' + path.join('/') : null const { searchParams } = new URL(request.url) + + // The folder comes as a query parameter. As path segments, a single-segment folder such as /music + // (MUSIC_FOLDERS=/music,/audiobooks) is routed to /api/songs/[id] instead of this route. + const folderParam = getSearchParam(searchParams, 'folderPath', 'string', '') + const folderPath = folderParam || (!!path?.length ? '/' + path.join('/') : null) const search = getSearchParam(searchParams, 'search', 'string', '') || undefined const sortFieldParam = getSearchParam(searchParams, 'sortField', 'string', 'title') as ColumnField const sortParam = getSearchParam(searchParams, 'sort', 'string', 'asc') as SongSortDirection diff --git a/src/features/songs/hooks/use-songs-by-folder.ts b/src/features/songs/hooks/use-songs-by-folder.ts index 624fb682..85c51fde 100644 --- a/src/features/songs/hooks/use-songs-by-folder.ts +++ b/src/features/songs/hooks/use-songs-by-folder.ts @@ -34,15 +34,9 @@ async function fetchSongsByFolder( ): Promise { const isAll = folderPath === ALL_SONGS_FOLDER_ID - const encodedPath = isAll - ? '' - : folderPath - .replace(/^\//, '') - .split('/') - .map(segment => encodeURIComponent(segment)) - .join('/') - + // As a query parameter, not path segments: /api/songs/music would be taken for a song ID. const params: Record = { + ...(!isAll && { folderPath }), search, ...sorting, limit, @@ -56,7 +50,7 @@ async function fetchSongsByFolder( } } - const { data } = await api.get(`/songs/${encodedPath}`, { + const { data } = await api.get('/songs', { params })