diff --git a/apps/admin-ui/src/app/nodes/page.tsx b/apps/admin-ui/src/app/nodes/page.tsx index 9bc2000..8f7eecd 100644 --- a/apps/admin-ui/src/app/nodes/page.tsx +++ b/apps/admin-ui/src/app/nodes/page.tsx @@ -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() { Naam Locatie Status + Scan Paden Bestanden Streams @@ -417,6 +419,15 @@ export default function NodesPage() { {node.status} + + {node.scanStatus === "scanning" ? ( + bezig… + ) : node.scanStatus === "idle" ? ( + idle + ) : ( + {node.scanStatus ?? "-"} + )} + {node.scanRoots?.length ? `${node.scanRoots.length} roots` diff --git a/apps/master-api/src/metadata/service.ts b/apps/master-api/src/metadata/service.ts index 7a2a5ff..f9d23e4 100644 --- a/apps/master-api/src/metadata/service.ts +++ b/apps/master-api/src/metadata/service.ts @@ -277,6 +277,32 @@ export class MetadataService { nodeId: string, file: MediaFileInfo ): Promise { + // 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); } diff --git a/apps/master-api/src/nodes/routes.ts b/apps/master-api/src/nodes/routes.ts index d4d5e3c..0d845f4 100644 --- a/apps/master-api/src/nodes/routes.ts +++ b/apps/master-api/src/nodes/routes.ts @@ -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"), + })), }; }); diff --git a/apps/master-api/src/websocket/manager.ts b/apps/master-api/src/websocket/manager.ts index ef1b6fe..577842c 100644 --- a/apps/master-api/src/websocket/manager.ts +++ b/apps/master-api/src/websocket/manager.ts @@ -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 { 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 { + const out: Record = {}; + for (const [id, conn] of this.connections) { + out[id] = conn.scanStatus || "idle"; + } + return out; + } + sendConfigUpdate( nodeId: string, payload: { diff --git a/deploy/docker/Dockerfile.master-api b/deploy/docker/Dockerfile.master-api index 98a00c3..b03b840 100644 --- a/deploy/docker/Dockerfile.master-api +++ b/deploy/docker/Dockerfile.master-api @@ -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 ./ diff --git a/node/media-node/cmd/media-node/main.go b/node/media-node/cmd/media-node/main.go index 54409ea..85e85e0 100644 --- a/node/media-node/cmd/media-node/main.go +++ b/node/media-node/cmd/media-node/main.go @@ -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 { diff --git a/node/media-node/internal/database/store.go b/node/media-node/internal/database/store.go index 60b95e9..9f588bf 100644 --- a/node/media-node/internal/database/store.go +++ b/node/media-node/internal/database/store.go @@ -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