mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 05:12:57 +02:00
add library rescan to server selector menu
This commit is contained in:
@@ -506,6 +506,7 @@
|
|||||||
"goBack": "Go back",
|
"goBack": "Go back",
|
||||||
"goForward": "Go forward",
|
"goForward": "Go forward",
|
||||||
"manageServers": "Manage servers",
|
"manageServers": "Manage servers",
|
||||||
|
"rescanLibrary": "Rescan library",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"privateModeOff": "Turn off private mode",
|
"privateModeOff": "Turn off private mode",
|
||||||
"privateModeOn": "Turn on private mode",
|
"privateModeOn": "Turn on private mode",
|
||||||
|
|||||||
@@ -1046,6 +1046,18 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(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) {
|
updateInternetRadioStation(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
|||||||
@@ -314,7 +314,12 @@ export const contract = c.router({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: 'Items/:id/Refresh',
|
path: 'Items/:id/Refresh',
|
||||||
query: z.object({
|
query: z.object({
|
||||||
|
ImageRefreshMode: z.string().optional(),
|
||||||
MetadataRefreshMode: z.string().optional(),
|
MetadataRefreshMode: z.string().optional(),
|
||||||
|
Recursive: z.boolean().optional(),
|
||||||
|
RegenerateTrickplay: z.boolean().optional(),
|
||||||
|
ReplaceAllImages: z.boolean().optional(),
|
||||||
|
ReplaceAllMetadata: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
responses: {
|
responses: {
|
||||||
204: z.null(),
|
204: z.null(),
|
||||||
|
|||||||
@@ -1917,6 +1917,54 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
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) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -1187,6 +1187,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
id: res.body.data.id,
|
id: res.body.data.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
startLibraryScan: SubsonicController.startLibraryScan,
|
||||||
updateInternetRadioStation: async (args) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -2366,6 +2366,17 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
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) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const stopWatchingScan = () => {
|
|||||||
setIsWatchingScan(false);
|
setIsWatchingScan(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startScanWatch = () => {
|
export const startScanWatch = () => {
|
||||||
clearWatchTimeout();
|
clearWatchTimeout();
|
||||||
hasSeenScanStart = false;
|
hasSeenScanStart = false;
|
||||||
setIsWatchingScan(true);
|
setIsWatchingScan(true);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import isElectron from 'is-electron';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { controller } from '/@/renderer/api/controller';
|
||||||
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
||||||
import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png';
|
import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png';
|
||||||
import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.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 { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
|
||||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
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 { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAuthStoreActions, useCurrentServer, useServerList } from '/@/renderer/store';
|
import { useAuthStoreActions, useCurrentServer, useServerList } from '/@/renderer/store';
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import {
|
import {
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerListItemWithCredential,
|
ServerListItemWithCredential,
|
||||||
@@ -31,6 +34,7 @@ export const ServerSelectorItems = () => {
|
|||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
const serverList = useServerList();
|
const serverList = useServerList();
|
||||||
const { logout, setCurrentServer, setMusicFolderId } = useAuthStoreActions();
|
const { logout, setCurrentServer, setMusicFolderId } = useAuthStoreActions();
|
||||||
|
const { isScanning, isWatching } = useScanStatus();
|
||||||
|
|
||||||
const { data: musicFolders } = useQuery(
|
const { data: musicFolders } = useQuery(
|
||||||
currentServer
|
currentServer
|
||||||
@@ -137,6 +141,23 @@ export const ServerSelectorItems = () => {
|
|||||||
}, 0);
|
}, 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Label>{t('page.appMenu.selectServer')}</DropdownMenu.Label>
|
<DropdownMenu.Label>{t('page.appMenu.selectServer')}</DropdownMenu.Label>
|
||||||
@@ -184,6 +205,12 @@ export const ServerSelectorItems = () => {
|
|||||||
>
|
>
|
||||||
{t('page.appMenu.manageServers')}
|
{t('page.appMenu.manageServers')}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
leftSection={<Icon icon="refresh" />}
|
||||||
|
onClick={handleRescanLibrary}
|
||||||
|
>
|
||||||
|
{t('page.appMenu.rescanLibrary')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
leftSection={<Icon color="error" icon="signOut" />}
|
leftSection={<Icon color="error" icon="signOut" />}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { openModal } from '@mantine/modals';
|
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { Fragment, ReactNode } from 'react';
|
import { Fragment, ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -6,8 +5,6 @@ import { Link, useNavigate } from 'react-router';
|
|||||||
|
|
||||||
import styles from './app-menu.module.css';
|
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 { UpdateAvailableButton } from '/@/renderer/features/settings/components/update-available-button';
|
||||||
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
||||||
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
||||||
@@ -120,13 +117,6 @@ export const AppMenu = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManageServersModal = () => {
|
|
||||||
openModal({
|
|
||||||
children: <ServerList />,
|
|
||||||
title: t('page.manageServers.title'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuit = () => {
|
const handleQuit = () => {
|
||||||
browser?.quit();
|
browser?.quit();
|
||||||
};
|
};
|
||||||
@@ -222,21 +212,6 @@ export const AppMenu = () => {
|
|||||||
id: 'divider-2',
|
id: 'divider-2',
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
condition: !isServerLock(),
|
|
||||||
id: 'manage-servers',
|
|
||||||
item: {
|
|
||||||
label: t('page.appMenu.manageServers'),
|
|
||||||
leftSection: <Icon icon="edit" />,
|
|
||||||
onClick: handleManageServersModal,
|
|
||||||
type: 'item',
|
|
||||||
},
|
|
||||||
type: 'conditional-item',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'divider-3',
|
|
||||||
type: 'divider',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
id: 'settings',
|
id: 'settings',
|
||||||
|
|||||||
@@ -1124,6 +1124,10 @@ export type ShareItemBody = {
|
|||||||
// Sharing
|
// Sharing
|
||||||
export type ShareItemResponse = undefined | { id: string };
|
export type ShareItemResponse = undefined | { id: string };
|
||||||
|
|
||||||
|
export type StartLibraryScanArgs = BaseEndpointArgs;
|
||||||
|
|
||||||
|
export type StartLibraryScanResponse = null;
|
||||||
|
|
||||||
export type UpdateInternetRadioStationArgs = BaseEndpointArgs & {
|
export type UpdateInternetRadioStationArgs = BaseEndpointArgs & {
|
||||||
body: UpdateInternetRadioStationBody;
|
body: UpdateInternetRadioStationBody;
|
||||||
query: UpdateInternetRadioStationQuery;
|
query: UpdateInternetRadioStationQuery;
|
||||||
@@ -1602,6 +1606,7 @@ export type ControllerEndpoint = {
|
|||||||
setPlaylistSongs: (args: SetPlaylistSongsArgs) => Promise<SetPlaylistSongsResponse>;
|
setPlaylistSongs: (args: SetPlaylistSongsArgs) => Promise<SetPlaylistSongsResponse>;
|
||||||
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
||||||
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
||||||
|
startLibraryScan: (args: StartLibraryScanArgs) => Promise<StartLibraryScanResponse>;
|
||||||
updateInternetRadioStation: (
|
updateInternetRadioStation: (
|
||||||
args: UpdateInternetRadioStationArgs,
|
args: UpdateInternetRadioStationArgs,
|
||||||
) => Promise<UpdateInternetRadioStationResponse>;
|
) => Promise<UpdateInternetRadioStationResponse>;
|
||||||
@@ -1780,6 +1785,9 @@ export type InternalControllerEndpoint = {
|
|||||||
) => Promise<SetPlaylistSongsResponse>;
|
) => Promise<SetPlaylistSongsResponse>;
|
||||||
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
||||||
shareItem?: (args: ReplaceApiClientProps<ShareItemArgs>) => Promise<ShareItemResponse>;
|
shareItem?: (args: ReplaceApiClientProps<ShareItemArgs>) => Promise<ShareItemResponse>;
|
||||||
|
startLibraryScan: (
|
||||||
|
args: ReplaceApiClientProps<StartLibraryScanArgs>,
|
||||||
|
) => Promise<StartLibraryScanResponse>;
|
||||||
updateInternetRadioStation: (
|
updateInternetRadioStation: (
|
||||||
args: ReplaceApiClientProps<UpdateInternetRadioStationArgs>,
|
args: ReplaceApiClientProps<UpdateInternetRadioStationArgs>,
|
||||||
) => Promise<UpdateInternetRadioStationResponse>;
|
) => Promise<UpdateInternetRadioStationResponse>;
|
||||||
|
|||||||
Reference in New Issue
Block a user