fix subtitle search: IMDb-only to stop wrong movies; restore OS.org hits.
This commit is contained in:
parent
942da3a58c
commit
d233878e6d
4 changed files with 114 additions and 63 deletions
|
|
@ -265,7 +265,7 @@ export default function DownloadsPage() {
|
|||
setSelectedSubId(preferred.fileId);
|
||||
setSelectedSubSource(preferred.source === "org" ? "org" : "com");
|
||||
} else {
|
||||
setSubsMsg("Geen NL-ondertitels gevonden (gezocht op IMDb + filmnaam via .com en .org)");
|
||||
setSubsMsg("Geen NL-ondertitels gevonden (alleen IMDb via OS.com + OS.org)");
|
||||
}
|
||||
} finally {
|
||||
setSubsBusy(false);
|
||||
|
|
|
|||
|
|
@ -106,16 +106,11 @@ export class DownloadService {
|
|||
}
|
||||
|
||||
const languages = opts.languages?.trim() || "nl";
|
||||
const title =
|
||||
opts.movieTitle?.trim() ||
|
||||
stripQualityTokens(opts.releaseHint || "").replace(/\b(19|20)\d{2}\b/g, "").trim() ||
|
||||
"";
|
||||
const year = opts.movieYear ?? null;
|
||||
const titleQuery = title
|
||||
? year && year > 1900
|
||||
? `${title} ${year}`
|
||||
: title
|
||||
: "";
|
||||
|
||||
// Alleen IMDb — free-text query mengt verkeerde films door elkaar
|
||||
if (!imdbId) {
|
||||
return { imdbId: null, results: [], configured };
|
||||
}
|
||||
|
||||
const tasks: Array<Promise<SubtitleHit[]>> = [];
|
||||
|
||||
|
|
@ -125,19 +120,11 @@ export class DownloadService {
|
|||
return [] as SubtitleHit[];
|
||||
});
|
||||
|
||||
if (comClient && imdbId) {
|
||||
if (comClient) {
|
||||
tasks.push(safe("com/imdb", comClient.search({ imdbId, languages })));
|
||||
}
|
||||
if (comClient && titleQuery) {
|
||||
tasks.push(safe("com/title", comClient.search({ query: titleQuery, languages })));
|
||||
}
|
||||
// OpenSubtitles.org = aparte catalogus (rest.opensubtitles.org)
|
||||
if (imdbId) {
|
||||
tasks.push(safe("org/imdb", orgClient.search({ imdbId, languages })));
|
||||
}
|
||||
if (titleQuery) {
|
||||
tasks.push(safe("org/title", orgClient.search({ query: titleQuery, languages })));
|
||||
}
|
||||
tasks.push(safe("org/imdb", orgClient.search({ imdbId, languages })));
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return { imdbId, results: [], configured };
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ export type SubtitleHit = {
|
|||
fps: number | null;
|
||||
uploader: string | null;
|
||||
source: SubtitleSource;
|
||||
movieName?: string | null;
|
||||
movieYear?: number | null;
|
||||
imdbId?: string | null;
|
||||
};
|
||||
|
||||
type OsLogin = { token: string; baseUrl: string; expiresAt: number };
|
||||
|
|
@ -58,7 +61,7 @@ export class OpenSubtitlesClient {
|
|||
return (
|
||||
`OpenSubtitles.com weigert toegang (403). Check: ` +
|
||||
`1) account op opensubtitles.com (niet .org), ` +
|
||||
`2) User-Agent exact gelijk aan je API Consumer-naam (nu: “${this.userAgent}”), ` +
|
||||
`2) User-Agent exact gelijk aan je API Consumer-naam (nu: "${this.userAgent}"), ` +
|
||||
`3) of kies een OS.org-ondertitel (geen .com-login nodig).`
|
||||
);
|
||||
}
|
||||
|
|
@ -92,23 +95,24 @@ export class OpenSubtitlesClient {
|
|||
return data.token;
|
||||
}
|
||||
|
||||
/** Search — login optional; failures are ignored so API-key-only still works. */
|
||||
/** Search - login optional; API-key-only still works. Only IMDb (no free-text query). */
|
||||
async search(opts: {
|
||||
imdbId?: string;
|
||||
languages?: string;
|
||||
/** Ignored — free-text query mixes wrong movies. */
|
||||
query?: string;
|
||||
}): Promise<SubtitleHit[]> {
|
||||
const languages = (opts.languages || "nl").trim() || "nl";
|
||||
const imdb = opts.imdbId?.trim().replace(/^tt/i, "").replace(/^0+/, "") || "";
|
||||
if (!imdb || !/^\d+$/.test(imdb)) return [];
|
||||
|
||||
const params = new URLSearchParams({
|
||||
languages,
|
||||
type: "movie",
|
||||
order_by: "download_count",
|
||||
order_direction: "desc",
|
||||
imdb_id: imdb,
|
||||
});
|
||||
const imdb = opts.imdbId?.trim().replace(/^tt/i, "");
|
||||
if (imdb && /^\d+$/.test(imdb)) params.set("imdb_id", imdb);
|
||||
if (opts.query?.trim()) params.set("query", opts.query.trim());
|
||||
if (!params.has("imdb_id") && !params.has("query")) return [];
|
||||
|
||||
const token = await this.ensureLogin().catch(() => null);
|
||||
const headers = this.headers(
|
||||
|
|
@ -121,7 +125,7 @@ export class OpenSubtitlesClient {
|
|||
if (res.status === 403 || /cannot consume/i.test(text)) {
|
||||
throw new AppError(
|
||||
"OPENSUBTITLES",
|
||||
`OpenSubtitles.com zoeken geweigerd (403). User-Agent “${this.userAgent}” moet exact je API Consumer-naam zijn, of gebruik alleen OS.org-hits.`,
|
||||
`OpenSubtitles.com zoeken geweigerd (403). User-Agent "${this.userAgent}" moet exact je API Consumer-naam zijn, of gebruik alleen OS.org-hits.`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
|
@ -140,6 +144,11 @@ export class OpenSubtitlesClient {
|
|||
download_count?: number;
|
||||
hearing_impaired?: boolean;
|
||||
fps?: number;
|
||||
feature_details?: {
|
||||
imdb_id?: number | string;
|
||||
title?: string;
|
||||
year?: number;
|
||||
};
|
||||
files?: Array<{ file_id?: number; file_name?: string }>;
|
||||
uploader?: { name?: string };
|
||||
};
|
||||
|
|
@ -151,6 +160,10 @@ export class OpenSubtitlesClient {
|
|||
const a = row.attributes;
|
||||
const file = a?.files?.[0];
|
||||
if (!file?.file_id) continue;
|
||||
const featImdb = a?.feature_details?.imdb_id
|
||||
? String(a.feature_details.imdb_id).replace(/^tt/i, "").replace(/^0+/, "")
|
||||
: null;
|
||||
if (featImdb && featImdb !== imdb) continue;
|
||||
out.push({
|
||||
fileId: file.file_id,
|
||||
language: (a?.language || languages).toLowerCase(),
|
||||
|
|
@ -161,6 +174,9 @@ export class OpenSubtitlesClient {
|
|||
fps: a?.fps ?? null,
|
||||
uploader: a?.uploader?.name ?? null,
|
||||
source: "com",
|
||||
movieName: a?.feature_details?.title ?? null,
|
||||
movieYear: a?.feature_details?.year ?? null,
|
||||
imdbId: featImdb || imdb,
|
||||
});
|
||||
}
|
||||
return out.slice(0, 40);
|
||||
|
|
@ -199,7 +215,7 @@ export class OpenSubtitlesClient {
|
|||
if (res.status === 403 || /cannot consume/i.test(text)) {
|
||||
throw new AppError(
|
||||
"OPENSUBTITLES",
|
||||
`OpenSubtitles.com download geweigerd (403). Kies een OS.org-ondertitel, of fix API Consumer User-Agent (“${this.userAgent}”).`,
|
||||
`OpenSubtitles.com download geweigerd (403). Kies een OS.org-ondertitel, of fix API Consumer User-Agent ("${this.userAgent}").`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,17 @@ type OrgRow = {
|
|||
SubLanguageID?: string;
|
||||
SubDownloadLink?: string;
|
||||
SubFormat?: string;
|
||||
MovieName?: string | null;
|
||||
MovieYear?: string | null;
|
||||
IDMovieImdb?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy OpenSubtitles.org REST search (still live).
|
||||
* Download via stable dl.opensubtitles.org/file/{id} (plain .srt).
|
||||
* OpenSubtitles.org legacy REST (rest.opensubtitles.org).
|
||||
* Dutch language id is `dut`. IMDb without "tt". No API key.
|
||||
*/
|
||||
export class OpenSubtitlesOrgClient {
|
||||
private readonly ua = "MediaCluster v1.3";
|
||||
private readonly ua = "MediaCluster v1.3.2";
|
||||
|
||||
async search(opts: {
|
||||
imdbId?: string;
|
||||
|
|
@ -29,23 +32,55 @@ export class OpenSubtitlesOrgClient {
|
|||
languages?: string;
|
||||
}): Promise<SubtitleHit[]> {
|
||||
const lang = toOrgLang(opts.languages || "nl");
|
||||
const parts: string[] = [`sublanguageid-${lang}`];
|
||||
const imdb = opts.imdbId?.trim().replace(/^tt/i, "");
|
||||
if (imdb && /^\d+$/.test(imdb)) {
|
||||
parts.unshift(`imdbid-${imdb}`);
|
||||
const imdbDigits = normalizeImdb(opts.imdbId);
|
||||
|
||||
const urls: string[] = [];
|
||||
if (imdbDigits) {
|
||||
urls.push(
|
||||
`https://rest.opensubtitles.org/search/imdbid-${imdbDigits}/sublanguageid-${lang}`
|
||||
);
|
||||
} else if (opts.query?.trim()) {
|
||||
parts.unshift(`query-${encodeURIComponent(opts.query.trim()).replace(/%20/g, "+")}`);
|
||||
} else {
|
||||
return [];
|
||||
const q = encodeURIComponent(opts.query.trim()).replace(/%20/g, "+");
|
||||
urls.push(`https://rest.opensubtitles.org/search/query-${q}/sublanguageid-${lang}`);
|
||||
}
|
||||
if (!urls.length) return [];
|
||||
|
||||
let lastErr: string | null = null;
|
||||
const out: SubtitleHit[] = [];
|
||||
const seen = new Set<number>();
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const rows = await this.fetchSearch(url);
|
||||
console.info(`[opensubtitles.org] ${url} -> ${rows.length} raw`);
|
||||
for (const row of rows) {
|
||||
const hit = mapRow(row, lang);
|
||||
if (!hit || seen.has(hit.fileId)) continue;
|
||||
// Alleen droppen bij expliciet andere IMDb — ontbrekende IDMovieImdb behouden
|
||||
if (imdbDigits && hit.imdbId && hit.imdbId !== imdbDigits) continue;
|
||||
seen.add(hit.fileId);
|
||||
out.push(hit);
|
||||
}
|
||||
} catch (err) {
|
||||
lastErr = err instanceof Error ? err.message : String(err);
|
||||
console.warn(`[opensubtitles.org] ${url}:`, lastErr);
|
||||
}
|
||||
}
|
||||
|
||||
const url = `https://rest.opensubtitles.org/search/${parts.join("/")}`;
|
||||
if (!out.length && lastErr) {
|
||||
throw new AppError("OPENSUBTITLES", lastErr, 400);
|
||||
}
|
||||
return out.slice(0, 50);
|
||||
}
|
||||
|
||||
private async fetchSearch(url: string): Promise<OrgRow[]> {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": this.ua,
|
||||
"X-User-Agent": this.ua,
|
||||
},
|
||||
redirect: "follow",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
|
|
@ -55,27 +90,10 @@ export class OpenSubtitlesOrgClient {
|
|||
400
|
||||
);
|
||||
}
|
||||
|
||||
const body = (await res.json()) as OrgRow[] | { data?: OrgRow[] };
|
||||
const rows = Array.isArray(body) ? body : body.data ?? [];
|
||||
const out: SubtitleHit[] = [];
|
||||
for (const row of rows) {
|
||||
const fileId = parseInt(String(row.IDSubtitleFile || ""), 10);
|
||||
if (!Number.isFinite(fileId) || fileId <= 0) continue;
|
||||
const fps = row.MovieFPS ? parseFloat(row.MovieFPS) : null;
|
||||
out.push({
|
||||
fileId,
|
||||
language: (row.ISO639 || lang || "nl").toLowerCase(),
|
||||
release: (row.MovieReleaseName || row.SubFileName || "").trim(),
|
||||
fileName: row.SubFileName || `sub-${fileId}.srt`,
|
||||
downloadCount: parseInt(String(row.SubDownloadsCnt || "0"), 10) || 0,
|
||||
hearingImpaired: row.SubHearingImpaired === "1",
|
||||
fps: fps && Number.isFinite(fps) ? fps : null,
|
||||
uploader: row.UserNickName || null,
|
||||
source: "org",
|
||||
});
|
||||
}
|
||||
return out.slice(0, 40);
|
||||
const body = (await res.json()) as OrgRow[] | { data?: OrgRow[] } | null;
|
||||
if (Array.isArray(body)) return body;
|
||||
if (body && Array.isArray(body.data)) return body.data;
|
||||
return [];
|
||||
}
|
||||
|
||||
async downloadFile(fileId: number): Promise<{ content: Buffer; fileName: string }> {
|
||||
|
|
@ -101,16 +119,46 @@ export class OpenSubtitlesOrgClient {
|
|||
fileName = fileName.replace(/\.gz$/i, "");
|
||||
if (!/\.(srt|ass|ssa|vtt)$/i.test(fileName)) fileName = `${fileName}.srt`;
|
||||
}
|
||||
if (!buf.length) {
|
||||
if (!content.length) {
|
||||
throw new AppError("OPENSUBTITLES", "OpenSubtitles.org: leeg ondertitelbestand", 400);
|
||||
}
|
||||
return { content, fileName };
|
||||
}
|
||||
}
|
||||
|
||||
function mapRow(row: OrgRow, fallbackLang: string): SubtitleHit | null {
|
||||
const fileId = parseInt(String(row.IDSubtitleFile || ""), 10);
|
||||
if (!Number.isFinite(fileId) || fileId <= 0) return null;
|
||||
const fps = row.MovieFPS ? parseFloat(row.MovieFPS) : null;
|
||||
const yearRaw = row.MovieYear ? parseInt(String(row.MovieYear), 10) : NaN;
|
||||
const imdb = normalizeImdb(row.IDMovieImdb || undefined);
|
||||
return {
|
||||
fileId,
|
||||
language: (row.ISO639 || row.SubLanguageID || fallbackLang || "nl").toLowerCase(),
|
||||
release: (row.MovieReleaseName || row.SubFileName || "").trim(),
|
||||
fileName: row.SubFileName || `sub-${fileId}.srt`,
|
||||
downloadCount: parseInt(String(row.SubDownloadsCnt || "0"), 10) || 0,
|
||||
hearingImpaired: row.SubHearingImpaired === "1",
|
||||
fps: fps && Number.isFinite(fps) ? fps : null,
|
||||
uploader: row.UserNickName || null,
|
||||
source: "org",
|
||||
movieName: row.MovieName?.trim() || null,
|
||||
movieYear: Number.isFinite(yearRaw) ? yearRaw : null,
|
||||
imdbId: imdb,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeImdb(id?: string | null): string | null {
|
||||
if (!id) return null;
|
||||
const digits = String(id).trim().replace(/^tt/i, "").replace(/^0+(?=\d)/, "");
|
||||
return /^\d+$/.test(digits) ? digits : null;
|
||||
}
|
||||
|
||||
/** Legacy REST uses OpenSubtitles language ids (`dut` = Dutch). */
|
||||
function toOrgLang(languages: string): string {
|
||||
const first = languages.split(",")[0]?.trim().toLowerCase() || "nl";
|
||||
if (first === "nl" || first === "nld" || first === "dut") return "dut";
|
||||
if (first === "nl" || first === "nld" || first === "dut" || first === "dutch") return "dut";
|
||||
if (first === "en" || first === "eng") return "eng";
|
||||
if (first.length === 3) return first;
|
||||
return first;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue