diff --git a/CHANGELOG.md b/CHANGELOG.md index 75553adc..5fd9304f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The Previous / Play-Pause / Next buttons in the Windows taskbar thumbnail preview (the popup shown when hovering the taskbar icon) had stopped appearing. They are back, and the middle button's icon again reflects the current playback state. +### Album order within each artist when sorting by artist + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1115](https://github.com/Psychotoxical/psysonic/pull/1115)**, suggested by [@kingley82](https://github.com/kingley82) + +* When browsing albums sorted by artist, each artist's albums appeared in an arbitrary order. They are now ordered A–Z by album title within each artist. + ## [1.48.1] - 2026-06-15 diff --git a/src/utils/library/albumBrowseSort.test.ts b/src/utils/library/albumBrowseSort.test.ts new file mode 100644 index 00000000..ad5c51e8 --- /dev/null +++ b/src/utils/library/albumBrowseSort.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; +import type { SubsonicAlbum } from '../../api/subsonicTypes'; +import { albumSortClauses, sortSubsonicAlbums } from './albumBrowseSort'; + +const album = (artist: string, name: string): SubsonicAlbum => + ({ id: `${artist}-${name}`, artist, name }) as SubsonicAlbum; + +describe('albumSortClauses', () => { + it('sorts by artist then album name', () => { + expect(albumSortClauses('alphabeticalByArtist')).toEqual([ + { field: 'artist', dir: 'asc' }, + { field: 'name', dir: 'asc' }, + ]); + }); + + it('sorts by album name then artist', () => { + expect(albumSortClauses('alphabeticalByName')).toEqual([ + { field: 'name', dir: 'asc' }, + { field: 'artist', dir: 'asc' }, + ]); + }); +}); + +describe('sortSubsonicAlbums', () => { + it('orders each artist group by album name when sorting by artist', () => { + const input = [ + album('Rammstein', 'Sehnsucht'), + album('Duran Duran', 'Rio'), + album('Rammstein', 'Mutter'), + album('Duran Duran', 'DD'), + album('Duran Duran', 'The Wedding Album'), + ]; + const ordered = sortSubsonicAlbums(input, 'alphabeticalByArtist').map(a => `${a.artist} - ${a.name}`); + expect(ordered).toEqual([ + 'Duran Duran - DD', + 'Duran Duran - Rio', + 'Duran Duran - The Wedding Album', + 'Rammstein - Mutter', + 'Rammstein - Sehnsucht', + ]); + }); + + it('breaks album-name ties by artist when sorting by name', () => { + const input = [ + album('Zebra', 'Greatest Hits'), + album('Alpha', 'Greatest Hits'), + ]; + const ordered = sortSubsonicAlbums(input, 'alphabeticalByName').map(a => a.artist); + expect(ordered).toEqual(['Alpha', 'Zebra']); + }); +}); diff --git a/src/utils/library/albumBrowseSort.ts b/src/utils/library/albumBrowseSort.ts index 9d054443..dc55087e 100644 --- a/src/utils/library/albumBrowseSort.ts +++ b/src/utils/library/albumBrowseSort.ts @@ -4,10 +4,19 @@ import type { LibrarySortClause } from '../../api/library'; export type AlbumBrowseSort = 'alphabeticalByName' | 'alphabeticalByArtist'; export function albumSortClauses(sort: AlbumBrowseSort): LibrarySortClause[] { + // Always append a secondary key so albums sharing the primary key keep a + // stable order — by artist groups each artist's albums by title (rather than + // an undefined order within the artist), mirroring `sortSubsonicAlbums`. if (sort === 'alphabeticalByArtist') { - return [{ field: 'artist', dir: 'asc' }]; + return [ + { field: 'artist', dir: 'asc' }, + { field: 'name', dir: 'asc' }, + ]; } - return [{ field: 'name', dir: 'asc' }]; + return [ + { field: 'name', dir: 'asc' }, + { field: 'artist', dir: 'asc' }, + ]; } export function sortSubsonicAlbums(albums: SubsonicAlbum[], sort: AlbumBrowseSort): SubsonicAlbum[] {