add SortName client side sort option (#1612)

This commit is contained in:
jeffvli
2026-01-28 20:45:47 -08:00
parent 78aebd7c5d
commit ced3b491ff
10 changed files with 54 additions and 18 deletions
+1
View File
@@ -273,6 +273,7 @@
"releaseYear": "release year", "releaseYear": "release year",
"search": "search", "search": "search",
"songCount": "song count", "songCount": "song count",
"sortName": "sort name",
"title": "title", "title": "title",
"toYear": "to year", "toYear": "to year",
"trackNumber": "track", "trackNumber": "track",
@@ -226,7 +226,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server?.userId, userId: apiClientProps.server?.userId,
}, },
query: { query: {
Fields: 'Genres, Overview', Fields: 'Genres, Overview, SortName',
}, },
}), }),
jfApiClient(apiClientProps).getSimilarArtistList({ jfApiClient(apiClientProps).getSimilarArtistList({
@@ -253,7 +253,7 @@ export const JellyfinController: InternalControllerEndpoint = {
const res = await jfApiClient(apiClientProps).getAlbumArtistList({ const res = await jfApiClient(apiClientProps).getAlbumArtistList({
query: { query: {
Fields: 'Genres, DateCreated, ExternalUrls, Overview', Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName',
ImageTypeLimit: 1, ImageTypeLimit: 1,
Limit: query.limit, Limit: query.limit,
ParentId: getLibraryId(query.musicFolderId), ParentId: getLibraryId(query.musicFolderId),
@@ -305,7 +305,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server.userId, userId: apiClientProps.server.userId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
ParentId: query.id, ParentId: query.id,
SortBy: 'ParentIndexNumber,IndexNumber,SortName', SortBy: 'ParentIndexNumber,IndexNumber,SortName',
@@ -363,7 +363,7 @@ export const JellyfinController: InternalControllerEndpoint = {
}, },
query: { query: {
...artistQuery, ...artistQuery,
Fields: 'People, Tags, Studios', Fields: 'People, Tags, Studios, SortName',
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined, GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
IncludeItemTypes: 'MusicAlbum', IncludeItemTypes: 'MusicAlbum',
IsFavorite: query.favorite, IsFavorite: query.favorite,
@@ -399,7 +399,7 @@ export const JellyfinController: InternalControllerEndpoint = {
const res = await jfApiClient(apiClientProps).getArtistList({ const res = await jfApiClient(apiClientProps).getArtistList({
query: { query: {
Fields: 'Genres, DateCreated, ExternalUrls, Overview', Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName',
ImageTypeLimit: 1, ImageTypeLimit: 1,
Limit: query.limit, Limit: query.limit,
ParentId: getLibraryId(query.musicFolderId), ParentId: getLibraryId(query.musicFolderId),
@@ -438,7 +438,7 @@ export const JellyfinController: InternalControllerEndpoint = {
itemId: query.artistId, itemId: query.artistId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId', Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
Limit: query.count, Limit: query.count,
UserId: apiClientProps.server?.userId || undefined, UserId: apiClientProps.server?.userId || undefined,
}, },
@@ -794,7 +794,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server?.userId, userId: apiClientProps.server?.userId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId', Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
Ids: query.id, Ids: query.id,
}, },
}); });
@@ -855,7 +855,7 @@ export const JellyfinController: InternalControllerEndpoint = {
id: query.id, id: query.id,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags, SortName',
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
UserId: apiClientProps.server?.userId, UserId: apiClientProps.server?.userId,
}, },
@@ -902,7 +902,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server?.userId, userId: apiClientProps.server?.userId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
GenreIds: query.genre ? query.genre : undefined, GenreIds: query.genre ? query.genre : undefined,
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
IsPlayed: IsPlayed:
@@ -974,7 +974,7 @@ export const JellyfinController: InternalControllerEndpoint = {
itemId: query.songId, itemId: query.songId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId', Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
Limit: query.count, Limit: query.count,
UserId: apiClientProps.server?.userId || undefined, UserId: apiClientProps.server?.userId || undefined,
}, },
@@ -1007,7 +1007,7 @@ export const JellyfinController: InternalControllerEndpoint = {
itemId: query.songId, itemId: query.songId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId', Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
Limit: query.count, Limit: query.count,
UserId: apiClientProps.server?.userId || undefined, UserId: apiClientProps.server?.userId || undefined,
}, },
@@ -1092,7 +1092,7 @@ export const JellyfinController: InternalControllerEndpoint = {
query: { query: {
AlbumIds: albumIdsFilter, AlbumIds: albumIdsFilter,
ArtistIds: artistIdsFilter, ArtistIds: artistIdsFilter,
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
GenreIds: query.genreIds?.join(','), GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
IsFavorite: query.favorite, IsFavorite: query.favorite,
@@ -1127,7 +1127,7 @@ export const JellyfinController: InternalControllerEndpoint = {
query: { query: {
AlbumIds: albumIdsFilter, AlbumIds: albumIdsFilter,
ArtistIds: artistIdsFilter, ArtistIds: artistIdsFilter,
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
GenreIds: query.genreIds?.join(','), GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
IsFavorite: query.favorite, IsFavorite: query.favorite,
@@ -1282,7 +1282,7 @@ export const JellyfinController: InternalControllerEndpoint = {
}, },
query: { query: {
ArtistIds: query.artistId, ArtistIds: query.artistId,
Fields: 'Genres, DateCreated, MediaSources, ParentId', Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
Limit: query.limit, Limit: query.limit,
Recursive: true, Recursive: true,
@@ -1378,7 +1378,7 @@ export const JellyfinController: InternalControllerEndpoint = {
id: query.id, id: query.id,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags, SortName',
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
UserId: apiClientProps.server?.userId, UserId: apiClientProps.server?.userId,
}, },
@@ -1404,7 +1404,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server?.userId, userId: apiClientProps.server?.userId,
}, },
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId', Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
Ids: query.id, Ids: query.id,
}, },
}); });
@@ -1562,7 +1562,7 @@ export const JellyfinController: InternalControllerEndpoint = {
}, },
query: { query: {
EnableTotalRecordCount: true, EnableTotalRecordCount: true,
Fields: 'People, Tags', Fields: 'People, Tags, SortName',
ImageTypeLimit: 1, ImageTypeLimit: 1,
IncludeItemTypes: 'MusicAlbum', IncludeItemTypes: 'MusicAlbum',
Limit: query.albumLimit, Limit: query.albumLimit,
@@ -1610,7 +1610,7 @@ export const JellyfinController: InternalControllerEndpoint = {
}, },
query: { query: {
EnableTotalRecordCount: true, EnableTotalRecordCount: true,
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
Limit: query.songLimit, Limit: query.songLimit,
Recursive: true, Recursive: true,
@@ -46,6 +46,7 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
[AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT, [AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT,
[AlbumListSort.RELEASE_DATE]: AlbumListSortType.BY_YEAR, [AlbumListSort.RELEASE_DATE]: AlbumListSortType.BY_YEAR,
[AlbumListSort.SONG_COUNT]: undefined, [AlbumListSort.SONG_COUNT]: undefined,
[AlbumListSort.SORT_NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR, [AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
}; };
@@ -186,6 +186,11 @@ const CLIENT_SIDE_SONG_FILTERS = [
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: SongListSort.NAME, value: SongListSort.NAME,
}, },
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.sortName', { postProcess: 'titleCase' }),
value: SongListSort.SORT_NAME,
},
{ {
defaultOrder: SortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }), name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
@@ -234,6 +239,11 @@ export const CLIENT_SIDE_ALBUM_FILTERS = [
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME, value: AlbumListSort.NAME,
}, },
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.sortName', { postProcess: 'titleCase' }),
value: AlbumListSort.SORT_NAME,
},
{ {
defaultOrder: SortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }), name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
@@ -241,6 +241,7 @@ const normalizeSong = (
releaseYear: item.ProductionYear || null, releaseYear: item.ProductionYear || null,
sampleRate, sampleRate,
size, size,
sortName: item.SortName || item.Name,
tags: getTags(item), tags: getTags(item),
trackNumber: item.IndexNumber, trackNumber: item.IndexNumber,
trackSubtitle: null, trackSubtitle: null,
@@ -313,6 +314,7 @@ const normalizeAlbum = (
size: null, size: null,
songCount: item?.ChildCount || null, songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server)), songs: item.Songs?.map((song) => normalizeSong(song, server)),
sortName: item.SortName || item.Name,
tags: getTags(item), tags: getTags(item),
updatedAt: item?.DateLastMediaAdded || item.DateCreated, updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false, userFavorite: item.UserData?.IsFavorite || false,
@@ -560,6 +560,7 @@ const album = z.object({
RunTimeTicks: z.number(), RunTimeTicks: z.number(),
ServerId: z.string(), ServerId: z.string(),
Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail
SortName: z.string().optional(),
Studios: z.array(studio), Studios: z.array(studio),
Tags: z.string().array().optional(), Tags: z.string().array().optional(),
Type: z.string(), Type: z.string(),
@@ -299,6 +299,7 @@ const normalizeSong = (
releaseYear: item.year || null, releaseYear: item.year || null,
sampleRate: item.sampleRate || null, sampleRate: item.sampleRate || null,
size: item.size, size: item.size,
sortName: item.orderTitle,
tags: item.tags || null, tags: item.tags || null,
trackNumber: item.trackNumber, trackNumber: item.trackNumber,
trackSubtitle: item.tags?.subtitle ? item.tags.subtitle.join(' · ') : null, trackSubtitle: item.tags?.subtitle ? item.tags.subtitle.join(' · ') : null,
@@ -405,6 +406,7 @@ const normalizeAlbum = (
songs: item.songs songs: item.songs
? item.songs.map((song) => normalizeSong(song, server, pathReplace, pathReplaceWith)) ? item.songs.map((song) => normalizeSong(song, server, pathReplace, pathReplaceWith))
: undefined, : undefined,
sortName: item.orderAlbumName,
tags: item.tags || null, tags: item.tags || null,
updatedAt: item.updatedAt, updatedAt: item.updatedAt,
userFavorite: item.starred || false, userFavorite: item.starred || false,
@@ -196,6 +196,7 @@ const normalizeSong = (
releaseYear: item.year || null, releaseYear: item.year || null,
sampleRate: item.samplingRate || null, sampleRate: item.samplingRate || null,
size: item.size, size: item.size,
sortName: item.title,
tags: null, tags: null,
trackNumber: item.track || 1, trackNumber: item.track || 1,
trackSubtitle: null, trackSubtitle: null,
@@ -321,6 +322,7 @@ const normalizeAlbum = (
(item as z.infer<typeof ssType._response.album>).song?.map((song) => (item as z.infer<typeof ssType._response.album>).song?.map((song) =>
normalizeSong(song, server, pathReplace, pathReplaceWith, undefined, discTitleMap), normalizeSong(song, server, pathReplace, pathReplaceWith, undefined, discTitleMap),
) || [], ) || [],
sortName: item.title,
tags: null, tags: null,
updatedAt: item.created, updatedAt: item.created,
userFavorite: Boolean(item.starred) || false, userFavorite: Boolean(item.starred) || false,
+7
View File
@@ -244,6 +244,10 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
results = orderBy(results, ['releaseDate'], [order]); results = orderBy(results, ['releaseDate'], [order]);
break; break;
case SongListSort.SORT_NAME:
results = orderBy(results, [(v) => v.sortName ?? v.name], [order]);
break;
case SongListSort.YEAR: case SongListSort.YEAR:
results = orderBy( results = orderBy(
results, results,
@@ -440,6 +444,9 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder:
case AlbumListSort.SONG_COUNT: case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]); results = orderBy(results, ['songCount'], [order]);
break; break;
case AlbumListSort.SORT_NAME:
results = orderBy(results, [(v) => v.sortName ?? v.name], [order]);
break;
case AlbumListSort.YEAR: case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]); results = orderBy(results, ['releaseYear'], [order]);
break; break;
+10
View File
@@ -192,6 +192,7 @@ export type Album = {
size: null | number; size: null | number;
songCount: null | number; songCount: null | number;
songs?: Song[]; songs?: Song[];
sortName: string;
tags: null | Record<string, string[]>; tags: null | Record<string, string[]>;
updatedAt: string; updatedAt: string;
userFavorite: boolean; userFavorite: boolean;
@@ -392,6 +393,7 @@ export type Song = {
releaseYear: null | number; releaseYear: null | number;
sampleRate: null | number; sampleRate: null | number;
size: number; size: number;
sortName: string;
tags: null | Record<string, string[]>; tags: null | Record<string, string[]>;
trackNumber: number; trackNumber: number;
trackSubtitle: null | string; trackSubtitle: null | string;
@@ -464,6 +466,7 @@ export enum AlbumListSort {
RECENTLY_PLAYED = 'recentlyPlayed', RECENTLY_PLAYED = 'recentlyPlayed',
RELEASE_DATE = 'releaseDate', RELEASE_DATE = 'releaseDate',
SONG_COUNT = 'songCount', SONG_COUNT = 'songCount',
SORT_NAME = 'sortName',
YEAR = 'year', YEAR = 'year',
} }
@@ -518,6 +521,7 @@ export const albumListSortMap: AlbumListSortMap = {
recentlyPlayed: undefined, recentlyPlayed: undefined,
releaseDate: JFAlbumListSort.RELEASE_DATE, releaseDate: JFAlbumListSort.RELEASE_DATE,
songCount: undefined, songCount: undefined,
sortName: JFAlbumListSort.NAME,
year: undefined, year: undefined,
}, },
navidrome: { navidrome: {
@@ -537,6 +541,7 @@ export const albumListSortMap: AlbumListSortMap = {
// Recent versions of Navidrome support release date, but fallback to year for now // Recent versions of Navidrome support release date, but fallback to year for now
releaseDate: NDAlbumListSort.YEAR, releaseDate: NDAlbumListSort.YEAR,
songCount: NDAlbumListSort.SONG_COUNT, songCount: NDAlbumListSort.SONG_COUNT,
sortName: NDAlbumListSort.NAME,
year: NDAlbumListSort.YEAR, year: NDAlbumListSort.YEAR,
}, },
subsonic: { subsonic: {
@@ -555,6 +560,7 @@ export const albumListSortMap: AlbumListSortMap = {
recentlyPlayed: undefined, recentlyPlayed: undefined,
releaseDate: undefined, releaseDate: undefined,
songCount: undefined, songCount: undefined,
sortName: undefined,
year: undefined, year: undefined,
}, },
}; };
@@ -578,6 +584,7 @@ export enum SongListSort {
RECENTLY_ADDED = 'recentlyAdded', RECENTLY_ADDED = 'recentlyAdded',
RECENTLY_PLAYED = 'recentlyPlayed', RECENTLY_PLAYED = 'recentlyPlayed',
RELEASE_DATE = 'releaseDate', RELEASE_DATE = 'releaseDate',
SORT_NAME = 'sortName',
YEAR = 'year', YEAR = 'year',
} }
@@ -642,6 +649,7 @@ export const songListSortMap: SongListSortMap = {
recentlyAdded: JFSongListSort.RECENTLY_ADDED, recentlyAdded: JFSongListSort.RECENTLY_ADDED,
recentlyPlayed: JFSongListSort.RECENTLY_PLAYED, recentlyPlayed: JFSongListSort.RECENTLY_PLAYED,
releaseDate: JFSongListSort.RELEASE_DATE, releaseDate: JFSongListSort.RELEASE_DATE,
sortName: JFSongListSort.NAME,
year: undefined, year: undefined,
}, },
navidrome: { navidrome: {
@@ -663,6 +671,7 @@ export const songListSortMap: SongListSortMap = {
recentlyAdded: NDSongListSort.RECENTLY_ADDED, recentlyAdded: NDSongListSort.RECENTLY_ADDED,
recentlyPlayed: NDSongListSort.PLAY_DATE, recentlyPlayed: NDSongListSort.PLAY_DATE,
releaseDate: undefined, releaseDate: undefined,
sortName: NDSongListSort.TITLE,
year: NDSongListSort.YEAR, year: NDSongListSort.YEAR,
}, },
subsonic: { subsonic: {
@@ -684,6 +693,7 @@ export const songListSortMap: SongListSortMap = {
recentlyAdded: undefined, recentlyAdded: undefined,
recentlyPlayed: undefined, recentlyPlayed: undefined,
releaseDate: undefined, releaseDate: undefined,
sortName: undefined,
year: undefined, year: undefined,
}, },
}; };