fix(albums): order each artist's albums by title when sorting by artist (#1115)

* fix(albums): order each artist's albums by title when sorting by artist

Browsing albums by artist left the albums within each artist in an
undefined order. The local-index sort only emitted the artist key; it
now appends album title as a secondary key (and artist as the tiebreak
for the by-name sort), matching the network path's per-page ordering.

* docs(changelog): note album sort-within-artist fix (#1115)
This commit is contained in:
Psychotoxical
2026-06-17 16:50:47 +02:00
committed by GitHub
parent 68b21643f8
commit 116196f0d4
3 changed files with 68 additions and 2 deletions
+51
View File
@@ -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']);
});
});
+11 -2
View File
@@ -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[] {