fixes and improvements
This commit is contained in:
@@ -27,7 +27,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { AuthProvider, useAuth } from "react-oidc-context";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { Monitoring } from "./pages/Monitoring";
|
||||
import { Media } from "./pages/Media";
|
||||
import { Applications } from "./pages/Applications";
|
||||
import { Settings } from "./pages/Settings";
|
||||
import { UsersPage } from "./pages/Users";
|
||||
import { FileBrowser } from "./pages/FileBrowser";
|
||||
@@ -173,7 +173,12 @@ function Shell({
|
||||
component={NavLink}
|
||||
to="/monitoring"
|
||||
/>
|
||||
<Tab value="/media" label="Media" component={NavLink} to="/media" />
|
||||
<Tab
|
||||
value="/applications"
|
||||
label="Applications"
|
||||
component={NavLink}
|
||||
to="/applications"
|
||||
/>
|
||||
<Tab value="/users" label="Users" component={NavLink} to="/users" />
|
||||
<Tab value="/files" label="Files" component={NavLink} to="/files" />
|
||||
<Tab
|
||||
@@ -192,7 +197,8 @@ function Shell({
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/media" element={<Media />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
|
||||
+79
-24
@@ -25,6 +25,8 @@ import type {
|
||||
JobTemplate,
|
||||
JobResult,
|
||||
ResolvedPath,
|
||||
ResetLocalDatabaseInput,
|
||||
ResetLocalDatabaseResponse,
|
||||
} from "../types";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || "/api";
|
||||
@@ -126,12 +128,26 @@ async function del<T>(path: string): Promise<T> {
|
||||
}
|
||||
|
||||
// Dashboard
|
||||
export const fetchCounts = () => get<MediaCounts>("/api/dashboard/counts");
|
||||
export const fetchLibraries = () =>
|
||||
get<LibraryCount[]>("/api/dashboard/libraries");
|
||||
export const fetchActivity = () =>
|
||||
get<NowPlayingSession[]>("/api/dashboard/activity");
|
||||
export const fetchUsers = () => get<UserDirectoryResponse>("/api/users");
|
||||
export const fetchCounts = (machineId?: string) =>
|
||||
get<MediaCounts>(
|
||||
"/api/dashboard/counts",
|
||||
machineId ? { machine_id: machineId } : undefined,
|
||||
);
|
||||
export const fetchLibraries = (machineId?: string) =>
|
||||
get<LibraryCount[]>(
|
||||
"/api/dashboard/libraries",
|
||||
machineId ? { machine_id: machineId } : undefined,
|
||||
);
|
||||
export const fetchActivity = (machineId?: string) =>
|
||||
get<NowPlayingSession[]>(
|
||||
"/api/dashboard/activity",
|
||||
machineId ? { machine_id: machineId } : undefined,
|
||||
);
|
||||
export const fetchUsers = (machineId?: string) =>
|
||||
get<UserDirectoryResponse>(
|
||||
"/api/users",
|
||||
machineId ? { machine_id: machineId } : undefined,
|
||||
);
|
||||
|
||||
// Backward-compatible alias used by older hooks/components.
|
||||
export const fetchNowPlaying = fetchActivity;
|
||||
@@ -211,16 +227,36 @@ export const deleteMonitoringMachine = (machineId: string) =>
|
||||
del<{ status: string }>(
|
||||
`/api/settings/machines/${encodeURIComponent(machineId)}`,
|
||||
);
|
||||
export const resetLocalDatabase = (payload: ResetLocalDatabaseInput) =>
|
||||
post<ResetLocalDatabaseResponse>(
|
||||
"/api/settings/reset-local-database",
|
||||
payload,
|
||||
);
|
||||
|
||||
// Media
|
||||
export const fetchMediaStatus = () =>
|
||||
get<MediaIndexStatus>("/api/media/status");
|
||||
export const buildMediaIndex = () =>
|
||||
post<MediaIndexActionResponse>("/api/media/build");
|
||||
export const stopMediaIndexBuild = () =>
|
||||
post<MediaIndexActionResponse>("/api/media/stop");
|
||||
export const forceStopMediaIndexBuild = () =>
|
||||
post<MediaIndexActionResponse>("/api/media/force-stop");
|
||||
export const fetchMediaStatus = (machineId?: string) =>
|
||||
get<MediaIndexStatus>(
|
||||
"/api/media/status",
|
||||
machineId ? { machine_id: machineId } : undefined,
|
||||
);
|
||||
export const buildMediaIndex = (machineId?: string) =>
|
||||
post<MediaIndexActionResponse>(
|
||||
machineId
|
||||
? `/api/media/build?machine_id=${encodeURIComponent(machineId)}`
|
||||
: "/api/media/build",
|
||||
);
|
||||
export const stopMediaIndexBuild = (machineId?: string) =>
|
||||
post<MediaIndexActionResponse>(
|
||||
machineId
|
||||
? `/api/media/stop?machine_id=${encodeURIComponent(machineId)}`
|
||||
: "/api/media/stop",
|
||||
);
|
||||
export const forceStopMediaIndexBuild = (machineId?: string) =>
|
||||
post<MediaIndexActionResponse>(
|
||||
machineId
|
||||
? `/api/media/force-stop?machine_id=${encodeURIComponent(machineId)}`
|
||||
: "/api/media/force-stop",
|
||||
);
|
||||
export const queryMedia = (params: {
|
||||
libraries?: string;
|
||||
types?: string;
|
||||
@@ -230,6 +266,7 @@ export const queryMedia = (params: {
|
||||
sort_order?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
machineId?: string;
|
||||
}) =>
|
||||
get<MediaQueryResponse>("/api/media/query", {
|
||||
libraries: params.libraries || "",
|
||||
@@ -240,23 +277,41 @@ export const queryMedia = (params: {
|
||||
sort_order: params.sort_order || "Ascending",
|
||||
limit: String(params.limit || 100),
|
||||
offset: String(params.offset || 0),
|
||||
...(params.machineId ? { machine_id: params.machineId } : {}),
|
||||
});
|
||||
|
||||
// Files
|
||||
export const fetchDirectoryListing = (path: string) =>
|
||||
get<DirectoryListing>("/api/files/list", { path });
|
||||
export const fetchFfprobe = (path: string) =>
|
||||
get<Record<string, unknown>>("/api/files/ffprobe", { path });
|
||||
export const fetchStat = (path: string) =>
|
||||
get<{ path: string; output: string }>("/api/files/stat", { path });
|
||||
export const resolvePath = (path: string) =>
|
||||
get<ResolvedPath>("/api/files/resolve-path", { path });
|
||||
export const fetchDirectoryListing = (path: string, machineId?: string) =>
|
||||
get<DirectoryListing>("/api/files/list", {
|
||||
path,
|
||||
...(machineId ? { machine_id: machineId } : {}),
|
||||
});
|
||||
export const fetchFfprobe = (path: string, machineId?: string) =>
|
||||
get<Record<string, unknown>>("/api/files/ffprobe", {
|
||||
path,
|
||||
...(machineId ? { machine_id: machineId } : {}),
|
||||
});
|
||||
export const fetchStat = (path: string, machineId?: string) =>
|
||||
get<{ path: string; output: string }>("/api/files/stat", {
|
||||
path,
|
||||
...(machineId ? { machine_id: machineId } : {}),
|
||||
});
|
||||
export const resolvePath = (path: string, machineId?: string) =>
|
||||
get<ResolvedPath>("/api/files/resolve-path", {
|
||||
path,
|
||||
...(machineId ? { machine_id: machineId } : {}),
|
||||
});
|
||||
|
||||
// Jobs
|
||||
export const fetchJobTemplates = () =>
|
||||
get<JobTemplate[]>("/api/jobs/templates");
|
||||
export const runJob = (jobKey: string, path: string) =>
|
||||
post<JobResult>("/api/jobs/run", { job_key: jobKey, path });
|
||||
export const runJob = (jobKey: string, path: string, machineId?: string) =>
|
||||
post<JobResult>(
|
||||
machineId
|
||||
? `/api/jobs/run?machine_id=${encodeURIComponent(machineId)}`
|
||||
: "/api/jobs/run",
|
||||
{ job_key: jobKey, path },
|
||||
);
|
||||
|
||||
export const fetchUserMessageQueueStatus = () =>
|
||||
get<UserMessageQueueStatus>("/api/users/message/status");
|
||||
|
||||
@@ -6,26 +6,26 @@ import {
|
||||
fetchMonitoringOverview,
|
||||
} from "../api/client";
|
||||
|
||||
export function useCounts() {
|
||||
export function useCounts(machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard", "counts"],
|
||||
queryFn: fetchCounts,
|
||||
queryKey: ["dashboard", "counts", machineId ?? "default"],
|
||||
queryFn: () => fetchCounts(machineId),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLibraries() {
|
||||
export function useLibraries(machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard", "libraries"],
|
||||
queryFn: fetchLibraries,
|
||||
queryKey: ["dashboard", "libraries", machineId ?? "default"],
|
||||
queryFn: () => fetchLibraries(machineId),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useActivity() {
|
||||
export function useActivity(machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard", "activity"],
|
||||
queryFn: fetchActivity,
|
||||
queryKey: ["dashboard", "activity", machineId ?? "default"],
|
||||
queryFn: () => fetchActivity(machineId),
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,28 +7,28 @@ import {
|
||||
runJob,
|
||||
} from "../api/client";
|
||||
|
||||
export function useDirectoryListing(path: string) {
|
||||
export function useDirectoryListing(path: string, machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["files", "list", path],
|
||||
queryFn: () => fetchDirectoryListing(path),
|
||||
queryKey: ["files", "list", path, machineId ?? "default"],
|
||||
queryFn: () => fetchDirectoryListing(path, machineId),
|
||||
enabled: !!path,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useFfprobe(path: string, enabled = false) {
|
||||
export function useFfprobe(path: string, enabled = false, machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["files", "ffprobe", path],
|
||||
queryFn: () => fetchFfprobe(path),
|
||||
queryKey: ["files", "ffprobe", path, machineId ?? "default"],
|
||||
queryFn: () => fetchFfprobe(path, machineId),
|
||||
enabled: enabled && !!path,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useStat(path: string, enabled = false) {
|
||||
export function useStat(path: string, enabled = false, machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["files", "stat", path],
|
||||
queryFn: () => fetchStat(path),
|
||||
queryKey: ["files", "stat", path, machineId ?? "default"],
|
||||
queryFn: () => fetchStat(path, machineId),
|
||||
enabled: enabled && !!path,
|
||||
});
|
||||
}
|
||||
@@ -41,9 +41,9 @@ export function useJobTemplates() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useRunJob() {
|
||||
export function useRunJob(machineId?: string) {
|
||||
return useMutation({
|
||||
mutationFn: ({ jobKey, path }: { jobKey: string; path: string }) =>
|
||||
runJob(jobKey, path),
|
||||
runJob(jobKey, path, machineId),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
forceStopMediaIndexBuild,
|
||||
} from "../api/client";
|
||||
|
||||
export function useMediaStatus() {
|
||||
export function useMediaStatus(machineId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["media", "status"],
|
||||
queryFn: fetchMediaStatus,
|
||||
queryKey: ["media", "status", machineId ?? "default"],
|
||||
queryFn: () => fetchMediaStatus(machineId),
|
||||
staleTime: 5_000,
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.build_running ? 1000 : false,
|
||||
@@ -27,11 +27,11 @@ export function useMediaQuery(params: {
|
||||
sort_order?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
machineId?: string;
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
const { enabled = true, ...queryParams } = params;
|
||||
|
||||
// Feature: Sync file browser with selected media path
|
||||
return useQuery({
|
||||
queryKey: ["media", "query", queryParams],
|
||||
queryFn: () => queryMedia(queryParams),
|
||||
@@ -44,30 +44,30 @@ function invalidateMedia(queryClient: ReturnType<typeof useQueryClient>) {
|
||||
queryClient.invalidateQueries({ queryKey: ["media"] });
|
||||
}
|
||||
|
||||
export function useBuildIndex() {
|
||||
export function useBuildIndex(machineId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: buildMediaIndex,
|
||||
mutationFn: () => buildMediaIndex(machineId),
|
||||
onSuccess: () => {
|
||||
invalidateMedia(queryClient);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useStopBuildIndex() {
|
||||
export function useStopBuildIndex(machineId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: stopMediaIndexBuild,
|
||||
mutationFn: () => stopMediaIndexBuild(machineId),
|
||||
onSuccess: () => {
|
||||
invalidateMedia(queryClient);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useForceStopBuildIndex() {
|
||||
export function useForceStopBuildIndex(machineId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: forceStopMediaIndexBuild,
|
||||
mutationFn: () => forceStopMediaIndexBuild(machineId),
|
||||
onSuccess: () => {
|
||||
invalidateMedia(queryClient);
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchUsers } from "../api/client";
|
||||
import type { UserDirectoryResponse } from "../types";
|
||||
|
||||
export function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: fetchUsers,
|
||||
export function useUsers(machineId?: string) {
|
||||
return useQuery<UserDirectoryResponse>({
|
||||
queryKey: ["users", machineId ?? "default"],
|
||||
queryFn: () => fetchUsers(machineId),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
import type { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
useRunJob,
|
||||
} from "../hooks/useFiles";
|
||||
import { usePersistentState } from "../hooks/usePersistentState";
|
||||
import { useMonitoringSettings } from "../hooks/useSettings";
|
||||
|
||||
interface DisplayRow {
|
||||
id: string;
|
||||
@@ -556,9 +558,22 @@ function FfprobeDetails({ path, data }: { path: string; data: FfprobeData }) {
|
||||
}
|
||||
|
||||
export function FileBrowser() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const { data: machines } = useMonitoringSettings();
|
||||
const fileMachines = useMemo(
|
||||
() =>
|
||||
(machines ?? []).filter(
|
||||
(machine) =>
|
||||
machine.enabled &&
|
||||
(machine.services.includes("files") ||
|
||||
machine.services.includes("monitoring")),
|
||||
),
|
||||
[machines],
|
||||
);
|
||||
const initialRequestedPath = searchParams.get("path");
|
||||
const initialMachineId =
|
||||
searchParams.get("machine_id") || fileMachines[0]?.id || "";
|
||||
const [browserState, setBrowserState] = usePersistentState<FileBrowserState>(
|
||||
FILE_BROWSER_STATE_KEY,
|
||||
() => {
|
||||
@@ -580,6 +595,7 @@ export function FileBrowser() {
|
||||
},
|
||||
);
|
||||
const { currentDir, pathInput, selectedPath, selectedJob } = browserState;
|
||||
const selectedMachineId = searchParams.get("machine_id") || initialMachineId;
|
||||
const updateBrowserState = (patch: Partial<FileBrowserState>) =>
|
||||
setBrowserState((current) => ({ ...current, ...patch }));
|
||||
|
||||
@@ -588,7 +604,7 @@ export function FileBrowser() {
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useDirectoryListing(currentDir);
|
||||
} = useDirectoryListing(currentDir, selectedMachineId || undefined);
|
||||
const {
|
||||
data: ffprobeData,
|
||||
isLoading: ffprobeLoading,
|
||||
@@ -596,9 +612,10 @@ export function FileBrowser() {
|
||||
} = useFfprobe(
|
||||
selectedPath ?? "",
|
||||
!!selectedPath && isVideoFile(selectedPath),
|
||||
selectedMachineId || undefined,
|
||||
);
|
||||
const { data: templates } = useJobTemplates();
|
||||
const runJob = useRunJob();
|
||||
const runJob = useRunJob(selectedMachineId || undefined);
|
||||
|
||||
const navigate = (path: string) => {
|
||||
updateBrowserState({
|
||||
@@ -608,6 +625,18 @@ export function FileBrowser() {
|
||||
});
|
||||
};
|
||||
|
||||
const setMachine = (machineId: string) => {
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
if (machineId) next.set("machine_id", machineId);
|
||||
else next.delete("machine_id");
|
||||
return next;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
|
||||
const handlePathSubmit = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") navigate(pathInput || "/");
|
||||
};
|
||||
@@ -657,7 +686,27 @@ export function FileBrowser() {
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h5">File Browser</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{ alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography variant="h5">File Browser</Typography>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel>Machine</InputLabel>
|
||||
<Select
|
||||
label="Machine"
|
||||
value={selectedMachineId}
|
||||
onChange={(e) => setMachine(String(e.target.value))}
|
||||
>
|
||||
{fileMachines.map((machine) => (
|
||||
<MenuItem key={machine.id} value={machine.id}>
|
||||
{machine.name} · {machine.mode}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
<Stack direction={isMobile ? "column" : "row"} spacing={1}>
|
||||
<TextField
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
import type { GridColDef } from "@mui/x-data-grid";
|
||||
import {
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
} from "../hooks/useMedia";
|
||||
import { usePersistentState } from "../hooks/usePersistentState";
|
||||
import type { MediaItem } from "../types";
|
||||
import { useMonitoringSettings } from "../hooks/useSettings";
|
||||
import { useCounts, useLibraries } from "../hooks/useDashboard";
|
||||
|
||||
function formatDuration(seconds: number | null | undefined): string {
|
||||
if (seconds == null || Number.isNaN(seconds)) return "-";
|
||||
@@ -64,11 +66,26 @@ function defaultMediaTabState(): MediaTabState {
|
||||
|
||||
export function Media() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const { data: status } = useMediaStatus();
|
||||
const buildIndex = useBuildIndex();
|
||||
const stopBuildIndex = useStopBuildIndex();
|
||||
const forceStopBuildIndex = useForceStopBuildIndex();
|
||||
const { data: machines } = useMonitoringSettings();
|
||||
const jellyfinMachines = useMemo(
|
||||
() =>
|
||||
(machines ?? []).filter(
|
||||
(machine) => machine.enabled && machine.services.includes("jellyfin"),
|
||||
),
|
||||
[machines],
|
||||
);
|
||||
const selectedMachineId =
|
||||
searchParams.get("machine_id") || jellyfinMachines[0]?.id || "";
|
||||
const { data: counts } = useCounts(selectedMachineId || undefined);
|
||||
const { data: libraries } = useLibraries(selectedMachineId || undefined);
|
||||
const { data: status } = useMediaStatus(selectedMachineId || undefined);
|
||||
const buildIndex = useBuildIndex(selectedMachineId || undefined);
|
||||
const stopBuildIndex = useStopBuildIndex(selectedMachineId || undefined);
|
||||
const forceStopBuildIndex = useForceStopBuildIndex(
|
||||
selectedMachineId || undefined,
|
||||
);
|
||||
|
||||
const [mediaState, setMediaState] = usePersistentState<MediaTabState>(
|
||||
MEDIA_TAB_STATE_KEY,
|
||||
@@ -79,6 +96,19 @@ export function Media() {
|
||||
setMediaState((current) => ({ ...current, ...patch }));
|
||||
const limit = 100;
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams.get("machine_id") && selectedMachineId) {
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
next.set("machine_id", selectedMachineId);
|
||||
return next;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
}
|
||||
}, [searchParams, selectedMachineId, setSearchParams]);
|
||||
|
||||
const { data: queryResult, isLoading } = useMediaDataQuery({
|
||||
types,
|
||||
search,
|
||||
@@ -87,6 +117,7 @@ export function Media() {
|
||||
sort_order: sortOrder,
|
||||
limit,
|
||||
offset,
|
||||
machineId: selectedMachineId || undefined,
|
||||
enabled: status?.exists ?? false,
|
||||
});
|
||||
|
||||
@@ -155,7 +186,30 @@ export function Media() {
|
||||
spacing={1.5}
|
||||
sx={{ alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography variant="h5">Media</Typography>
|
||||
<Typography variant="h5">Jellyfin</Typography>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel>Machine</InputLabel>
|
||||
<Select
|
||||
label="Machine"
|
||||
value={selectedMachineId}
|
||||
onChange={(e) =>
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
next.set("machine_id", String(e.target.value));
|
||||
return next;
|
||||
},
|
||||
{ replace: true },
|
||||
)
|
||||
}
|
||||
>
|
||||
{jellyfinMachines.map((machine) => (
|
||||
<MenuItem key={machine.id} value={machine.id}>
|
||||
{machine.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{status?.exists ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Index: {status.item_count.toLocaleString()} items
|
||||
@@ -168,6 +222,14 @@ export function Media() {
|
||||
No index built yet.
|
||||
</Alert>
|
||||
)}
|
||||
{counts && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Library stats: {counts.movies.toLocaleString()} movies ·{" "}
|
||||
{counts.series.toLocaleString()} series ·{" "}
|
||||
{counts.episodes.toLocaleString()} episodes ·{" "}
|
||||
{(libraries?.length ?? 0).toLocaleString()} libraries
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => buildIndex.mutate()}
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface MonitoringMachine {
|
||||
name: string;
|
||||
mode: "local" | "ssh";
|
||||
enabled: boolean;
|
||||
services: string[];
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
@@ -102,6 +103,11 @@ export interface MonitoringMachine {
|
||||
password_set: boolean;
|
||||
media_root: string;
|
||||
path_prefix: string;
|
||||
jellyfin_url: string;
|
||||
jellyfin_user_id: string;
|
||||
jellyfin_api_key_set: boolean;
|
||||
jellyseerr_url: string;
|
||||
jellyseerr_api_key_set: boolean;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -110,6 +116,7 @@ export interface MonitoringMachineInput {
|
||||
name: string;
|
||||
mode: "local" | "ssh";
|
||||
enabled: boolean;
|
||||
services: string[];
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
@@ -118,6 +125,11 @@ export interface MonitoringMachineInput {
|
||||
password: string;
|
||||
media_root: string;
|
||||
path_prefix: string;
|
||||
jellyfin_url: string;
|
||||
jellyfin_user_id: string;
|
||||
jellyfin_api_key: string;
|
||||
jellyseerr_url: string;
|
||||
jellyseerr_api_key: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -143,6 +155,21 @@ export interface MetricSummary {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ResetLocalDatabaseInput {
|
||||
confirm_phrase: string;
|
||||
acknowledge_settings_loss: boolean;
|
||||
acknowledge_media_index_loss: boolean;
|
||||
acknowledge_irreversible: boolean;
|
||||
}
|
||||
|
||||
export interface ResetLocalDatabaseResponse {
|
||||
status: string;
|
||||
settings_db_removed: boolean;
|
||||
media_index_removed: boolean;
|
||||
settings_files: string[];
|
||||
media_index_files: string[];
|
||||
}
|
||||
|
||||
export interface MonitoringPollerStatus {
|
||||
worker_running: boolean;
|
||||
stop_requested: boolean;
|
||||
|
||||
Reference in New Issue
Block a user