mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
1 Commits
v1.10.0
...
feat/jukebox
| Author | SHA1 | Date | |
|---|---|---|---|
| 95f395bd87 |
@@ -1202,6 +1202,9 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
name: res.body.Name,
|
name: res.body.Name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
jukeboxControl: async () => {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
movePlaylistItem: async (args) => {
|
movePlaylistItem: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
const features = {
|
const features = {
|
||||||
...subsonicArgs.features,
|
...subsonicArgs.features,
|
||||||
...navidromeFeatures,
|
...navidromeFeatures,
|
||||||
|
jukebox: [1],
|
||||||
publicPlaylist: [1],
|
publicPlaylist: [1],
|
||||||
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
|
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
|
||||||
};
|
};
|
||||||
@@ -761,6 +762,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
jukeboxControl: SubsonicController.jukeboxControl,
|
||||||
movePlaylistItem: async (args) => {
|
movePlaylistItem: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
ArtistListQuery,
|
ArtistListQuery,
|
||||||
FolderQuery,
|
FolderQuery,
|
||||||
GenreListQuery,
|
GenreListQuery,
|
||||||
|
JukeboxControlQuery,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricsQuery,
|
LyricsQuery,
|
||||||
PlaylistDetailQuery,
|
PlaylistDetailQuery,
|
||||||
@@ -262,6 +263,12 @@ export const queryKeys: Record<
|
|||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'genres'] as const,
|
root: (serverId: string) => [serverId, 'genres'] as const,
|
||||||
},
|
},
|
||||||
|
jukebox: {
|
||||||
|
control: (serverId: string, query?: JukeboxControlQuery) => {
|
||||||
|
if (query) return [serverId, 'jukebox', 'control', query] as const;
|
||||||
|
return [serverId, 'jukebox', 'control'] as const;
|
||||||
|
},
|
||||||
|
},
|
||||||
musicFolders: {
|
musicFolders: {
|
||||||
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
|
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -249,6 +249,14 @@ export const contract = c.router({
|
|||||||
200: ssType._response.user,
|
200: ssType._response.user,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
jukeboxControl: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'jukeboxControl.view',
|
||||||
|
query: ssType._parameters.jukeboxControl,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.jukeboxPlaylist,
|
||||||
|
},
|
||||||
|
},
|
||||||
ping: {
|
ping: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'ping.view',
|
path: 'ping.view',
|
||||||
|
|||||||
@@ -867,7 +867,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlaylistList: async ({ apiClientProps, query }) => {
|
getPlaylistList: async ({ apiClientProps, query }) => {
|
||||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
@@ -1060,7 +1059,9 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to ping server');
|
throw new Error('Failed to ping server');
|
||||||
}
|
}
|
||||||
|
|
||||||
const features: ServerFeatures = {};
|
const features: ServerFeatures = {
|
||||||
|
jukebox: [1],
|
||||||
|
};
|
||||||
|
|
||||||
if (!ping.body.openSubsonic || !ping.body.serverVersion) {
|
if (!ping.body.openSubsonic || !ping.body.serverVersion) {
|
||||||
return { features, version: ping.body.version };
|
return { features, version: ping.body.version };
|
||||||
@@ -1579,6 +1580,30 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
name: res.body.user.username,
|
name: res.body.user.username,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
jukeboxControl: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).jukeboxControl({
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to control jukebox');
|
||||||
|
}
|
||||||
|
|
||||||
|
const jukeboxPlaylist = res.body.jukeboxPlaylist;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentIndex: jukeboxPlaylist.currentIndex,
|
||||||
|
gain: jukeboxPlaylist.gain,
|
||||||
|
playing: jukeboxPlaylist.playing,
|
||||||
|
position: jukeboxPlaylist.position ?? 0,
|
||||||
|
songs:
|
||||||
|
jukeboxPlaylist.entry?.map((song) =>
|
||||||
|
ssNormalize.song(song, apiClientProps.server),
|
||||||
|
) || [],
|
||||||
|
};
|
||||||
|
},
|
||||||
removeFromPlaylist: async ({ apiClientProps, query }) => {
|
removeFromPlaylist: async ({ apiClientProps, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { JukeboxControlQuery } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const jukeboxQueries = {
|
||||||
|
jukeboxControl: (args: QueryHookArgs<JukeboxControlQuery>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
api.controller.jukeboxControl({
|
||||||
|
apiClientProps: { serverId: args.serverId, signal },
|
||||||
|
query: args.query,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.jukebox.control(args.serverId, args.query),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -692,6 +692,61 @@ const getInternetRadioStations = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const jukeboxStatus = z.object({
|
||||||
|
jukeboxStatus: z.object({
|
||||||
|
currentIndex: z.number().describe('The index of the current song in the queue'),
|
||||||
|
gain: z.number().describe('Volume, in a range of [0.0, 1.0]'),
|
||||||
|
playing: z.boolean().describe('Whether the jukebox is playing'),
|
||||||
|
position: z.number().optional().describe('The position of the current song in seconds'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const jukeboxPlaylist = z.object({
|
||||||
|
jukeboxPlaylist: z.object({
|
||||||
|
currentIndex: z.number().describe('The index of the current song in the queue'),
|
||||||
|
entry: z.array(song).optional(),
|
||||||
|
gain: z.number().describe('Volume, in a range of [0.0, 1.0]'),
|
||||||
|
playing: z.boolean().describe('Whether the jukebox is playing'),
|
||||||
|
position: z.number().optional().describe('The position of the current song in seconds'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const jukeboxControlParameters = z.object({
|
||||||
|
action: z.enum([
|
||||||
|
'get',
|
||||||
|
'status',
|
||||||
|
'set',
|
||||||
|
'start',
|
||||||
|
'stop',
|
||||||
|
'skip',
|
||||||
|
'add',
|
||||||
|
'clear',
|
||||||
|
'remove',
|
||||||
|
'shuffle',
|
||||||
|
'setGain',
|
||||||
|
]),
|
||||||
|
gain: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Used by setGain to control the playback volume. A float value between 0.0 and 1.0.',
|
||||||
|
),
|
||||||
|
id: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Used by add and set. ID of song to add to the jukebox playlist. Use multiple id parameters to add many songs in the same request. (set is similar to a clear followed by a add, but will not change the currently playing track.)',
|
||||||
|
),
|
||||||
|
index: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe('Used by skip and remove. Zero-based index of the song to skip to or remove.'),
|
||||||
|
offset: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe('Used by skip. Start playing this many seconds into the track.'),
|
||||||
|
});
|
||||||
|
|
||||||
export const ssType = {
|
export const ssType = {
|
||||||
_parameters: {
|
_parameters: {
|
||||||
albumInfo: albumInfoParameters,
|
albumInfo: albumInfoParameters,
|
||||||
@@ -716,6 +771,7 @@ export const ssType = {
|
|||||||
getSong: getSongParameters,
|
getSong: getSongParameters,
|
||||||
getSongsByGenre: getSongsByGenreParameters,
|
getSongsByGenre: getSongsByGenreParameters,
|
||||||
getStarred: getStarredParameters,
|
getStarred: getStarredParameters,
|
||||||
|
jukeboxControl: jukeboxControlParameters,
|
||||||
randomSongList: randomSongListParameters,
|
randomSongList: randomSongListParameters,
|
||||||
removeFavorite: removeFavoriteParameters,
|
removeFavorite: removeFavoriteParameters,
|
||||||
savePlayQueueByIndex: savePlayQueueByIndexParameters,
|
savePlayQueueByIndex: savePlayQueueByIndexParameters,
|
||||||
@@ -761,6 +817,8 @@ export const ssType = {
|
|||||||
getSongsByGenre,
|
getSongsByGenre,
|
||||||
getStarred,
|
getStarred,
|
||||||
internetRadioStation,
|
internetRadioStation,
|
||||||
|
jukeboxPlaylist,
|
||||||
|
jukeboxStatus,
|
||||||
musicFolderList,
|
musicFolderList,
|
||||||
ping,
|
ping,
|
||||||
playlist,
|
playlist,
|
||||||
|
|||||||
@@ -1364,6 +1364,7 @@ export type ControllerEndpoint = {
|
|||||||
// getArtistInfo?: (args: any) => void;
|
// getArtistInfo?: (args: any) => void;
|
||||||
getUserInfo: (args: UserInfoArgs) => Promise<UserInfoResponse>;
|
getUserInfo: (args: UserInfoArgs) => Promise<UserInfoResponse>;
|
||||||
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
||||||
|
jukeboxControl: (args: JukeboxControlArgs) => Promise<JukeboxControlResponse>;
|
||||||
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
||||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||||
replacePlaylist: (args: ReplacePlaylistArgs) => Promise<ReplacePlaylistResponse>;
|
replacePlaylist: (args: ReplacePlaylistArgs) => Promise<ReplacePlaylistResponse>;
|
||||||
@@ -1486,6 +1487,9 @@ export type InternalControllerEndpoint = {
|
|||||||
getTopSongs: (args: ReplaceApiClientProps<TopSongListArgs>) => Promise<TopSongListResponse>;
|
getTopSongs: (args: ReplaceApiClientProps<TopSongListArgs>) => Promise<TopSongListResponse>;
|
||||||
getUserInfo: (args: ReplaceApiClientProps<UserInfoArgs>) => Promise<UserInfoResponse>;
|
getUserInfo: (args: ReplaceApiClientProps<UserInfoArgs>) => Promise<UserInfoResponse>;
|
||||||
getUserList?: (args: ReplaceApiClientProps<UserListArgs>) => Promise<UserListResponse>;
|
getUserList?: (args: ReplaceApiClientProps<UserListArgs>) => Promise<UserListResponse>;
|
||||||
|
jukeboxControl: (
|
||||||
|
args: ReplaceApiClientProps<JukeboxControlArgs>,
|
||||||
|
) => Promise<JukeboxControlResponse>;
|
||||||
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
|
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
|
||||||
removeFromPlaylist: (
|
removeFromPlaylist: (
|
||||||
args: ReplaceApiClientProps<RemoveFromPlaylistArgs>,
|
args: ReplaceApiClientProps<RemoveFromPlaylistArgs>,
|
||||||
@@ -1506,6 +1510,37 @@ export type InternalControllerEndpoint = {
|
|||||||
) => Promise<UpdatePlaylistResponse>;
|
) => Promise<UpdatePlaylistResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JukeboxControlArgs = BaseEndpointArgs & {
|
||||||
|
query: JukeboxControlQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JukeboxControlQuery = {
|
||||||
|
action:
|
||||||
|
| 'add'
|
||||||
|
| 'clear'
|
||||||
|
| 'get'
|
||||||
|
| 'remove'
|
||||||
|
| 'set'
|
||||||
|
| 'setGain'
|
||||||
|
| 'shuffle'
|
||||||
|
| 'skip'
|
||||||
|
| 'start'
|
||||||
|
| 'status'
|
||||||
|
| 'stop';
|
||||||
|
gain?: number;
|
||||||
|
id?: string;
|
||||||
|
index?: number;
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JukeboxControlResponse = {
|
||||||
|
currentIndex: number;
|
||||||
|
gain: number;
|
||||||
|
playing: boolean;
|
||||||
|
position: number;
|
||||||
|
songs: Song[];
|
||||||
|
};
|
||||||
|
|
||||||
export type LyricGetQuery = {
|
export type LyricGetQuery = {
|
||||||
remoteSongId: string;
|
remoteSongId: string;
|
||||||
remoteSource: LyricSource;
|
remoteSource: LyricSource;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
|
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
|
||||||
export enum ServerFeature {
|
export enum ServerFeature {
|
||||||
BFR = 'bfr',
|
BFR = 'bfr',
|
||||||
|
JUKEBOX = 'jukebox',
|
||||||
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
||||||
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
||||||
MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect',
|
MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect',
|
||||||
|
|||||||
Reference in New Issue
Block a user