Show node scan status and fast-path shelf updates during library sync.

This commit is contained in:
Jos Vooges | STH 2026-08-26 03:00:47 +02:00
parent 9168a159f7
commit 76c989ba3c
7 changed files with 65 additions and 6 deletions

View file

@ -27,6 +27,7 @@ interface Node {
libraryFileCount: number;
fullScanInterval: string;
fullScanAt: string;
scanStatus?: string;
moviesPaths: string[];
seriesPaths: string[];
scanRoots: ScanRootRow[];
@ -399,6 +400,7 @@ export default function NodesPage() {
<th>Naam</th>
<th>Locatie</th>
<th>Status</th>
<th>Scan</th>
<th>Paden</th>
<th>Bestanden</th>
<th>Streams</th>
@ -417,6 +419,15 @@ export default function NodesPage() {
<td>
<span className={`status ${node.status.toLowerCase()}`}>{node.status}</span>
</td>
<td style={{ fontSize: "0.8rem" }}>
{node.scanStatus === "scanning" ? (
<span style={{ color: "#f59e0b" }}>bezig…</span>
) : node.scanStatus === "idle" ? (
<span style={{ color: "#71717a" }}>idle</span>
) : (
<span style={{ color: "#71717a" }}>{node.scanStatus ?? "-"}</span>
)}
</td>
<td style={{ fontSize: "0.75rem", color: "#a1a1aa" }}>
{node.scanRoots?.length
? `${node.scanRoots.length} roots`

View file

@ -277,6 +277,32 @@ export class MetadataService {
nodeId: string,
file: MediaFileInfo
): Promise<string | null> {
// Fast path: already matched → only refresh shelf / availability (no TMDB).
const existing = await prisma.mediaFile.findUnique({
where: {
nodeId_localFileId: { nodeId, localFileId: file.localFileId },
},
select: { id: true, movieId: true, episodeId: true, shelfId: true },
});
if (existing && (existing.movieId || existing.episodeId)) {
const nextShelf = file.shelfId || null;
await prisma.mediaFile.update({
where: { id: existing.id },
data: {
available: true,
shelfId: nextShelf,
sizeBytes: BigInt(file.sizeBytes),
container: file.container,
resolution: file.resolution,
videoCodec: file.videoCodec,
audioCodec: file.audioCodec,
releaseName: file.releaseName,
lastSeen: new Date(file.modifiedAt),
},
});
return existing.movieId ?? existing.episodeId;
}
if (file.mediaType === "movie" && file.parsedMovie) {
return this.matchMovie(nodeId, file);
}

View file

@ -121,6 +121,8 @@ export async function registerNodeRoutes(app: FastifyInstance) {
app.get("/api/v1/admin/nodes", { preHandler: requireAdmin }, async () => {
const { ensureDefaultShelves } = await import("../library/shelves");
await ensureDefaultShelves();
const { nodeConnectionManager } = await import("../websocket/manager");
const scanStatuses = nodeConnectionManager.getAllScanStatuses();
const nodes = await prisma.node.findMany({
include: {
location: true,
@ -130,7 +132,10 @@ export async function registerNodeRoutes(app: FastifyInstance) {
});
return {
nodes: nodes.map((n) => serializeNode(n)),
nodes: nodes.map((n) => ({
...serializeNode(n),
scanStatus: scanStatuses[n.id] ?? (n.status === "ONLINE" ? "idle" : "offline"),
})),
};
});

View file

@ -20,6 +20,7 @@ interface NodeConnection {
nodeId: string;
socket: WebSocket;
lastHeartbeat: Date;
scanStatus: string;
}
interface PendingAck {
@ -162,6 +163,7 @@ class NodeConnectionManager {
nodeId: payload.nodeId,
socket,
lastHeartbeat: new Date(),
scanStatus: "idle",
});
onAuth(payload.nodeId);
@ -276,7 +278,10 @@ class NodeConnectionManager {
private async handleHeartbeat(payload: HeartbeatPayload): Promise<void> {
const conn = this.connections.get(payload.nodeId);
if (conn) conn.lastHeartbeat = new Date();
if (conn) {
conn.lastHeartbeat = new Date();
conn.scanStatus = payload.scanStatus || "idle";
}
await prisma.node.update({
where: { id: payload.nodeId },
@ -422,6 +427,18 @@ class NodeConnectionManager {
return this.sendToNode(nodeId, "RESCAN", { full });
}
getScanStatus(nodeId: string): string {
return this.connections.get(nodeId)?.scanStatus ?? "offline";
}
getAllScanStatuses(): Record<string, string> {
const out: Record<string, string> = {};
for (const [id, conn] of this.connections) {
out[id] = conn.scanStatus || "idle";
}
return out;
}
sendConfigUpdate(
nodeId: string,
payload: {

View file

@ -7,8 +7,8 @@ WORKDIR /src
RUN apk add --no-cache git ca-certificates
COPY node/media-node/ ./
RUN go mod tidy \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=1.2.4" -o /media-node-linux-amd64 ./cmd/media-node \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=1.2.4" -o /media-node-linux-arm64 ./cmd/media-node
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=1.2.5" -o /media-node-linux-amd64 ./cmd/media-node \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=1.2.5" -o /media-node-linux-arm64 ./cmd/media-node
FROM base AS builder
COPY package.json pnpm-workspace.yaml ./

View file

@ -24,7 +24,7 @@ import (
"github.com/sthmedia/media-node/internal/streaming"
)
var version = "1.2.4"
var version = "1.2.5"
func main() {
if len(os.Args) < 2 {

View file

@ -124,7 +124,7 @@ func (s *Store) GetFileByPath(path string) (*LocalFile, error) {
func (s *Store) AllFiles() ([]LocalFile, error) {
rows, err := s.db.Query(`
SELECT local_file_id, path, size_bytes, modified_at, media_type, release_name, revision
FROM local_files ORDER BY release_name
FROM local_files ORDER BY path
`)
if err != nil {
return nil, err