Clarify OpenSubtitles.com 403 (User-Agent) and prefer OS.org downloads without .com login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
93bbd07487
commit
afcc2e759f
8 changed files with 140 additions and 33 deletions
|
|
@ -254,8 +254,11 @@ export default function DownloadsPage() {
|
||||||
const results: SubtitleHit[] = data.results ?? [];
|
const results: SubtitleHit[] = data.results ?? [];
|
||||||
setSubs(results);
|
setSubs(results);
|
||||||
if (results[0]) {
|
if (results[0]) {
|
||||||
setSelectedSubId(results[0].fileId);
|
// Voorkeur OS.org — werkt zonder .com-login
|
||||||
setSelectedSubSource(results[0].source === "org" ? "org" : "com");
|
const preferred =
|
||||||
|
results.find((r) => r.source === "org") || results[0];
|
||||||
|
setSelectedSubId(preferred.fileId);
|
||||||
|
setSelectedSubSource(preferred.source === "org" ? "org" : "com");
|
||||||
} else {
|
} else {
|
||||||
setSubsMsg("Geen NL-ondertitels gevonden (gezocht op IMDb + filmnaam via .com en .org)");
|
setSubsMsg("Geen NL-ondertitels gevonden (gezocht op IMDb + filmnaam via .com en .org)");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export default function SettingsPage() {
|
||||||
const [osApiKey, setOsApiKey] = useState("");
|
const [osApiKey, setOsApiKey] = useState("");
|
||||||
const [osUsername, setOsUsername] = useState("");
|
const [osUsername, setOsUsername] = useState("");
|
||||||
const [osPassword, setOsPassword] = useState("");
|
const [osPassword, setOsPassword] = useState("");
|
||||||
|
const [osUserAgent, setOsUserAgent] = useState("MediaCluster v1.3");
|
||||||
const [osEnabled, setOsEnabled] = useState(true);
|
const [osEnabled, setOsEnabled] = useState(true);
|
||||||
const [osHasKey, setOsHasKey] = useState(false);
|
const [osHasKey, setOsHasKey] = useState(false);
|
||||||
const [osHasPassword, setOsHasPassword] = useState(false);
|
const [osHasPassword, setOsHasPassword] = useState(false);
|
||||||
|
|
@ -96,6 +97,7 @@ export default function SettingsPage() {
|
||||||
const [osBusy, setOsBusy] = useState(false);
|
const [osBusy, setOsBusy] = useState(false);
|
||||||
const [osMsg, setOsMsg] = useState<string | null>(null);
|
const [osMsg, setOsMsg] = useState<string | null>(null);
|
||||||
const [osErr, setOsErr] = useState<string | null>(null);
|
const [osErr, setOsErr] = useState<string | null>(null);
|
||||||
|
const [osWarn, setOsWarn] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadShelves = useCallback(() => {
|
const loadShelves = useCallback(() => {
|
||||||
fetch("/api/v1/admin/library/shelves", { credentials: "include" })
|
fetch("/api/v1/admin/library/shelves", { credentials: "include" })
|
||||||
|
|
@ -131,8 +133,10 @@ export default function SettingsPage() {
|
||||||
setOsConfigured(!!s.configured);
|
setOsConfigured(!!s.configured);
|
||||||
setOsEnabled(s.enabled !== false);
|
setOsEnabled(s.enabled !== false);
|
||||||
setOsUsername(s.username ?? "");
|
setOsUsername(s.username ?? "");
|
||||||
|
setOsUserAgent(s.userAgent || "MediaCluster v1.3");
|
||||||
setOsHasKey(!!s.hasApiKey);
|
setOsHasKey(!!s.hasApiKey);
|
||||||
setOsHasPassword(!!s.hasPassword);
|
setOsHasPassword(!!s.hasPassword);
|
||||||
|
setOsWarn(s.warning || null);
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -217,6 +221,7 @@ export default function SettingsPage() {
|
||||||
setOsBusy(true);
|
setOsBusy(true);
|
||||||
setOsMsg(null);
|
setOsMsg(null);
|
||||||
setOsErr(null);
|
setOsErr(null);
|
||||||
|
setOsWarn(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/v1/admin/settings/opensubtitles", {
|
const res = await fetch("/api/v1/admin/settings/opensubtitles", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -226,6 +231,7 @@ export default function SettingsPage() {
|
||||||
apiKey: osApiKey || undefined,
|
apiKey: osApiKey || undefined,
|
||||||
username: osUsername,
|
username: osUsername,
|
||||||
password: osPassword || undefined,
|
password: osPassword || undefined,
|
||||||
|
userAgent: osUserAgent.trim() || "MediaCluster v1.3",
|
||||||
enabled: osEnabled,
|
enabled: osEnabled,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -234,7 +240,13 @@ export default function SettingsPage() {
|
||||||
setOsErr(data.error?.message ?? "Opslaan mislukt");
|
setOsErr(data.error?.message ?? "Opslaan mislukt");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setOsMsg("Opgeslagen — OpenSubtitles OK");
|
const s = data.settings;
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (s?.orgOk) parts.push("OS.org OK");
|
||||||
|
if (s?.comOk) parts.push("OS.com OK");
|
||||||
|
else if (s?.hasApiKey !== false) parts.push("OS.com niet OK");
|
||||||
|
setOsMsg(`Opgeslagen — ${parts.join(" · ") || "OK"}`);
|
||||||
|
setOsWarn(s?.warning || null);
|
||||||
setOsApiKey("");
|
setOsApiKey("");
|
||||||
setOsPassword("");
|
setOsPassword("");
|
||||||
loadOpenSubtitles();
|
loadOpenSubtitles();
|
||||||
|
|
@ -491,8 +503,10 @@ export default function SettingsPage() {
|
||||||
<div className="section-head">
|
<div className="section-head">
|
||||||
<h2 className="section-title">OpenSubtitles</h2>
|
<h2 className="section-title">OpenSubtitles</h2>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
Zoekt via OpenSubtitles.com (API-key) én OpenSubtitles.org. Key optioneel maar
|
Zoekt via OpenSubtitles.com én .org. Account/login moet van{" "}
|
||||||
verhoogt de kans — aanmaken via opensubtitles.com → API consumers.
|
<strong>opensubtitles.com</strong> zijn (niet .org). User-Agent moet exact gelijk
|
||||||
|
zijn aan de naam van je API Consumer. Lukt .com niet: kies bij Downloads een{" "}
|
||||||
|
<strong>OS.org</strong>-hit.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
|
@ -508,7 +522,16 @@ export default function SettingsPage() {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Gebruikersnaam
|
User-Agent (API Consumer-naam)
|
||||||
|
<input
|
||||||
|
value={osUserAgent}
|
||||||
|
onChange={(e) => setOsUserAgent(e.target.value)}
|
||||||
|
placeholder="MediaCluster v1.3"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Gebruikersnaam (.com)
|
||||||
<input
|
<input
|
||||||
value={osUsername}
|
value={osUsername}
|
||||||
onChange={(e) => setOsUsername(e.target.value)}
|
onChange={(e) => setOsUsername(e.target.value)}
|
||||||
|
|
@ -516,7 +539,7 @@ export default function SettingsPage() {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Wachtwoord {osHasPassword ? "(leeg = behouden)" : ""}
|
Wachtwoord (.com) {osHasPassword ? "(leeg = behouden)" : ""}
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={osPassword}
|
value={osPassword}
|
||||||
|
|
@ -538,9 +561,10 @@ export default function SettingsPage() {
|
||||||
{osBusy ? "Bezig…" : "Opslaan + testen"}
|
{osBusy ? "Bezig…" : "Opslaan + testen"}
|
||||||
</button>
|
</button>
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
Status: {osConfigured ? "geconfigureerd" : "niet geconfigureerd"}
|
Status: {osConfigured ? "aan" : "uit"}
|
||||||
</span>
|
</span>
|
||||||
{osMsg && <span className="ok-text">{osMsg}</span>}
|
{osMsg && <span className="ok-text">{osMsg}</span>}
|
||||||
|
{osWarn && <span className="danger-text">{osWarn}</span>}
|
||||||
{osErr && <span className="danger-text">{osErr}</span>}
|
{osErr && <span className="danger-text">{osErr}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "integration_settings" ADD COLUMN IF NOT EXISTS "user_agent" TEXT;
|
||||||
|
|
@ -439,6 +439,8 @@ model IntegrationSetting {
|
||||||
apiKeyEnc String? @map("api_key_enc")
|
apiKeyEnc String? @map("api_key_enc")
|
||||||
username String?
|
username String?
|
||||||
passwordEnc String? @map("password_enc")
|
passwordEnc String? @map("password_enc")
|
||||||
|
/** Must match OpenSubtitles.com API Consumer / User-Agent exactly */
|
||||||
|
userAgent String? @map("user_agent")
|
||||||
enabled Boolean @default(true)
|
enabled Boolean @default(true)
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
|
||||||
|
|
@ -265,12 +265,16 @@ async function main() {
|
||||||
"api_key_enc" TEXT,
|
"api_key_enc" TEXT,
|
||||||
"username" TEXT,
|
"username" TEXT,
|
||||||
"password_enc" TEXT,
|
"password_enc" TEXT,
|
||||||
|
"user_agent" TEXT,
|
||||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT "integration_settings_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "integration_settings_pkey" PRIMARY KEY ("id")
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "integration_settings" ADD COLUMN IF NOT EXISTS "user_agent" TEXT`
|
||||||
|
);
|
||||||
await prisma.$executeRawUnsafe(`
|
await prisma.$executeRawUnsafe(`
|
||||||
CREATE TABLE IF NOT EXISTS "library_subtitles" (
|
CREATE TABLE IF NOT EXISTS "library_subtitles" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
|
|
|
||||||
|
|
@ -16,23 +16,29 @@ export type SubtitleHit = {
|
||||||
|
|
||||||
type OsLogin = { token: string; baseUrl: string; expiresAt: number };
|
type OsLogin = { token: string; baseUrl: string; expiresAt: number };
|
||||||
|
|
||||||
|
const DEFAULT_UA = "MediaCluster v1.3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenSubtitles.com REST API (v1).
|
* OpenSubtitles.com REST API (v1).
|
||||||
* Requires OPENSUBTITLES_API_KEY; login optional but needed for higher download quotas.
|
* User-Agent must match the registered API Consumer name exactly.
|
||||||
*/
|
*/
|
||||||
export class OpenSubtitlesClient {
|
export class OpenSubtitlesClient {
|
||||||
private loginCache: OsLogin | null = null;
|
private loginCache: OsLogin | null = null;
|
||||||
|
private readonly userAgent: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly apiKey: string,
|
private readonly apiKey: string,
|
||||||
private readonly username?: string,
|
private readonly username?: string,
|
||||||
private readonly password?: string
|
private readonly password?: string,
|
||||||
) {}
|
userAgent?: string | null
|
||||||
|
) {
|
||||||
|
this.userAgent = userAgent?.trim() || DEFAULT_UA;
|
||||||
|
}
|
||||||
|
|
||||||
private headers(extra?: Record<string, string>): Record<string, string> {
|
private headers(extra?: Record<string, string>): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
"Api-Key": this.apiKey,
|
"Api-Key": this.apiKey,
|
||||||
"User-Agent": "MediaCluster v1.3",
|
"User-Agent": this.userAgent,
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
|
|
@ -45,6 +51,18 @@ export class OpenSubtitlesClient {
|
||||||
return "https://api.opensubtitles.com";
|
return "https://api.opensubtitles.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loginHint(status: number, body: string): string {
|
||||||
|
if (status === 403 || /cannot consume/i.test(body)) {
|
||||||
|
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}”), ` +
|
||||||
|
`3) of kies een OS.org-ondertitel (geen .com-login nodig).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return `OpenSubtitles.com login mislukt (${status}): ${body.slice(0, 160)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async ensureLogin(): Promise<string | null> {
|
async ensureLogin(): Promise<string | null> {
|
||||||
if (!this.username?.trim() || !this.password?.trim()) return null;
|
if (!this.username?.trim() || !this.password?.trim()) return null;
|
||||||
if (this.loginCache && Date.now() < this.loginCache.expiresAt) {
|
if (this.loginCache && Date.now() < this.loginCache.expiresAt) {
|
||||||
|
|
@ -60,14 +78,10 @@ export class OpenSubtitlesClient {
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
throw new AppError(
|
throw new AppError("OPENSUBTITLES", this.loginHint(res.status, text), 400);
|
||||||
"OPENSUBTITLES",
|
|
||||||
`OpenSubtitles login mislukt (${res.status}): ${text.slice(0, 160)}`,
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const data = (await res.json()) as { token?: string; base_url?: string };
|
const data = (await res.json()) as { token?: string; base_url?: string };
|
||||||
if (!data.token) throw new AppError("OPENSUBTITLES", "OpenSubtitles: geen token", 400);
|
if (!data.token) throw new AppError("OPENSUBTITLES", "OpenSubtitles.com: geen token", 400);
|
||||||
this.loginCache = {
|
this.loginCache = {
|
||||||
token: data.token,
|
token: data.token,
|
||||||
baseUrl: data.base_url || "api.opensubtitles.com",
|
baseUrl: data.base_url || "api.opensubtitles.com",
|
||||||
|
|
@ -76,6 +90,7 @@ export class OpenSubtitlesClient {
|
||||||
return data.token;
|
return data.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Search — login optional; failures are ignored so API-key-only still works. */
|
||||||
async search(opts: {
|
async search(opts: {
|
||||||
imdbId?: string;
|
imdbId?: string;
|
||||||
languages?: string;
|
languages?: string;
|
||||||
|
|
@ -101,6 +116,13 @@ export class OpenSubtitlesClient {
|
||||||
const res = await fetch(`${this.root()}/api/v1/subtitles?${params}`, { headers });
|
const res = await fetch(`${this.root()}/api/v1/subtitles?${params}`, { headers });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
|
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.`,
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
"OPENSUBTITLES",
|
"OPENSUBTITLES",
|
||||||
`OpenSubtitles.com zoeken mislukt (${res.status}): ${text.slice(0, 160)}`,
|
`OpenSubtitles.com zoeken mislukt (${res.status}): ${text.slice(0, 160)}`,
|
||||||
|
|
@ -143,14 +165,23 @@ export class OpenSubtitlesClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(fileId: number): Promise<{ content: Buffer; fileName: string }> {
|
async downloadFile(fileId: number): Promise<{ content: Buffer; fileName: string }> {
|
||||||
const token = await this.ensureLogin();
|
let token: string | null = null;
|
||||||
|
let loginError: string | null = null;
|
||||||
|
try {
|
||||||
|
token = await this.ensureLogin();
|
||||||
|
} catch (err) {
|
||||||
|
loginError = err instanceof Error ? err.message : "Login mislukt";
|
||||||
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
"OPENSUBTITLES",
|
"OPENSUBTITLES",
|
||||||
"OpenSubtitles.com download vereist gebruikersnaam + wachtwoord (Instellingen)",
|
loginError ||
|
||||||
|
"OpenSubtitles.com download vereist werkende login (opensubtitles.com-account). Kies liever een OS.org-hit.",
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = this.headers({
|
const headers = this.headers({
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
|
|
@ -163,6 +194,13 @@ export class OpenSubtitlesClient {
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
|
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}”).`,
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
"OPENSUBTITLES",
|
"OPENSUBTITLES",
|
||||||
`OpenSubtitles.com download mislukt (${res.status}): ${text.slice(0, 200)}`,
|
`OpenSubtitles.com download mislukt (${res.status}): ${text.slice(0, 200)}`,
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { AppError } from "../security/errors";
|
||||||
import { OpenSubtitlesClient } from "../opensubtitles/client";
|
import { OpenSubtitlesClient } from "../opensubtitles/client";
|
||||||
|
|
||||||
const OS_ID = "opensubtitles";
|
const OS_ID = "opensubtitles";
|
||||||
|
const DEFAULT_UA = "MediaCluster v1.3";
|
||||||
|
|
||||||
export async function isOpenSubtitlesEnabled(): Promise<boolean> {
|
export async function isOpenSubtitlesEnabled(): Promise<boolean> {
|
||||||
const row = await prisma.integrationSetting.findUnique({ where: { id: OS_ID } });
|
const row = await prisma.integrationSetting.findUnique({ where: { id: OS_ID } });
|
||||||
// Geen rij = standaard aan (.org werkt zonder key)
|
|
||||||
if (!row) return true;
|
if (!row) return true;
|
||||||
return row.enabled;
|
return row.enabled;
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,12 @@ export type OpenSubtitlesSettingsPublic = {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
|
userAgent: string;
|
||||||
hasApiKey: boolean;
|
hasApiKey: boolean;
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
|
comOk: boolean;
|
||||||
|
orgOk: boolean;
|
||||||
|
warning: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getOpenSubtitlesSettingsPublic(): Promise<OpenSubtitlesSettingsPublic> {
|
export async function getOpenSubtitlesSettingsPublic(): Promise<OpenSubtitlesSettingsPublic> {
|
||||||
|
|
@ -27,8 +31,12 @@ export async function getOpenSubtitlesSettingsPublic(): Promise<OpenSubtitlesSet
|
||||||
configured: enabled,
|
configured: enabled,
|
||||||
enabled,
|
enabled,
|
||||||
username: row?.username ?? "",
|
username: row?.username ?? "",
|
||||||
|
userAgent: row?.userAgent?.trim() || DEFAULT_UA,
|
||||||
hasApiKey: !!row?.apiKeyEnc,
|
hasApiKey: !!row?.apiKeyEnc,
|
||||||
hasPassword: !!row?.passwordEnc,
|
hasPassword: !!row?.passwordEnc,
|
||||||
|
comOk: false,
|
||||||
|
orgOk: false,
|
||||||
|
warning: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,12 +46,12 @@ export async function upsertOpenSubtitlesSettings(
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
userAgent?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<OpenSubtitlesSettingsPublic> {
|
): Promise<OpenSubtitlesSettingsPublic> {
|
||||||
const existing = await prisma.integrationSetting.findUnique({ where: { id: OS_ID } });
|
const existing = await prisma.integrationSetting.findUnique({ where: { id: OS_ID } });
|
||||||
const apiKeyTrim = input.apiKey?.trim();
|
const apiKeyTrim = input.apiKey?.trim();
|
||||||
// API-key optioneel: zonder key blijft OpenSubtitles.org beschikbaar
|
|
||||||
const apiKeyEnc = apiKeyTrim
|
const apiKeyEnc = apiKeyTrim
|
||||||
? encryptSecret(apiKeyTrim, sessionSecret)
|
? encryptSecret(apiKeyTrim, sessionSecret)
|
||||||
: existing?.apiKeyEnc ?? null;
|
: existing?.apiKeyEnc ?? null;
|
||||||
|
|
@ -55,6 +63,10 @@ export async function upsertOpenSubtitlesSettings(
|
||||||
: existing?.passwordEnc ?? null;
|
: existing?.passwordEnc ?? null;
|
||||||
const username =
|
const username =
|
||||||
input.username !== undefined ? input.username.trim() || null : existing?.username ?? null;
|
input.username !== undefined ? input.username.trim() || null : existing?.username ?? null;
|
||||||
|
const userAgent =
|
||||||
|
input.userAgent !== undefined
|
||||||
|
? input.userAgent.trim() || DEFAULT_UA
|
||||||
|
: existing?.userAgent?.trim() || DEFAULT_UA;
|
||||||
|
|
||||||
await prisma.integrationSetting.upsert({
|
await prisma.integrationSetting.upsert({
|
||||||
where: { id: OS_ID },
|
where: { id: OS_ID },
|
||||||
|
|
@ -63,31 +75,48 @@ export async function upsertOpenSubtitlesSettings(
|
||||||
apiKeyEnc,
|
apiKeyEnc,
|
||||||
username,
|
username,
|
||||||
passwordEnc,
|
passwordEnc,
|
||||||
|
userAgent,
|
||||||
enabled: input.enabled ?? true,
|
enabled: input.enabled ?? true,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
apiKeyEnc,
|
apiKeyEnc,
|
||||||
username,
|
username,
|
||||||
passwordEnc,
|
passwordEnc,
|
||||||
|
userAgent,
|
||||||
enabled: input.enabled ?? true,
|
enabled: input.enabled ?? true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate: .com key if present, else .org search smoke-test
|
let comOk = false;
|
||||||
|
let orgOk = false;
|
||||||
|
let warning: string | null = null;
|
||||||
|
|
||||||
const client = await createOpenSubtitlesClientFromDb(sessionSecret);
|
const client = await createOpenSubtitlesClientFromDb(sessionSecret);
|
||||||
try {
|
if (client) {
|
||||||
if (client) {
|
try {
|
||||||
await client.search({ imdbId: "tt1375666", languages: "nl" });
|
await client.search({ imdbId: "tt1375666", languages: "nl" });
|
||||||
} else {
|
comOk = true;
|
||||||
const { OpenSubtitlesOrgClient } = await import("../opensubtitles/org-client");
|
} catch (err) {
|
||||||
await new OpenSubtitlesOrgClient().search({ imdbId: "tt1375666", languages: "nl" });
|
warning = err instanceof Error ? err.message : "OpenSubtitles.com test mislukt";
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : "OpenSubtitles test mislukt";
|
|
||||||
throw new AppError("OPENSUBTITLES", message, 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getOpenSubtitlesSettingsPublic();
|
try {
|
||||||
|
const { OpenSubtitlesOrgClient } = await import("../opensubtitles/org-client");
|
||||||
|
await new OpenSubtitlesOrgClient().search({ imdbId: "tt1375666", languages: "nl" });
|
||||||
|
orgOk = true;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : "OpenSubtitles.org test mislukt";
|
||||||
|
warning = warning ? `${warning} · ${msg}` : msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comOk && !orgOk) {
|
||||||
|
throw new AppError("OPENSUBTITLES", warning || "Geen OpenSubtitles-bron bereikbaar", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opslaan mag als .org OK is, ook als .com login/API 403 geeft
|
||||||
|
const pub = await getOpenSubtitlesSettingsPublic();
|
||||||
|
return { ...pub, comOk, orgOk, warning };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOpenSubtitlesClientFromDb(
|
export async function createOpenSubtitlesClientFromDb(
|
||||||
|
|
@ -97,5 +126,10 @@ export async function createOpenSubtitlesClientFromDb(
|
||||||
if (!row?.enabled || !row.apiKeyEnc) return null;
|
if (!row?.enabled || !row.apiKeyEnc) return null;
|
||||||
const apiKey = decryptSecret(row.apiKeyEnc, sessionSecret);
|
const apiKey = decryptSecret(row.apiKeyEnc, sessionSecret);
|
||||||
const password = row.passwordEnc ? decryptSecret(row.passwordEnc, sessionSecret) : undefined;
|
const password = row.passwordEnc ? decryptSecret(row.passwordEnc, sessionSecret) : undefined;
|
||||||
return new OpenSubtitlesClient(apiKey, row.username ?? undefined, password);
|
return new OpenSubtitlesClient(
|
||||||
|
apiKey,
|
||||||
|
row.username ?? undefined,
|
||||||
|
password,
|
||||||
|
row.userAgent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export function registerSettingsRoutes(app: FastifyInstance, config: Config) {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
userAgent?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
const settings = await upsertOpenSubtitlesSettings(config.SESSION_SECRET, body);
|
const settings = await upsertOpenSubtitlesSettings(config.SESSION_SECRET, body);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue