diff --git a/apps/master-api/src/admin/routes.ts b/apps/master-api/src/admin/routes.ts index f7ae348..bf065ed 100644 --- a/apps/master-api/src/admin/routes.ts +++ b/apps/master-api/src/admin/routes.ts @@ -164,8 +164,25 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config) }); app.get("/api/v1/admin/streams", { preHandler: requireAdmin }, async () => { + const now = new Date(); + // Drop expired rows so prefetch leftovers do not clutter the UI. + await prisma.playbackSession.updateMany({ + where: { + status: "ACTIVE", + revoked: false, + absoluteExpiresAt: { lt: now }, + }, + data: { status: "EXPIRED" }, + }); + const sessions = await prisma.playbackSession.findMany({ - where: { status: "ACTIVE", revoked: false }, + where: { + status: "ACTIVE", + revoked: false, + absoluteExpiresAt: { gt: now }, + // Only show recently touched sessions (prefetch often never streams bytes). + lastActivity: { gt: new Date(now.getTime() - 15 * 60 * 1000) }, + }, include: { node: { select: { id: true, name: true } }, mediaFile: { diff --git a/apps/master-api/src/stremio/routes.ts b/apps/master-api/src/stremio/routes.ts index 25c8f46..2a432d0 100644 --- a/apps/master-api/src/stremio/routes.ts +++ b/apps/master-api/src/stremio/routes.ts @@ -131,7 +131,8 @@ export async function registerStremioRoutes(app: FastifyInstance, config: Config return { streams }; }); - // Lazy session: create on play so the token is fresh; redirect to node stream URL. + // Lazy session: create on GET play so the token is fresh; redirect to node stream URL. + // HEAD is treated as a probe — do not create a session (Stremio binge/prefetch uses HEAD/GET). app.route({ method: ["GET", "HEAD"], url: "/stremio/:token/play/:mediaFileId", @@ -142,9 +143,23 @@ export async function registerStremioRoutes(app: FastifyInstance, config: Config return reply.status(401).send({ error: "Invalid addon token" }); } + if (request.method === "HEAD") { + const mediaFile = await prisma.mediaFile.findUnique({ + where: { id: mediaFileId }, + include: { node: true }, + }); + if ( + !mediaFile?.available || + mediaFile.node.revoked || + mediaFile.node.status !== "ONLINE" + ) { + return reply.status(404).send(); + } + return reply.status(200).send(); + } + try { const session = await playback.createSession(mediaFileId, userId); - // 307 keeps method; Stremio/external players follow to the node. return reply.redirect(307, session.streamUrl); } catch (err) { const message = err instanceof Error ? err.message : "Playback unavailable"; @@ -362,7 +377,6 @@ async function getStreams( url: playback.resolveStreamUrl(addonToken, file.id), behaviorHints: { notWebReady: true, - bingeGroup: `mc-movie-${movie.id}`, }, }); } @@ -409,7 +423,6 @@ async function getStreams( url: playback.resolveStreamUrl(addonToken, file.id), behaviorHints: { notWebReady: true, - bingeGroup: `mc-series-${series.id}-S${seasonNumber}`, }, }); }