feat: add jukebox player engine for server-side playback (#2109)

* feat: add jukebox player engine for server-side playback

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
mannansainidev
2026-07-10 09:59:22 +05:30
committed by GitHub
parent dd136f91e6
commit 0c89fea1ea
15 changed files with 743 additions and 31 deletions
+12
View File
@@ -762,6 +762,18 @@ export const controller: GeneralController = {
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
},
jukeboxControl(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(`${i18n.t('error.apiRouteError')}: jukeboxControl`);
}
const fn = apiController('jukeboxControl', server.type);
return fn
? fn(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }))
: Promise.resolve(null);
},
movePlaylistItem(args) {
const server = getServerById(args.apiClientProps.serverId);
@@ -745,6 +745,7 @@ export const NavidromeController: InternalControllerEndpoint = {
...navidromeFeatures,
publicPlaylist: [1],
[ServerFeature.ALBUM_YES_NO_RATING_FILTER]: [1],
[ServerFeature.JUKEBOX]: [1],
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
};
@@ -866,12 +867,12 @@ export const NavidromeController: InternalControllerEndpoint = {
totalRecordCount: albums.totalRecordCount,
};
},
getSongListCount: async ({ apiClientProps, query }) =>
NavidromeController.getSongList({
apiClientProps,
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getStreamUrl: SubsonicController.getStreamUrl,
getStructuredLyrics: SubsonicController.getStructuredLyrics,
getTagList: async (args) => {
@@ -1008,6 +1009,7 @@ export const NavidromeController: InternalControllerEndpoint = {
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
jukeboxControl: SubsonicController.jukeboxControl,
movePlaylistItem: async (args) => {
const { apiClientProps, query } = args;
@@ -274,6 +274,14 @@ export const contract = c.router({
200: ssType._response.user,
},
},
jukeboxControl: {
method: 'GET',
path: 'jukeboxControl.view',
query: ssType._parameters.jukeboxControl,
responses: {
200: ssType._response.jukeboxControl,
},
},
ping: {
method: 'GET',
path: 'ping.view',
@@ -1395,6 +1395,22 @@ export const SubsonicController: InternalControllerEndpoint = {
if (subsonicFeatures[SubsonicExtensions.PLAYBACK_REPORT]) {
features.reportPlayback = [1];
}
try {
const jukeboxStatus = await ssApiClient(apiClientProps).jukeboxControl({
query: { action: 'status' },
});
if (jukeboxStatus.status === 200 && !(jukeboxStatus.body as any)?.error) {
features[ServerFeature.JUKEBOX] = [1];
} else {
console.log(
'Jukebox endpoint returned an error payload:',
(jukeboxStatus.body as any)?.error,
);
}
} catch (error) {
console.log('Jukebox is not supported by this server:', error);
}
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
},
@@ -2052,6 +2068,25 @@ export const SubsonicController: InternalControllerEndpoint = {
name: res.body.user.username,
};
},
jukeboxControl: async (args) => {
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).jukeboxControl({
query: {
action: query.action,
gain: query.gain,
id: query.id,
index: query.index,
offset: query.offset,
},
});
if (res.status !== 200) {
throw new Error('Failed to control jukebox');
}
return res.body;
},
removeFromPlaylist: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({
query: {