From 13528eff098aed37fc4b9f8b14236383ccca6c0e Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 29 Jul 2026 22:55:52 -0700 Subject: [PATCH] add library rescan to server selector menu --- src/i18n/locales/en.json | 1 + src/renderer/api/controller.ts | 12 +++++ src/renderer/api/jellyfin/jellyfin-api.ts | 5 ++ .../api/jellyfin/jellyfin-controller.ts | 48 +++++++++++++++++++ .../api/navidrome/navidrome-controller.ts | 1 + .../api/subsonic/subsonic-controller.ts | 11 +++++ .../features/shared/hooks/use-scan-status.ts | 2 +- .../components/server-selector-items.tsx | 27 +++++++++++ .../features/titlebar/components/app-menu.tsx | 25 ---------- src/shared/types/domain-types.ts | 8 ++++ 10 files changed, 114 insertions(+), 26 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b2e0743cc..b1440ff59 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -506,6 +506,7 @@ "goBack": "Go back", "goForward": "Go forward", "manageServers": "Manage servers", + "rescanLibrary": "Rescan library", "logout": "Logout", "privateModeOff": "Turn off private mode", "privateModeOn": "Turn on private mode", diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index c0cc1dee5..4ea6b8a3b 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -1046,6 +1046,18 @@ export const controller: GeneralController = { server.type, )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, + startLibraryScan(args) { + const server = getServerById(args.apiClientProps.serverId); + + if (!server) { + throw new Error(`${i18n.t('error.apiRouteError')}: startLibraryScan`); + } + + return apiController( + 'startLibraryScan', + server.type, + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); + }, updateInternetRadioStation(args) { const server = getServerById(args.apiClientProps.serverId); diff --git a/src/renderer/api/jellyfin/jellyfin-api.ts b/src/renderer/api/jellyfin/jellyfin-api.ts index 7136f7af2..79b50acc8 100644 --- a/src/renderer/api/jellyfin/jellyfin-api.ts +++ b/src/renderer/api/jellyfin/jellyfin-api.ts @@ -314,7 +314,12 @@ export const contract = c.router({ method: 'POST', path: 'Items/:id/Refresh', query: z.object({ + ImageRefreshMode: z.string().optional(), MetadataRefreshMode: z.string().optional(), + Recursive: z.boolean().optional(), + RegenerateTrickplay: z.boolean().optional(), + ReplaceAllImages: z.boolean().optional(), + ReplaceAllMetadata: z.boolean().optional(), }), responses: { 204: z.null(), diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index e957fb006..4cb24c69d 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -1917,6 +1917,54 @@ export const JellyfinController: InternalControllerEndpoint = { return null; }, + startLibraryScan: async (args) => { + const { apiClientProps } = args; + const server = apiClientProps.server; + const userId = server?.userId; + + if (!userId) { + throw new Error('No userId found'); + } + + let musicFolderIds = server.musicFolderId?.filter(Boolean) ?? []; + + if (musicFolderIds.length === 0) { + const res = await jfApiClient(apiClientProps).getMusicFolderList({ + params: { userId }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get music folders'); + } + + musicFolderIds = res.body.Items.filter( + (folder) => folder.CollectionType === jfType._enum.collection.MUSIC, + ).map((folder) => folder.Id); + } + + if (musicFolderIds.length === 0) { + throw new Error('No music folders found'); + } + + await Promise.all( + musicFolderIds.map((id) => + jfApiClient(apiClientProps).refreshItem({ + body: null, + params: { id }, + query: { + ImageRefreshMode: 'Default', + MetadataRefreshMode: 'Default', + Recursive: true, + RegenerateTrickplay: false, + ReplaceAllImages: false, + ReplaceAllMetadata: false, + }, + }), + ), + ); + + return null; + }, updateInternetRadioStation: async (args) => { const { apiClientProps, body, query } = args; diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index b9642a69d..4788159a2 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -1187,6 +1187,7 @@ export const NavidromeController: InternalControllerEndpoint = { id: res.body.data.id, }; }, + startLibraryScan: SubsonicController.startLibraryScan, updateInternetRadioStation: async (args) => { const { apiClientProps, body, query } = args; diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 572908bb6..a835c3a07 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -2366,6 +2366,17 @@ export const SubsonicController: InternalControllerEndpoint = { return null; }, + startLibraryScan: async (args) => { + const { apiClientProps } = args; + + const res = await ssApiClient(apiClientProps).startScan({ query: {} }); + + if (res.status !== 200) { + throw new Error('Failed to start library scan'); + } + + return null; + }, updateInternetRadioStation: async (args) => { const { apiClientProps, body, query } = args; diff --git a/src/renderer/features/shared/hooks/use-scan-status.ts b/src/renderer/features/shared/hooks/use-scan-status.ts index 45dd96e2e..e5a70cc91 100644 --- a/src/renderer/features/shared/hooks/use-scan-status.ts +++ b/src/renderer/features/shared/hooks/use-scan-status.ts @@ -49,7 +49,7 @@ const stopWatchingScan = () => { setIsWatchingScan(false); }; -const startScanWatch = () => { +export const startScanWatch = () => { clearWatchTimeout(); hasSeenScanStart = false; setIsWatchingScan(true); diff --git a/src/renderer/features/sidebar/components/server-selector-items.tsx b/src/renderer/features/sidebar/components/server-selector-items.tsx index 1a574f1e7..2e6ecd8ee 100644 --- a/src/renderer/features/sidebar/components/server-selector-items.tsx +++ b/src/renderer/features/sidebar/components/server-selector-items.tsx @@ -4,6 +4,7 @@ import isElectron from 'is-electron'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; +import { controller } from '/@/renderer/api/controller'; import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties'; import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png'; import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png'; @@ -11,11 +12,13 @@ import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.p import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form'; import { ServerList } from '/@/renderer/features/servers/components/server-list'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; +import { startScanWatch, useScanStatus } from '/@/renderer/features/shared/hooks/use-scan-status'; import { AppRoute } from '/@/renderer/router/routes'; import { useAuthStoreActions, useCurrentServer, useServerList } from '/@/renderer/store'; import { hasFeature } from '/@/shared/api/utils'; import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { Icon } from '/@/shared/components/icon/icon'; +import { toast } from '/@/shared/components/toast/toast'; import { ServerListItem, ServerListItemWithCredential, @@ -31,6 +34,7 @@ export const ServerSelectorItems = () => { const currentServer = useCurrentServer(); const serverList = useServerList(); const { logout, setCurrentServer, setMusicFolderId } = useAuthStoreActions(); + const { isScanning, isWatching } = useScanStatus(); const { data: musicFolders } = useQuery( currentServer @@ -137,6 +141,23 @@ export const ServerSelectorItems = () => { }, 0); }; + const handleRescanLibrary = async () => { + if (!currentServer || isWatching || isScanning) { + return; + } + + try { + await controller.startLibraryScan({ + apiClientProps: { serverId: currentServer.id }, + }); + startScanWatch(); + } catch (err) { + toast.error({ + message: err instanceof Error ? err.message : String(err), + }); + } + }; + return ( <> {t('page.appMenu.selectServer')} @@ -184,6 +205,12 @@ export const ServerSelectorItems = () => { > {t('page.appMenu.manageServers')} + } + onClick={handleRescanLibrary} + > + {t('page.appMenu.rescanLibrary')} + } onClick={handleLogout} diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index eb09c0e3d..5825712a2 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -1,4 +1,3 @@ -import { openModal } from '@mantine/modals'; import isElectron from 'is-electron'; import { Fragment, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; @@ -6,8 +5,6 @@ import { Link, useNavigate } from 'react-router'; import styles from './app-menu.module.css'; -import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties'; -import { ServerList } from '/@/renderer/features/servers/components/server-list'; import { UpdateAvailableButton } from '/@/renderer/features/settings/components/update-available-button'; import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal'; import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector'; @@ -120,13 +117,6 @@ export const AppMenu = () => { }); }; - const handleManageServersModal = () => { - openModal({ - children: , - title: t('page.manageServers.title'), - }); - }; - const handleQuit = () => { browser?.quit(); }; @@ -222,21 +212,6 @@ export const AppMenu = () => { id: 'divider-2', type: 'divider', }, - { - condition: !isServerLock(), - id: 'manage-servers', - item: { - label: t('page.appMenu.manageServers'), - leftSection: , - onClick: handleManageServersModal, - type: 'item', - }, - type: 'conditional-item', - }, - { - id: 'divider-3', - type: 'divider', - }, { icon: 'settings', id: 'settings', diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index ccf9a63e6..74d099090 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -1124,6 +1124,10 @@ export type ShareItemBody = { // Sharing export type ShareItemResponse = undefined | { id: string }; +export type StartLibraryScanArgs = BaseEndpointArgs; + +export type StartLibraryScanResponse = null; + export type UpdateInternetRadioStationArgs = BaseEndpointArgs & { body: UpdateInternetRadioStationBody; query: UpdateInternetRadioStationQuery; @@ -1602,6 +1606,7 @@ export type ControllerEndpoint = { setPlaylistSongs: (args: SetPlaylistSongsArgs) => Promise; setRating?: (args: SetRatingArgs) => Promise; shareItem?: (args: ShareItemArgs) => Promise; + startLibraryScan: (args: StartLibraryScanArgs) => Promise; updateInternetRadioStation: ( args: UpdateInternetRadioStationArgs, ) => Promise; @@ -1780,6 +1785,9 @@ export type InternalControllerEndpoint = { ) => Promise; setRating?: (args: ReplaceApiClientProps) => Promise; shareItem?: (args: ReplaceApiClientProps) => Promise; + startLibraryScan: ( + args: ReplaceApiClientProps, + ) => Promise; updateInternetRadioStation: ( args: ReplaceApiClientProps, ) => Promise;