stremio/apps/master-api/src/downloads/sidecars.ts
Jos Vooges | STH 9de3ee62d7 Serve local sidecar subtitles via existing client search/fetch and Stremio.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 03:37:08 +02:00

137 lines
4.1 KiB
TypeScript

import { randomUUID } from "crypto";
import { prisma } from "../database/client";
import { nodeConnectionManager } from "../websocket/manager";
import type { SubtitleHit } from "../opensubtitles/client";
type LocalSubCacheEntry = {
contentBase64: string;
fileName: string;
format: string;
language: string;
mediaFileId: string;
expiresAt: number;
};
const localSubCache = new Map<number, LocalSubCacheEntry>();
const CACHE_TTL_MS = 30 * 60_000;
export function stableLocalFileId(key: string): number {
let h = 2166136261;
for (let i = 0; i < key.length; i++) {
h ^= key.charCodeAt(i);
h = Math.imul(h, 16777619);
}
const n = Math.abs(h) % 2_000_000_000;
return n === 0 ? 1 : n;
}
function putLocalCache(fileId: number, entry: Omit<LocalSubCacheEntry, "expiresAt">) {
localSubCache.set(fileId, { ...entry, expiresAt: Date.now() + CACHE_TTL_MS });
}
export function getLocalSubtitleCache(fileId: number): LocalSubCacheEntry | null {
const hit = localSubCache.get(fileId);
if (!hit) return null;
if (Date.now() > hit.expiresAt) {
localSubCache.delete(fileId);
return null;
}
return hit;
}
/** Ask node for sidecar .srt/.ass next to the video; cache + upsert library_subtitles. */
export async function discoverSidecarSubtitles(mediaFileId: string): Promise<SubtitleHit[]> {
const file = await prisma.mediaFile.findUnique({
where: { id: mediaFileId },
include: {
movie: { select: { id: true, tmdbId: true, imdbId: true } },
episode: {
select: {
imdbId: true,
series: { select: { imdbId: true, tmdbId: true } },
},
},
},
});
if (!file || !nodeConnectionManager.isOnline(file.nodeId)) return [];
const requestId = randomUUID();
const ack = await nodeConnectionManager.findSidecarSubtitlesAcked(
file.nodeId,
{ requestId, localFileId: file.localFileId },
15_000
);
if (!ack.ok || !ack.subtitles?.length) return [];
const hits: SubtitleHit[] = [];
for (const sub of ack.subtitles) {
const format = (sub.format || "srt").toLowerCase();
const language = (sub.language || "nl").toLowerCase();
const fileName = sub.fileName || `local.${format}`;
const fileId = stableLocalFileId(`${mediaFileId}:${fileName}`);
putLocalCache(fileId, {
contentBase64: sub.contentBase64,
fileName,
format,
language,
mediaFileId,
});
// Persist for Stremio stream.subtitles
try {
const content = Buffer.from(sub.contentBase64, "base64").toString("utf8");
if (content.trim()) {
const movieId = file.movieId ?? null;
const tmdbId = file.movie?.tmdbId ?? file.episode?.series?.tmdbId ?? null;
const imdbId =
file.movie?.imdbId || file.episode?.imdbId || file.episode?.series?.imdbId || null;
const linkFilter = [
...(movieId ? [{ movieId }] : []),
...(tmdbId != null ? [{ tmdbId }] : []),
...(imdbId ? [{ imdbId }] : []),
];
const existing =
linkFilter.length > 0
? await prisma.librarySubtitle.findFirst({
where: { fileName, OR: linkFilter },
})
: await prisma.librarySubtitle.findFirst({
where: { fileName, content },
});
if (existing) {
await prisma.librarySubtitle.update({
where: { id: existing.id },
data: { content, language, format, updatedAt: new Date() },
});
} else {
await prisma.librarySubtitle.create({
data: {
movieId,
tmdbId,
imdbId,
language,
format,
content,
fileName,
},
});
}
}
} catch (err) {
console.warn("[sidecars] library_subtitles upsert:", err);
}
hits.push({
fileId,
language,
release: `Lokaal · ${fileName}`,
fileName,
downloadCount: 9_999_999,
hearingImpaired: /\b(sdh|hi|hearing)\b/i.test(fileName),
fps: null,
uploader: "lokaal",
source: "local",
});
}
return hits;
}