Stop Stremio binge prefetch creating phantom next-episode streams.

Remove bingeGroup, skip session creation on HEAD probes, and hide stale sessions in admin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-08-25 02:35:29 +02:00
parent b1d008e8aa
commit 0608020556
2 changed files with 35 additions and 5 deletions

View file

@ -164,8 +164,25 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
}); });
app.get("/api/v1/admin/streams", { preHandler: requireAdmin }, async () => { 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({ 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: { include: {
node: { select: { id: true, name: true } }, node: { select: { id: true, name: true } },
mediaFile: { mediaFile: {

View file

@ -131,7 +131,8 @@ export async function registerStremioRoutes(app: FastifyInstance, config: Config
return { streams }; 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({ app.route({
method: ["GET", "HEAD"], method: ["GET", "HEAD"],
url: "/stremio/:token/play/:mediaFileId", 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" }); 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 { try {
const session = await playback.createSession(mediaFileId, userId); const session = await playback.createSession(mediaFileId, userId);
// 307 keeps method; Stremio/external players follow to the node.
return reply.redirect(307, session.streamUrl); return reply.redirect(307, session.streamUrl);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : "Playback unavailable"; const message = err instanceof Error ? err.message : "Playback unavailable";
@ -362,7 +377,6 @@ async function getStreams(
url: playback.resolveStreamUrl(addonToken, file.id), url: playback.resolveStreamUrl(addonToken, file.id),
behaviorHints: { behaviorHints: {
notWebReady: true, notWebReady: true,
bingeGroup: `mc-movie-${movie.id}`,
}, },
}); });
} }
@ -409,7 +423,6 @@ async function getStreams(
url: playback.resolveStreamUrl(addonToken, file.id), url: playback.resolveStreamUrl(addonToken, file.id),
behaviorHints: { behaviorHints: {
notWebReady: true, notWebReady: true,
bingeGroup: `mc-series-${series.id}-S${seasonNumber}`,
}, },
}); });
} }