mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 20:40:15 +02:00
feat: sync play queue for navidrome/subsonic (#1335)
--------- Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
@@ -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 || '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]>>]
|
||||
>;
|
||||
|
||||
@@ -12,7 +12,7 @@ interface DragDropZoneProps {
|
||||
}
|
||||
|
||||
export const DragDropZone = ({ icon, onItemSelected, validateItem }: DragDropZoneProps) => {
|
||||
const zoneFileInput = useRef<HTMLInputElement | null>();
|
||||
const zoneFileInput = useRef<HTMLInputElement | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const processItem = useCallback(
|
||||
@@ -122,7 +122,9 @@ export const DragDropZone = ({ icon, onItemSelected, validateItem }: DragDropZon
|
||||
) : null}
|
||||
<input
|
||||
onChange={onZoneInputChange}
|
||||
ref={(self) => (zoneFileInput.current = self)}
|
||||
ref={(self) => {
|
||||
zoneFileInput.current = self;
|
||||
}}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
LuSun,
|
||||
LuTable,
|
||||
LuTriangleAlert,
|
||||
LuUpload,
|
||||
LuUser,
|
||||
LuUserPen,
|
||||
LuUserRoundCog,
|
||||
@@ -227,6 +228,7 @@ export const AppIcon = {
|
||||
themeLight: LuSun,
|
||||
track: LuMusic2,
|
||||
unfavorite: LuHeartCrack,
|
||||
upload: LuUpload,
|
||||
user: LuUser,
|
||||
userManage: LuUserRoundCog,
|
||||
visibility: MdOutlineVisibility,
|
||||
|
||||
@@ -1286,6 +1286,7 @@ export type ControllerEndpoint = {
|
||||
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
||||
getPlaylistListCount: (args: PlaylistListCountArgs) => Promise<number>;
|
||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
||||
getPlayQueue: (args: GetQueueArgs) => Promise<GetQueueResponse>;
|
||||
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
||||
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
|
||||
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
|
||||
@@ -1303,6 +1304,7 @@ export type ControllerEndpoint = {
|
||||
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||
replacePlaylist: (args: ReplacePlaylistArgs) => Promise<ReplacePlaylistResponse>;
|
||||
savePlayQueue: (args: SaveQueueArgs) => Promise<void>;
|
||||
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
||||
search: (args: SearchArgs) => Promise<SearchResponse>;
|
||||
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
||||
@@ -1327,6 +1329,19 @@ export type FontData = {
|
||||
style: string;
|
||||
};
|
||||
|
||||
export type GetQueueArgs = BaseEndpointArgs;
|
||||
|
||||
export interface GetQueueQuery {}
|
||||
|
||||
export type GetQueueResponse = {
|
||||
changed: string;
|
||||
changedBy: string;
|
||||
currentIndex: number;
|
||||
entry: Song[];
|
||||
positionMs: number;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type InternalControllerEndpoint = {
|
||||
addToPlaylist: (
|
||||
args: ReplaceApiClientProps<AddToPlaylistArgs>,
|
||||
@@ -1376,6 +1391,7 @@ export type InternalControllerEndpoint = {
|
||||
getPlaylistSongList: (
|
||||
args: ReplaceApiClientProps<PlaylistSongListArgs>,
|
||||
) => Promise<SongListResponse>;
|
||||
getPlayQueue: (args: ReplaceApiClientProps<GetQueueArgs>) => Promise<GetQueueResponse>;
|
||||
getRandomSongList: (
|
||||
args: ReplaceApiClientProps<RandomSongListArgs>,
|
||||
) => Promise<SongListResponse>;
|
||||
@@ -1402,6 +1418,7 @@ export type InternalControllerEndpoint = {
|
||||
replacePlaylist: (
|
||||
args: ReplaceApiClientProps<ReplacePlaylistArgs>,
|
||||
) => Promise<ReplacePlaylistResponse>;
|
||||
savePlayQueue: (args: ReplaceApiClientProps<SaveQueueArgs>) => Promise<void>;
|
||||
scrobble: (args: ReplaceApiClientProps<ScrobbleArgs>) => Promise<ScrobbleResponse>;
|
||||
search: (args: ReplaceApiClientProps<SearchArgs>) => Promise<SearchResponse>;
|
||||
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
||||
@@ -1439,6 +1456,16 @@ export type MoveItemQuery = {
|
||||
|
||||
export type ReplaceApiClientProps<T> = BaseEndpointArgsWithServer & Omit<T, 'apiClientProps'>;
|
||||
|
||||
export type SaveQueueArgs = BaseEndpointArgs & {
|
||||
query: SaveQueueQuery;
|
||||
};
|
||||
|
||||
export type SaveQueueQuery = {
|
||||
currentIndex?: number;
|
||||
positionMs?: number;
|
||||
songs: string[];
|
||||
};
|
||||
|
||||
export type ServerInfo = {
|
||||
features: ServerFeatures;
|
||||
id?: string;
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum ServerFeature {
|
||||
OS_FORM_POST = 'osFormPost',
|
||||
PLAYLISTS_SMART = 'playlistsSmart',
|
||||
PUBLIC_PLAYLIST = 'publicPlaylist',
|
||||
SERVER_PLAY_QUEUE = 'serverPlayQueue',
|
||||
SHARING_ALBUM_SONG = 'sharingAlbumSong',
|
||||
TAGS = 'tags',
|
||||
TRACK_ALBUM_ARTIST_SEARCH = 'trackAlbumArtistSearch',
|
||||
|
||||
Reference in New Issue
Block a user