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
@@ -14,6 +14,8 @@ import {
} from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types';
const TICKS_PER_MS = 10000;
const getAlbumArtistCoverArtUrl = (args: {
baseUrl: string;
item: z.infer<typeof jfType._response.albumArtist>;
@@ -221,7 +223,7 @@ const normalizeSong = (
createdAt: item.DateCreated,
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
discSubtitle: null,
duration: item.RunTimeTicks / 10000,
duration: item.RunTimeTicks / TICKS_PER_MS,
explicitStatus: null,
gain:
item.NormalizationGain !== undefined
@@ -294,7 +296,7 @@ const normalizeAlbum = (
backdropImageUrl: null,
comment: null,
createdAt: item.DateCreated,
duration: item.RunTimeTicks / 10000,
duration: item.RunTimeTicks / TICKS_PER_MS,
explicitStatus: null,
genres:
item.GenreItems?.map((entry) => ({
@@ -363,7 +365,7 @@ const normalizeAlbumArtist = (
albumCount: item.AlbumCount ?? null,
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000,
duration: item.RunTimeTicks / TICKS_PER_MS,
genres: item.GenreItems?.map((entry) => ({
_itemType: LibraryItem.GENRE,
_serverId: server?.id || '',
@@ -409,7 +411,7 @@ const normalizePlaylist = (
_serverId: server?.id || '',
_serverType: ServerType.JELLYFIN,
description: item.Overview || null,
duration: item.RunTimeTicks / 10000,
duration: item.RunTimeTicks / TICKS_PER_MS,
genres: item.GenreItems?.map((entry) => ({
_itemType: LibraryItem.GENRE,
_serverId: server?.id || '',
+26
View File
@@ -253,6 +253,7 @@ const sessionInfo = z.object({
CanSeek: z.boolean(),
IsMuted: z.boolean(),
IsPaused: z.boolean(),
PositionTicks: z.number().optional(),
RepeatMode: z.string(),
}),
RemoteEndPoint: z.string(),
@@ -801,6 +802,28 @@ const folderParameters = z.object({
SortOrder: z.enum(sortOrderValues).optional(),
});
const queueItem = z.object({
Id: z.string(),
PlaylistItemId: z.string().optional(),
});
const saveQueueParameters = scrobbleParameters.merge(
z.object({
NowPlayingQueue: z.array(queueItem),
PlaylistItemId: z.string().optional(),
}),
);
const getQueueParameters = z.object({});
const getSessions = z.array(
sessionInfo.merge(
z.object({
PlaylistItemId: z.string().optional(),
}),
),
);
export const jfType = {
_enum: {
albumArtistList: albumArtistListSort,
@@ -825,10 +848,12 @@ export const jfType = {
filterList: filterListParameters,
folder: folderParameters,
genreList: genreListParameters,
getQueue: getQueueParameters,
musicFolderList: musicFolderListParameters,
playlistDetail: playlistDetailParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
saveQueue: saveQueueParameters,
scrobble: scrobbleParameters,
search: searchParameters,
similarArtistList: similarArtistListParameters,
@@ -853,6 +878,7 @@ export const jfType = {
folderList,
genre,
genreList,
getSessions,
lyrics,
moveItem,
musicFolder,
@@ -676,6 +676,25 @@ const tagListParameters = optionalPaginationParameters.extend({
tag_value: z.string().optional(), // Search
});
const saveQueueParameters = z.object({
current: z.number().optional(),
ids: z.array(z.string()).optional(),
position: z.number().optional(),
});
const saveQueue = z.null();
const queue = z.object({
changedBy: z.string(),
createdAt: z.string(),
current: z.number(),
id: z.string(),
items: z.array(song),
position: z.number(),
updatedAt: z.string(),
userId: z.string(),
});
export const ndType = {
_enum: {
albumArtistList: NDAlbumArtistListSort,
@@ -696,6 +715,7 @@ export const ndType = {
moveItem: moveItemParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
saveQueue: saveQueueParameters,
shareItem: shareItemParameters,
songList: songListParameters,
tagList: tagListParameters,
@@ -719,7 +739,9 @@ export const ndType = {
playlistList,
playlistSong,
playlistSongList,
queue,
removeFromPlaylist,
saveQueue,
shareItem,
song,
songList,
+42
View File
@@ -356,6 +356,7 @@ const similarSongs = z.object({
export enum SubsonicExtensions {
FORM_POST = 'formPost',
INDEX_BASED_QUEUE = 'indexBasedQueue',
SONG_LYRICS = 'songLyrics',
TRANSCODE_OFFSET = 'transcodeOffset',
}
@@ -617,6 +618,42 @@ const getIndexesParameters = z.object({
musicFolderId: z.string().optional(),
});
const saveQueueParameters = z.object({
current: z.string().optional(),
id: z.string().array(),
position: z.number().optional(),
});
const savePlayQueueByIndexParameters = z.object({
currentIndex: z.number().optional(),
id: z.string().array().optional(),
position: z.number().optional(),
});
const saveQueue = z.null();
const playQueue = z.object({
playQueue: z.object({
changed: z.string(),
changedBy: z.string(),
current: z.string().optional(),
entry: song.array(),
position: z.number().optional(),
username: z.string(),
}),
});
const playQueueByIndex = z.object({
playQueueByIndex: z.object({
changed: z.string(),
changedBy: z.string(),
currentIndex: z.number().optional(),
entry: song.array().optional(),
position: z.number().optional(),
username: z.string(),
}),
});
export const ssType = {
_parameters: {
albumInfo: albumInfoParameters,
@@ -641,6 +678,8 @@ export const ssType = {
getStarred: getStarredParameters,
randomSongList: randomSongListParameters,
removeFavorite: removeFavoriteParameters,
savePlayQueueByIndex: savePlayQueueByIndexParameters,
saveQueue: saveQueueParameters,
scrobble: scrobbleParameters,
search3: search3Parameters,
setRating: setRatingParameters,
@@ -681,8 +720,11 @@ export const ssType = {
ping,
playlist,
playlistListEntry,
playQueue,
playQueueByIndex,
randomSongList,
removeFavorite,
saveQueue,
scrobble,
search3,
serverInfo,
+12
View File
@@ -49,6 +49,18 @@ export const hasFeature = (server: null | ServerListItem, feature: ServerFeature
return (server.features[feature]?.length || 0) > 0;
};
export const hasFeatureWithVersion = (
server: null | ServerListItem,
feature: ServerFeature,
version: number,
): boolean => {
if (!server || !server.features) {
return false;
}
return (server.features[feature] ?? []).includes(version);
};
export type VersionInfo = ReadonlyArray<
[string, Partial<Record<ServerFeature, readonly number[]>>]
>;