Eighth plugin out, and the second resurrection: like soulseek, hono.ts carried the router import and its mount commented out since 2026-08-13, with the permission reduced to api: [], so the server booted clean while every panel request 404'd. jellyfin_servers was also absent from the drizzle barrel, so bun db:push would have dropped it. Easier than soulseek in one specific way: mountPrefix derives /jellyfin from the appName, which is exactly what the router already hardcoded, so no frontend call site needed repointing. Soulseek needed 47 because its appName and its old prefix differed. The frontend also already routed through one helper, so there were five call sites rather than forty-seven. Self-contained, unlike transmission and soulseek: jellyfin_servers is a registry this plugin owns outright — label, URL, encrypted token, one active — because more than one Jellyfin is a thing someone has, and more than one Transmission is not. Carries its own .prettierignore for the vendored hls.mjs, rather than leaving a stale rule behind in the platform naming a path that no longer exists.
58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
import { Loader2 } from 'lucide-react';
|
|
import { MediaShelf } from './MediaCard';
|
|
import { useJellyfinHome } from './useJellyfinData';
|
|
|
|
// The landing screen: what you were watching, what comes next, and the newest thing in each library.
|
|
//
|
|
// One request. The sidecar fans out to Jellyfin's four different endpoints and swallows per-library failures,
|
|
// so a library mid-scan costs its own shelf rather than the whole page — which is why there is no per-shelf
|
|
// loading state here to write.
|
|
|
|
export const HomeSection = () => {
|
|
const { data, isLoading, error } = useJellyfinHome();
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center p-6 text-center text-xs text-muted-foreground">
|
|
The server did not answer.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const resume = data?.resume ?? [];
|
|
const nextUp = data?.nextUp ?? [];
|
|
const latest = data?.latest ?? [];
|
|
const empty = !resume.length && !nextUp.length && !latest.some((shelf) => shelf.items.length);
|
|
|
|
if (empty) {
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center gap-1 p-6 text-center">
|
|
<p className="text-sm font-medium">Nothing here yet</p>
|
|
<p className="max-w-sm text-xs text-muted-foreground">
|
|
The server is connected but its video libraries are empty — or still scanning.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="h-full overflow-y-auto">
|
|
<div className="flex flex-col gap-6 p-4">
|
|
<MediaShelf title="Continue watching" items={resume} wide />
|
|
<MediaShelf title="Next up" items={nextUp} wide />
|
|
{latest.map((shelf) => (
|
|
<MediaShelf key={shelf.viewId} title={`Latest in ${shelf.viewName}`} items={shelf.items} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|