diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e46f22ac8..5b72893c6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -273,6 +273,7 @@ "releaseYear": "release year", "search": "search", "songCount": "song count", + "sortName": "sort name", "title": "title", "toYear": "to year", "trackNumber": "track", diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 0fdc1c3d0..b05224997 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -226,7 +226,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server?.userId, }, query: { - Fields: 'Genres, Overview', + Fields: 'Genres, Overview, SortName', }, }), jfApiClient(apiClientProps).getSimilarArtistList({ @@ -253,7 +253,7 @@ export const JellyfinController: InternalControllerEndpoint = { const res = await jfApiClient(apiClientProps).getAlbumArtistList({ query: { - Fields: 'Genres, DateCreated, ExternalUrls, Overview', + Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName', ImageTypeLimit: 1, Limit: query.limit, ParentId: getLibraryId(query.musicFolderId), @@ -305,7 +305,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server.userId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName', IncludeItemTypes: 'Audio', ParentId: query.id, SortBy: 'ParentIndexNumber,IndexNumber,SortName', @@ -363,7 +363,7 @@ export const JellyfinController: InternalControllerEndpoint = { }, query: { ...artistQuery, - Fields: 'People, Tags, Studios', + Fields: 'People, Tags, Studios, SortName', GenreIds: query.genreIds ? query.genreIds.join(',') : undefined, IncludeItemTypes: 'MusicAlbum', IsFavorite: query.favorite, @@ -399,7 +399,7 @@ export const JellyfinController: InternalControllerEndpoint = { const res = await jfApiClient(apiClientProps).getArtistList({ query: { - Fields: 'Genres, DateCreated, ExternalUrls, Overview', + Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName', ImageTypeLimit: 1, Limit: query.limit, ParentId: getLibraryId(query.musicFolderId), @@ -438,7 +438,7 @@ export const JellyfinController: InternalControllerEndpoint = { itemId: query.artistId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ParentId', + Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName', Limit: query.count, UserId: apiClientProps.server?.userId || undefined, }, @@ -794,7 +794,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server?.userId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId', + Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName', Ids: query.id, }, }); @@ -855,7 +855,7 @@ export const JellyfinController: InternalControllerEndpoint = { id: query.id, }, query: { - Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags, SortName', IncludeItemTypes: 'Audio', UserId: apiClientProps.server?.userId, }, @@ -902,7 +902,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server?.userId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName', GenreIds: query.genre ? query.genre : undefined, IncludeItemTypes: 'Audio', IsPlayed: @@ -974,7 +974,7 @@ export const JellyfinController: InternalControllerEndpoint = { itemId: query.songId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ParentId', + Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName', Limit: query.count, UserId: apiClientProps.server?.userId || undefined, }, @@ -1007,7 +1007,7 @@ export const JellyfinController: InternalControllerEndpoint = { itemId: query.songId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ParentId', + Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName', Limit: query.count, UserId: apiClientProps.server?.userId || undefined, }, @@ -1092,7 +1092,7 @@ export const JellyfinController: InternalControllerEndpoint = { query: { AlbumIds: albumIdsFilter, ArtistIds: artistIdsFilter, - Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName', GenreIds: query.genreIds?.join(','), IncludeItemTypes: 'Audio', IsFavorite: query.favorite, @@ -1127,7 +1127,7 @@ export const JellyfinController: InternalControllerEndpoint = { query: { AlbumIds: albumIdsFilter, ArtistIds: artistIdsFilter, - Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName', GenreIds: query.genreIds?.join(','), IncludeItemTypes: 'Audio', IsFavorite: query.favorite, @@ -1282,7 +1282,7 @@ export const JellyfinController: InternalControllerEndpoint = { }, query: { ArtistIds: query.artistId, - Fields: 'Genres, DateCreated, MediaSources, ParentId', + Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName', IncludeItemTypes: 'Audio', Limit: query.limit, Recursive: true, @@ -1378,7 +1378,7 @@ export const JellyfinController: InternalControllerEndpoint = { id: query.id, }, query: { - Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags, SortName', IncludeItemTypes: 'Audio', UserId: apiClientProps.server?.userId, }, @@ -1404,7 +1404,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server?.userId, }, query: { - Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId', + Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName', Ids: query.id, }, }); @@ -1562,7 +1562,7 @@ export const JellyfinController: InternalControllerEndpoint = { }, query: { EnableTotalRecordCount: true, - Fields: 'People, Tags', + Fields: 'People, Tags, SortName', ImageTypeLimit: 1, IncludeItemTypes: 'MusicAlbum', Limit: query.albumLimit, @@ -1610,7 +1610,7 @@ export const JellyfinController: InternalControllerEndpoint = { }, query: { EnableTotalRecordCount: true, - Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags', + Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName', IncludeItemTypes: 'Audio', Limit: query.songLimit, Recursive: true, diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 2ee225ce8..64d8ba5f2 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -46,6 +46,7 @@ const ALBUM_LIST_SORT_MAPPING: Record normalizeSong(song, server)), + sortName: item.SortName || item.Name, tags: getTags(item), updatedAt: item?.DateLastMediaAdded || item.DateCreated, userFavorite: item.UserData?.IsFavorite || false, diff --git a/src/shared/api/jellyfin/jellyfin-types.ts b/src/shared/api/jellyfin/jellyfin-types.ts index be2e0d6ce..5b25dbf6f 100644 --- a/src/shared/api/jellyfin/jellyfin-types.ts +++ b/src/shared/api/jellyfin/jellyfin-types.ts @@ -560,6 +560,7 @@ const album = z.object({ RunTimeTicks: z.number(), ServerId: z.string(), 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), Tags: z.string().array().optional(), Type: z.string(), diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index d38c8c786..3d19ab1f2 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -299,6 +299,7 @@ const normalizeSong = ( releaseYear: item.year || null, sampleRate: item.sampleRate || null, size: item.size, + sortName: item.orderTitle, tags: item.tags || null, trackNumber: item.trackNumber, trackSubtitle: item.tags?.subtitle ? item.tags.subtitle.join(' ยท ') : null, @@ -405,6 +406,7 @@ const normalizeAlbum = ( songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, pathReplace, pathReplaceWith)) : undefined, + sortName: item.orderAlbumName, tags: item.tags || null, updatedAt: item.updatedAt, userFavorite: item.starred || false, diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index a1b110933..30b6df32b 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -196,6 +196,7 @@ const normalizeSong = ( releaseYear: item.year || null, sampleRate: item.samplingRate || null, size: item.size, + sortName: item.title, tags: null, trackNumber: item.track || 1, trackSubtitle: null, @@ -321,6 +322,7 @@ const normalizeAlbum = ( (item as z.infer).song?.map((song) => normalizeSong(song, server, pathReplace, pathReplaceWith, undefined, discTitleMap), ) || [], + sortName: item.title, tags: null, updatedAt: item.created, userFavorite: Boolean(item.starred) || false, diff --git a/src/shared/api/utils.ts b/src/shared/api/utils.ts index 6585bc50b..c1317d2c5 100644 --- a/src/shared/api/utils.ts +++ b/src/shared/api/utils.ts @@ -244,6 +244,10 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor results = orderBy(results, ['releaseDate'], [order]); break; + case SongListSort.SORT_NAME: + results = orderBy(results, [(v) => v.sortName ?? v.name], [order]); + break; + case SongListSort.YEAR: results = orderBy( results, @@ -440,6 +444,9 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: case AlbumListSort.SONG_COUNT: results = orderBy(results, ['songCount'], [order]); break; + case AlbumListSort.SORT_NAME: + results = orderBy(results, [(v) => v.sortName ?? v.name], [order]); + break; case AlbumListSort.YEAR: results = orderBy(results, ['releaseYear'], [order]); break; diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 0d41e1844..f55466e32 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -192,6 +192,7 @@ export type Album = { size: null | number; songCount: null | number; songs?: Song[]; + sortName: string; tags: null | Record; updatedAt: string; userFavorite: boolean; @@ -392,6 +393,7 @@ export type Song = { releaseYear: null | number; sampleRate: null | number; size: number; + sortName: string; tags: null | Record; trackNumber: number; trackSubtitle: null | string; @@ -464,6 +466,7 @@ export enum AlbumListSort { RECENTLY_PLAYED = 'recentlyPlayed', RELEASE_DATE = 'releaseDate', SONG_COUNT = 'songCount', + SORT_NAME = 'sortName', YEAR = 'year', } @@ -518,6 +521,7 @@ export const albumListSortMap: AlbumListSortMap = { recentlyPlayed: undefined, releaseDate: JFAlbumListSort.RELEASE_DATE, songCount: undefined, + sortName: JFAlbumListSort.NAME, year: undefined, }, navidrome: { @@ -537,6 +541,7 @@ export const albumListSortMap: AlbumListSortMap = { // Recent versions of Navidrome support release date, but fallback to year for now releaseDate: NDAlbumListSort.YEAR, songCount: NDAlbumListSort.SONG_COUNT, + sortName: NDAlbumListSort.NAME, year: NDAlbumListSort.YEAR, }, subsonic: { @@ -555,6 +560,7 @@ export const albumListSortMap: AlbumListSortMap = { recentlyPlayed: undefined, releaseDate: undefined, songCount: undefined, + sortName: undefined, year: undefined, }, }; @@ -578,6 +584,7 @@ export enum SongListSort { RECENTLY_ADDED = 'recentlyAdded', RECENTLY_PLAYED = 'recentlyPlayed', RELEASE_DATE = 'releaseDate', + SORT_NAME = 'sortName', YEAR = 'year', } @@ -642,6 +649,7 @@ export const songListSortMap: SongListSortMap = { recentlyAdded: JFSongListSort.RECENTLY_ADDED, recentlyPlayed: JFSongListSort.RECENTLY_PLAYED, releaseDate: JFSongListSort.RELEASE_DATE, + sortName: JFSongListSort.NAME, year: undefined, }, navidrome: { @@ -663,6 +671,7 @@ export const songListSortMap: SongListSortMap = { recentlyAdded: NDSongListSort.RECENTLY_ADDED, recentlyPlayed: NDSongListSort.PLAY_DATE, releaseDate: undefined, + sortName: NDSongListSort.TITLE, year: NDSongListSort.YEAR, }, subsonic: { @@ -684,6 +693,7 @@ export const songListSortMap: SongListSortMap = { recentlyAdded: undefined, recentlyPlayed: undefined, releaseDate: undefined, + sortName: undefined, year: undefined, }, };