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
+6
View File
@@ -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 AZ by album title within each artist.
## [1.48.1] - 2026-06-15
+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[] {