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
|