feat: sync play queue for navidrome/subsonic (#1335)

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
Kendall Garner
2025-12-13 05:05:00 +00:00
committed by GitHub
parent 13afd3d9c4
commit ed5d590a6b
31 changed files with 648 additions and 107 deletions
@@ -15,7 +15,7 @@ import {
ssType,
SubsonicExtensions,
} from '/@/shared/api/subsonic/subsonic-types';
import { sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/shared/api/utils';
import { hasFeature, sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/shared/api/utils';
import {
AlbumListSort,
GenreListSort,
@@ -27,7 +27,7 @@ import {
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ServerFeatures } from '/@/shared/types/features-types';
import { ServerFeature, ServerFeatures } from '/@/shared/types/features-types';
const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
@@ -913,6 +913,44 @@ export const SubsonicController: InternalControllerEndpoint = {
totalRecordCount: items.length,
};
},
getPlayQueue: async ({ apiClientProps }) => {
if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
const res = await ssApiClient(apiClientProps).getPlayQueueByIndex();
if (res.status !== 200) {
throw new Error('Failed to get random songs');
}
const { changed, changedBy, currentIndex, entry, position, username } =
res.body.playQueueByIndex;
return {
changed,
changedBy,
currentIndex: currentIndex ?? 0,
entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
positionMs: position ?? 0,
username,
};
} else {
const res = await ssApiClient(apiClientProps).getPlayQueue();
if (res.status !== 200) {
throw new Error('Failed to get random songs');
}
const { changed, changedBy, current, entry, position, username } = res.body.playQueue;
return {
changed,
changedBy,
currentIndex: current ? entry.findIndex((item) => item.id === current) : 0,
entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
positionMs: position ?? 0,
username,
};
}
},
getRandomSongList: async (args) => {
const { apiClientProps, query } = args;
@@ -967,6 +1005,7 @@ export const SubsonicController: InternalControllerEndpoint = {
final.splice(0, 0, { label: 'all artists', value: '' });
return final;
},
getServerInfo: async (args) => {
const { apiClientProps } = args;
@@ -1003,6 +1042,10 @@ export const SubsonicController: InternalControllerEndpoint = {
features.osFormPost = [1];
}
if (subsonicFeatures[SubsonicExtensions.INDEX_BASED_QUEUE]) {
features.serverPlayQueue = [1];
}
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
},
getSimilarSongs: async (args) => {
@@ -1586,6 +1629,36 @@ export const SubsonicController: InternalControllerEndpoint = {
return null;
},
savePlayQueue: async ({ apiClientProps, query }) => {
if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
const res = await ssApiClient(apiClientProps).savePlayQueueByIndex({
query: {
currentIndex: query.currentIndex !== undefined ? query.currentIndex : undefined,
id: query.songs,
position: query.positionMs,
},
});
if (res.status !== 200) {
throw new Error('Failed to save play queue');
}
} else {
const res = await ssApiClient(apiClientProps).savePlayQueue({
query: {
current:
query.currentIndex !== undefined && query.currentIndex < query.songs.length
? query.songs[query.currentIndex]
: undefined,
id: query.songs,
position: query.positionMs,
},
});
if (res.status !== 200) {
throw new Error('Failed to save play queue');
}
}
},
scrobble: async (args) => {
const { apiClientProps, query } = args;