Compare commits

..

3 Commits

Author SHA1 Message Date
jeffvli 95f395bd87 add jukebox endpoint / controller 2025-12-16 18:07:09 -08:00
jeffvli c9cd87bae5 remove release_channel from settings sync 2025-12-15 23:17:47 -08:00
jeffvli 9a8cb45510 Revert "prevent autoupdater from setting release channel (#1396)"
This reverts commit 614761efd7.
2025-12-15 23:06:25 -08:00
11 changed files with 178 additions and 12 deletions
+12 -3
View File
@@ -46,15 +46,24 @@ export default class AppUpdater {
const isBetaVersion = packageJson.version.includes('-beta'); const isBetaVersion = packageJson.version.includes('-beta');
const releaseChannel = store.get('release_channel'); const releaseChannel = store.get('release_channel');
const isNotConfigured = !releaseChannel;
console.log('[AppUpdater] Release channel from store: ', releaseChannel); console.log('Release channel: ', releaseChannel);
console.log('[AppUpdater] Is beta version: ', isBetaVersion); console.log('Is beta version: ', isBetaVersion);
if (isNotConfigured) {
console.log(
'Release channel not configured, setting to ',
isBetaVersion ? 'beta' : 'latest',
);
store.set('release_channel', isBetaVersion ? 'beta' : 'latest');
}
if (releaseChannel === 'beta') { if (releaseChannel === 'beta') {
autoUpdater.channel = 'beta'; autoUpdater.channel = 'beta';
autoUpdater.allowPrerelease = true; autoUpdater.allowPrerelease = true;
autoUpdater.disableDifferentialDownload = true; autoUpdater.disableDifferentialDownload = true;
} else { } else if (releaseChannel === 'latest') {
autoUpdater.channel = 'latest'; autoUpdater.channel = 'latest';
autoUpdater.allowDowngrade = true; autoUpdater.allowDowngrade = true;
autoUpdater.allowPrerelease = false; autoUpdater.allowPrerelease = false;
@@ -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;
+7
View File
@@ -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),
});
},
};
@@ -68,10 +68,12 @@ export const useSyncSettingsToMain = () => {
mainStoreKey: 'disable_auto_updates', mainStoreKey: 'disable_auto_updates',
rendererValue: settings.window.disableAutoUpdate, rendererValue: settings.window.disableAutoUpdate,
}, },
{ // For some reason after the application is updated, the release channel from the
mainStoreKey: 'release_channel', // renderer is always set to the latest channel. This causes an infinite update loop
rendererValue: settings.window.releaseChannel, // {
}, // mainStoreKey: 'release_channel',
// rendererValue: settings.window.releaseChannel,
// },
{ {
mainStoreKey: 'window_enable_tray', mainStoreKey: 'window_enable_tray',
rendererValue: settings.window.tray, rendererValue: settings.window.tray,
@@ -110,7 +112,6 @@ export const useSyncSettingsToMain = () => {
JSON.stringify(mainValueNormalized) !== JSON.stringify(rendererValueNormalized) JSON.stringify(mainValueNormalized) !== JSON.stringify(rendererValueNormalized)
) { ) {
hasDifferences = true; hasDifferences = true;
logFn.warn(logMsg.system.settingsSynchronized, { logFn.warn(logMsg.system.settingsSynchronized, {
meta: { meta: {
mainStoreKey: mapping.mainStoreKey, mainStoreKey: mapping.mainStoreKey,
@@ -118,14 +119,12 @@ export const useSyncSettingsToMain = () => {
rendererValue: rendererValueNormalized, rendererValue: rendererValueNormalized,
}, },
}); });
localSettings.set(mapping.mainStoreKey, rendererValue); localSettings.set(mapping.mainStoreKey, rendererValue);
} }
} }
// Show restart toast if there were differences // Show restart toast if there were differences
if (hasDifferences) { if (hasDifferences) {
logFn.info(logMsg.system.settingsSynchronized);
openRestartRequiredToast( openRestartRequiredToast(
i18n.t('error.settingsSyncError', { postProcess: 'sentenceCase' }), i18n.t('error.settingsSyncError', { postProcess: 'sentenceCase' }),
); );
+58
View File
@@ -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,
+35
View File
@@ -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;
+1
View File
@@ -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',