Show node scan status and fast-path shelf updates during library sync.
This commit is contained in:
parent
9168a159f7
commit
76c989ba3c
7 changed files with 65 additions and 6 deletions
|
|
@ -27,6 +27,7 @@ interface Node {
|
||||||
libraryFileCount: number;
|
libraryFileCount: number;
|
||||||
fullScanInterval: string;
|
fullScanInterval: string;
|
||||||
fullScanAt: string;
|
fullScanAt: string;
|
||||||
|
scanStatus?: string;
|
||||||
moviesPaths: string[];
|
moviesPaths: string[];
|
||||||
seriesPaths: string[];
|
seriesPaths: string[];
|
||||||
scanRoots: ScanRootRow[];
|
scanRoots: ScanRootRow[];
|
||||||
|
|
@ -399,6 +400,7 @@ export default function NodesPage() {
|
||||||
<th>Naam</th>
|
<th>Naam</th>
|
||||||
<th>Locatie</th>
|
<th>Locatie</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Scan</th>
|
||||||
<th>Paden</th>
|
<th>Paden</th>
|
||||||
<th>Bestanden</th>
|
<th>Bestanden</th>
|
||||||
<th>Streams</th>
|
<th>Streams</th>
|
||||||
|
|
@ -417,6 +419,15 @@ export default function NodesPage() {
|
||||||
<td>
|
<td>
|
||||||
<span className={`status ${node.status.toLowerCase()}`}>{node.status}</span>
|
<span className={`status ${node.status.toLowerCase()}`}>{node.status}</span>
|
||||||
</td>
|
</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" }}>
|
<td style={{ fontSize: "0.75rem", color: "#a1a1aa" }}>
|
||||||
{node.scanRoots?.length
|
{node.scanRoots?.length
|
||||||
? `${node.scanRoots.length} roots`
|
? `${node.scanRoots.length} roots`
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,32 @@ export class MetadataService {
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
file: MediaFileInfo
|
file: MediaFileInfo
|
||||||
): Promise<string | null> {
|
): 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) {
|
if (file.mediaType === "movie" && file.parsedMovie) {
|
||||||
return this.matchMovie(nodeId, file);
|
return this.matchMovie(nodeId, file);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ export async function registerNodeRoutes(app: FastifyInstance) {
|
||||||
app.get("/api/v1/admin/nodes", { preHandler: requireAdmin }, async () => {
|
app.get("/api/v1/admin/nodes", { preHandler: requireAdmin }, async () => {
|
||||||
const { ensureDefaultShelves } = await import("../library/shelves");
|
const { ensureDefaultShelves } = await import("../library/shelves");
|
||||||
await ensureDefaultShelves();
|
await ensureDefaultShelves();
|
||||||
|
const { nodeConnectionManager } = await import("../websocket/manager");
|
||||||
|
const scanStatuses = nodeConnectionManager.getAllScanStatuses();
|
||||||
const nodes = await prisma.node.findMany({
|
const nodes = await prisma.node.findMany({
|
||||||
include: {
|
include: {
|
||||||
location: true,
|
location: true,
|
||||||
|
|
@ -130,7 +132,10 @@ export async function registerNodeRoutes(app: FastifyInstance) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: nodes.map((n) => serializeNode(n)),
|
nodes: nodes.map((n) => ({
|
||||||
|
...serializeNode(n),
|
||||||
|
scanStatus: scanStatuses[n.id] ?? (n.status === "ONLINE" ? "idle" : "offline"),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ interface NodeConnection {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
lastHeartbeat: Date;
|
lastHeartbeat: Date;
|
||||||
|
scanStatus: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PendingAck {
|
interface PendingAck {
|
||||||
|
|
@ -162,6 +163,7 @@ class NodeConnectionManager {
|
||||||
nodeId: payload.nodeId,
|
nodeId: payload.nodeId,
|
||||||
socket,
|
socket,
|
||||||
lastHeartbeat: new Date(),
|
lastHeartbeat: new Date(),
|
||||||
|
scanStatus: "idle",
|
||||||
});
|
});
|
||||||
onAuth(payload.nodeId);
|
onAuth(payload.nodeId);
|
||||||
|
|
||||||
|
|
@ -276,7 +278,10 @@ class NodeConnectionManager {
|
||||||
|
|
||||||
private async handleHeartbeat(payload: HeartbeatPayload): Promise<void> {
|
private async handleHeartbeat(payload: HeartbeatPayload): Promise<void> {
|
||||||
const conn = this.connections.get(payload.nodeId);
|
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({
|
await prisma.node.update({
|
||||||
where: { id: payload.nodeId },
|
where: { id: payload.nodeId },
|
||||||
|
|
@ -422,6 +427,18 @@ class NodeConnectionManager {
|
||||||
return this.sendToNode(nodeId, "RESCAN", { full });
|
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(
|
sendConfigUpdate(
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ WORKDIR /src
|
||||||
RUN apk add --no-cache git ca-certificates
|
RUN apk add --no-cache git ca-certificates
|
||||||
COPY node/media-node/ ./
|
COPY node/media-node/ ./
|
||||||
RUN go mod tidy \
|
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=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.4" -o /media-node-linux-arm64 ./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
|
FROM base AS builder
|
||||||
COPY package.json pnpm-workspace.yaml ./
|
COPY package.json pnpm-workspace.yaml ./
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"github.com/sthmedia/media-node/internal/streaming"
|
"github.com/sthmedia/media-node/internal/streaming"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.2.4"
|
var version = "1.2.5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ func (s *Store) GetFileByPath(path string) (*LocalFile, error) {
|
||||||
func (s *Store) AllFiles() ([]LocalFile, error) {
|
func (s *Store) AllFiles() ([]LocalFile, error) {
|
||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT local_file_id, path, size_bytes, modified_at, media_type, release_name, revision
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue