1.0.0-rc3 rc3

This commit is contained in:
2026-05-27 16:54:22 +02:00
parent 196ac2ee07
commit 1502d4d9d0
4 changed files with 616 additions and 46 deletions
+2
View File
@@ -24,6 +24,7 @@ const diskDetectionService = require('./services/diskDetectionService');
const hardwareMonitorService = require('./services/hardwareMonitorService');
const coverArtRecoveryService = require('./services/coverArtRecoveryService');
const tempCleanupService = require('./services/tempCleanupService');
const { ensureCurrentBetaKeyCacheOnStartup } = require('./services/makemkvKeyService');
const logger = require('./services/logger').child('BOOT');
const { errorToMeta } = require('./utils/errorMeta');
const { getThumbnailsDir, migrateExistingThumbnails } = require('./services/thumbnailService');
@@ -37,6 +38,7 @@ async function start() {
} catch (error) {
logger.warn('backend:runtime-settings:apply-failed', { error: errorToMeta(error) });
}
await ensureCurrentBetaKeyCacheOnStartup();
await tempCleanupService.init();
await pipelineService.init();
await converterScanService.startPolling();
+33 -5
View File
@@ -58,11 +58,39 @@ router.get(
'/makemkv/beta-key',
asyncHandler(async (req, res) => {
logger.debug('get:settings:makemkv:beta-key', { reqId: req.reqId });
const result = await fetchCurrentBetaKey();
res.json({
betaKey: result.key,
sourceUrl: result.sourceUrl
});
try {
const result = await fetchCurrentBetaKey();
res.json({
betaKey: result.key,
sourceUrl: result.sourceUrl,
validUntil: result.validUntil || null,
cached: result.cached === true,
stale: result.stale === true
});
return;
} catch (error) {
const status = Number(error?.statusCode || 0);
if (status === 429 || status === 403) {
const currentSettings = await settingsService.getSettingsMap({ forceRefresh: false });
const existingKey = String(currentSettings?.makemkv_registration_key || '').trim();
logger.warn('get:settings:makemkv:beta-key:fallback-local', {
reqId: req.reqId,
status,
hasExistingKey: Boolean(existingKey),
retryAt: error?.retryAt || null
});
res.json({
betaKey: existingKey,
sourceUrl: null,
validUntil: null,
cached: Boolean(existingKey),
stale: true,
warning: error?.message || 'Betakey-API aktuell nicht erreichbar.'
});
return;
}
throw error;
}
})
);
+567 -40
View File
@@ -2,17 +2,318 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFile } = require('child_process');
const { getDb } = require('../db/database');
const logger = require('./logger').child('MAKEMKV_KEY');
const backendPackage = require('../../package.json');
const MAKEMKV_BETA_KEY_API_URL = 'https://cable.ayra.ch/makemkv/api.php?json';
const MAKEMKV_BETA_KEY_FORUM_URLS = [
'https://www.makemkv.com/forum/viewtopic.php?t=1053',
'https://forum.makemkv.com/forum/viewtopic.php?t=1053'
];
const MAKEMKV_BETA_KEY_FORUM_URL = MAKEMKV_BETA_KEY_FORUM_URLS[0];
const MAKEMKV_BETA_KEY_CACHE_PREF_KEY = 'makemkv_beta_key_cache_v1';
const MAKEMKV_BETA_KEY_EXPIRY_GUARD_MS = 1000;
const MAKEMKV_BETA_KEY_FALLBACK_VALIDITY_HOURS = Math.max(
1,
Number(process.env.MAKEMKV_BETA_KEY_FALLBACK_VALIDITY_HOURS || 24)
);
const RIPSTER_VERSION = String(backendPackage?.version || 'dev').trim() || 'dev';
const DEFAULT_USER_AGENT = `Ripster/MakeMKVKey-${RIPSTER_VERSION} (${os.platform()}/${os.release()}; Node/${process.versions.node}) +https://github.com/mboehmlaender/ripster`;
const MAKEMKV_BETA_KEY_API_USER_AGENT = process.env.MAKEMKV_BETA_KEY_API_USER_AGENT
|| 'Ripster/1.0 (MakeMKV beta key sync)';
|| DEFAULT_USER_AGENT;
const MAKEMKV_BETA_KEY_FORUM_USER_AGENT = process.env.MAKEMKV_BETA_KEY_FORUM_USER_AGENT
|| 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';
const MAKEMKV_BETA_KEY_API_FROM = process.env.MAKEMKV_BETA_KEY_API_FROM
|| 'admin@example.invalid';
|| '';
const MAKEMKV_BETA_KEY_API_TIMEOUT_MS = Math.max(
1000,
Number(process.env.MAKEMKV_BETA_KEY_API_TIMEOUT_MS || 15000)
);
const MAKEMKV_BETA_KEY_FORUM_TIMEOUT_MS = Math.max(
MAKEMKV_BETA_KEY_API_TIMEOUT_MS,
Number(process.env.MAKEMKV_BETA_KEY_FORUM_TIMEOUT_MS || 30000)
);
const MAKEMKV_BETA_KEY_BACKOFF_DEFAULT_MS = Math.max(
30 * 1000,
Number(process.env.MAKEMKV_BETA_KEY_BACKOFF_DEFAULT_MS || (15 * 60 * 1000))
);
const MONTH_NAME_TO_INDEX = new Map([
['january', 0], ['jan', 0],
['february', 1], ['feb', 1],
['march', 2], ['mar', 2],
['april', 3], ['apr', 3],
['may', 4],
['june', 5], ['jun', 5],
['july', 6], ['jul', 6],
['august', 7], ['aug', 7],
['september', 8], ['sep', 8], ['sept', 8],
['october', 9], ['oct', 9],
['november', 10], ['nov', 10],
['december', 11], ['dec', 11]
]);
let cachedBetaKeyEntry = null;
let blockedUntilMs = 0;
function parseRetryAfterToMs(retryAfterValue) {
const raw = String(retryAfterValue || '').trim();
if (!raw) {
return 0;
}
if (/^\d+$/.test(raw)) {
const seconds = Number(raw);
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : 0;
}
const parsedDate = Date.parse(raw);
if (!Number.isFinite(parsedDate)) {
return 0;
}
return Math.max(0, parsedDate - Date.now());
}
function parseBetaKeyCandidate(value, invalidMessage = 'Betakey-Antwort ist ungültig.') {
const betaKey = String(value || '').trim();
if (!/^[A-Za-z0-9][A-Za-z0-9-]{15,}$/.test(betaKey)) {
const error = new Error(invalidMessage);
error.statusCode = 502;
throw error;
}
return betaKey;
}
function resolveMonthIndex(monthName) {
const normalized = String(monthName || '').trim().toLowerCase();
return MONTH_NAME_TO_INDEX.has(normalized) ? MONTH_NAME_TO_INDEX.get(normalized) : null;
}
function endOfMonthUtcIso(year, monthIndex) {
const y = Number(year);
const m = Number(monthIndex);
if (!Number.isInteger(y) || !Number.isInteger(m) || m < 0 || m > 11) {
return null;
}
return new Date(Date.UTC(y, m + 1, 0, 23, 59, 59, 999)).toISOString();
}
function endOfDayUtcIso(year, monthIndex, day) {
const y = Number(year);
const m = Number(monthIndex);
const d = Number(day);
if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
return null;
}
if (m < 0 || m > 11 || d < 1 || d > 31) {
return null;
}
return new Date(Date.UTC(y, m, d, 23, 59, 59, 999)).toISOString();
}
function parseValidityPhraseToIso(phrase) {
const raw = String(phrase || '').trim().replace(/\s+/g, ' ');
if (!raw) {
return null;
}
const endOfMonthMatch = raw.match(/^end of\s+([A-Za-z]+)\s+(\d{4})$/i);
if (endOfMonthMatch) {
const monthIndex = resolveMonthIndex(endOfMonthMatch[1]);
if (monthIndex != null) {
return endOfMonthUtcIso(Number(endOfMonthMatch[2]), monthIndex);
}
}
const exactDayMatch = raw.match(/^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/i);
if (exactDayMatch) {
const monthIndex = resolveMonthIndex(exactDayMatch[2]);
if (monthIndex != null) {
return endOfDayUtcIso(Number(exactDayMatch[3]), monthIndex, Number(exactDayMatch[1]));
}
}
const monthYearMatch = raw.match(/^([A-Za-z]+)\s+(\d{4})$/i);
if (monthYearMatch) {
const monthIndex = resolveMonthIndex(monthYearMatch[1]);
if (monthIndex != null) {
return endOfMonthUtcIso(Number(monthYearMatch[2]), monthIndex);
}
}
return null;
}
function parseForumValidUntilIsoFromHtml(rawBody) {
const html = String(rawBody || '');
const plainText = html
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&#160;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim();
const match = plainText.match(/valid\s+until\s+([^\.]{3,80})/i);
if (!match?.[1]) {
return null;
}
return parseValidityPhraseToIso(match[1]);
}
function normalizeCacheEntry(rawEntry) {
if (!rawEntry || typeof rawEntry !== 'object') {
return null;
}
let key = '';
try {
key = parseBetaKeyCandidate(rawEntry.key || rawEntry.betaKey || '', 'Betakey-Cache ist ungültig.');
} catch (_error) {
return null;
}
const sourceUrl = String(rawEntry.sourceUrl || MAKEMKV_BETA_KEY_FORUM_URL).trim() || MAKEMKV_BETA_KEY_FORUM_URL;
const validUntilIsoRaw = String(rawEntry.validUntil || rawEntry.validUntilIso || '').trim();
const validUntilMs = Date.parse(validUntilIsoRaw);
if (!Number.isFinite(validUntilMs)) {
return null;
}
const fetchedAtIsoRaw = String(rawEntry.fetchedAt || rawEntry.fetchedAtIso || '').trim();
const fetchedAtMs = Date.parse(fetchedAtIsoRaw);
return {
key,
sourceUrl,
validUntil: new Date(validUntilMs).toISOString(),
validUntilMs,
fetchedAt: Number.isFinite(fetchedAtMs) ? new Date(fetchedAtMs).toISOString() : null,
fetchedAtMs: Number.isFinite(fetchedAtMs) ? fetchedAtMs : null
};
}
function isCacheEntryValid(entry, nowMs = Date.now()) {
return Boolean(
entry
&& typeof entry === 'object'
&& typeof entry.key === 'string'
&& entry.key.length >= 16
&& Number.isFinite(entry.validUntilMs)
&& entry.validUntilMs > (nowMs + MAKEMKV_BETA_KEY_EXPIRY_GUARD_MS)
);
}
function buildPublicResult(entry, options = {}) {
return {
key: entry.key,
sourceUrl: entry.sourceUrl,
validUntil: entry.validUntil,
cached: options.cached === true,
stale: options.stale === true
};
}
function setInMemoryCache(entry) {
cachedBetaKeyEntry = normalizeCacheEntry(entry);
}
async function readPersistentCacheEntry() {
try {
const db = await getDb();
const row = await db.get('SELECT value FROM user_prefs WHERE key = ? LIMIT 1', [MAKEMKV_BETA_KEY_CACHE_PREF_KEY]);
if (!row?.value) {
return null;
}
const parsed = JSON.parse(String(row.value));
return normalizeCacheEntry(parsed);
} catch (error) {
logger.warn('beta-key:cache-db-read-failed', {
error: error?.message || String(error)
});
return null;
}
}
async function persistCacheEntry(entry) {
const normalized = normalizeCacheEntry(entry);
if (!normalized) {
return;
}
const payload = JSON.stringify({
key: normalized.key,
sourceUrl: normalized.sourceUrl,
validUntil: normalized.validUntil,
fetchedAt: normalized.fetchedAt || new Date().toISOString()
});
try {
const db = await getDb();
await db.run(
`INSERT INTO user_prefs (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
[MAKEMKV_BETA_KEY_CACHE_PREF_KEY, payload]
);
} catch (error) {
logger.warn('beta-key:cache-db-write-failed', {
error: error?.message || String(error)
});
}
}
async function tryUsePersistentCache(nowMs = Date.now()) {
const persistent = await readPersistentCacheEntry();
if (!isCacheEntryValid(persistent, nowMs)) {
return null;
}
setInMemoryCache(persistent);
return buildPublicResult(persistent, { cached: true, stale: false });
}
function createRateLimitError(retryAfterMs = 0) {
const parsedRetryMs = Number(retryAfterMs || 0);
const waitMs = parsedRetryMs > 0 ? parsedRetryMs : MAKEMKV_BETA_KEY_BACKOFF_DEFAULT_MS;
blockedUntilMs = Math.max(blockedUntilMs, Date.now() + waitMs);
const retryAtIso = new Date(blockedUntilMs).toISOString();
const error = new Error(`Betakey-API limitiert Anfragen. Neuer Versuch nach ${retryAtIso}.`);
error.statusCode = 429;
error.retryAt = retryAtIso;
error.retryAfterMs = Math.max(1000, blockedUntilMs - Date.now());
return error;
}
function buildExistingBackoffError() {
const retryAtMs = Math.max(Date.now() + 1000, Number(blockedUntilMs || 0));
const retryAtIso = new Date(retryAtMs).toISOString();
const error = new Error(`Betakey-API limitiert Anfragen. Neuer Versuch nach ${retryAtIso}.`);
error.statusCode = 429;
error.retryAt = retryAtIso;
error.retryAfterMs = Math.max(1000, retryAtMs - Date.now());
return error;
}
function shouldSkipFallback(error) {
const status = Number(error?.httpStatus || error?.status || 0);
return status >= 400 && status < 500;
}
function resolveValidityOrFallback(validUntilIso, sourceUrl) {
const parsedMs = Date.parse(String(validUntilIso || '').trim());
if (Number.isFinite(parsedMs)) {
return new Date(parsedMs).toISOString();
}
const fallbackIso = new Date(Date.now() + MAKEMKV_BETA_KEY_FALLBACK_VALIDITY_HOURS * 60 * 60 * 1000).toISOString();
logger.warn('beta-key:validity-parse-fallback', {
sourceUrl: sourceUrl || null,
fallbackIso
});
return fallbackIso;
}
function normalizeRegistrationKey(rawValue) {
return String(rawValue || '').trim();
@@ -93,11 +394,100 @@ async function syncRegistrationKeyToConfig(rawValue, options = {}) {
};
}
async function fetchCurrentBetaKey() {
async function fetchCurrentBetaKey(options = {}) {
const forceRefresh = options?.forceRefresh === true;
const now = Date.now();
if (!forceRefresh && isCacheEntryValid(cachedBetaKeyEntry, now)) {
return buildPublicResult(cachedBetaKeyEntry, { cached: true, stale: false });
}
if (!forceRefresh) {
const persistentCacheHit = await tryUsePersistentCache(now);
if (persistentCacheHit) {
return persistentCacheHit;
}
}
if (!forceRefresh && blockedUntilMs > now) {
throw buildExistingBackoffError();
}
const errors = [];
try {
return await fetchCurrentBetaKeyViaCurl();
const forumResult = await fetchCurrentBetaKeyViaForum();
const normalized = normalizeCacheEntry({
...forumResult,
fetchedAt: new Date().toISOString()
});
if (!normalized) {
const error = new Error('Forum-Betakey konnte nicht normalisiert werden.');
error.statusCode = 502;
throw error;
}
blockedUntilMs = 0;
setInMemoryCache(normalized);
await persistCacheEntry(normalized);
return buildPublicResult(normalized, { cached: false, stale: false });
} catch (error) {
errors.push(error?.message || String(error));
logger.warn('beta-key:forum-failed', {
error: error?.message || String(error)
});
}
try {
const apiResult = await fetchCurrentBetaKeyViaFetch();
const normalized = normalizeCacheEntry({
...apiResult,
fetchedAt: new Date().toISOString()
});
if (!normalized) {
const error = new Error('API-Betakey konnte nicht normalisiert werden.');
error.statusCode = 502;
throw error;
}
blockedUntilMs = 0;
setInMemoryCache(normalized);
await persistCacheEntry(normalized);
return buildPublicResult(normalized, { cached: false, stale: false });
} catch (error) {
errors.push(error?.message || String(error));
logger.warn('beta-key:fetch-failed', {
error: error?.message || String(error)
});
if ([403, 429].includes(Number(error?.httpStatus || error?.status || 0))) {
throw createRateLimitError(Number(error?.retryAfterMs || 0));
}
if (shouldSkipFallback(error)) {
const resolved = new Error(`Betakey konnte nicht geladen werden. Details: ${errors.join(' | ')}`);
resolved.statusCode = Number(error?.httpStatus || error?.status || 502) || 502;
throw resolved;
}
}
try {
const apiCurlResult = await fetchCurrentBetaKeyViaCurl();
const normalized = normalizeCacheEntry({
...apiCurlResult,
fetchedAt: new Date().toISOString()
});
if (!normalized) {
const error = new Error('API-curl-Betakey konnte nicht normalisiert werden.');
error.statusCode = 502;
throw error;
}
blockedUntilMs = 0;
setInMemoryCache(normalized);
await persistCacheEntry(normalized);
return buildPublicResult(normalized, { cached: false, stale: false });
} catch (error) {
errors.push(error?.message || String(error));
logger.warn('beta-key:curl-failed', {
@@ -105,18 +495,28 @@ async function fetchCurrentBetaKey() {
});
}
try {
return await fetchCurrentBetaKeyViaFetch();
} catch (error) {
errors.push(error?.message || String(error));
logger.warn('beta-key:fetch-failed', {
error: error?.message || String(error)
});
}
const finalError = new Error(`Betakey konnte nicht geladen werden. Details: ${errors.join(' | ')}`);
finalError.statusCode = 502;
throw finalError;
}
const error = new Error(`Betakey konnte nicht geladen werden. Details: ${errors.join(' | ')}`);
error.statusCode = 502;
throw error;
async function ensureCurrentBetaKeyCacheOnStartup() {
try {
const current = await fetchCurrentBetaKey();
logger.info('beta-key:startup-check:done', {
sourceUrl: current?.sourceUrl || null,
validUntil: current?.validUntil || null,
cached: current?.cached === true
});
return current;
} catch (error) {
logger.warn('beta-key:startup-check:failed', {
error: error?.message || String(error),
statusCode: Number(error?.statusCode || 0) || null,
retryAt: error?.retryAt || null
});
return null;
}
}
function parseBetaKeyResponse(rawBody) {
@@ -129,35 +529,62 @@ function parseBetaKeyResponse(rawBody) {
throw error;
}
const betaKey = String(payload?.key || '').trim();
const betaKey = parseBetaKeyCandidate(String(payload?.key || '').trim(), 'Betakey-Antwort ist ungültig.');
if (!/^[A-Za-z0-9][A-Za-z0-9-]{15,}$/.test(betaKey)) {
const error = new Error('Betakey-Antwort ist ungültig.');
error.statusCode = 502;
throw error;
let validUntil = null;
const unixExpiry = Number(payload?.keydate || payload?.expires || payload?.valid_until_unix || 0);
if (Number.isFinite(unixExpiry) && unixExpiry > 0) {
validUntil = new Date(unixExpiry * 1000).toISOString();
}
const resolvedValidUntil = resolveValidityOrFallback(validUntil, MAKEMKV_BETA_KEY_API_URL);
return {
key: betaKey,
sourceUrl: MAKEMKV_BETA_KEY_API_URL
sourceUrl: MAKEMKV_BETA_KEY_API_URL,
validUntil: resolvedValidUntil
};
}
function parseBetaKeyFromForumHtml(rawBody, sourceUrl = MAKEMKV_BETA_KEY_FORUM_URL) {
const html = String(rawBody || '');
const contextualMatch = html.match(
/current\s+beta\s+key\s+is[\s\S]{0,4000}?<pre><code>\s*([A-Za-z0-9-]{16,})\s*<\/code>/i
);
const firstCodeMatch = html.match(/<pre><code>\s*([A-Za-z0-9-]{16,})\s*<\/code>/i);
const keyRaw = contextualMatch?.[1] || firstCodeMatch?.[1] || '';
const key = parseBetaKeyCandidate(keyRaw, 'Betakey konnte im Forum nicht gefunden werden.');
const parsedValidity = parseForumValidUntilIsoFromHtml(html);
const validUntil = resolveValidityOrFallback(parsedValidity, sourceUrl);
return {
key,
sourceUrl,
validUntil
};
}
function fetchCurrentBetaKeyViaCurl() {
return new Promise((resolve, reject) => {
const curlArgs = [
'-fsSL',
'--max-time', String(Math.ceil(MAKEMKV_BETA_KEY_API_TIMEOUT_MS / 1000)),
'-A', MAKEMKV_BETA_KEY_API_USER_AGENT,
'-H', 'Accept: text/plain, text/html;q=0.9, */*;q=0.8',
'-H', 'Accept-Language: de,en-US;q=0.7,en;q=0.3',
'-H', 'Cache-Control: no-cache',
'-H', 'Pragma: no-cache'
];
const fromHeader = String(MAKEMKV_BETA_KEY_API_FROM || '').trim();
if (fromHeader) {
curlArgs.push('-H', `From: ${fromHeader}`);
}
curlArgs.push(MAKEMKV_BETA_KEY_API_URL);
execFile(
'curl',
[
'-fsSL',
'--max-time', String(Math.ceil(MAKEMKV_BETA_KEY_API_TIMEOUT_MS / 1000)),
'-A', MAKEMKV_BETA_KEY_API_USER_AGENT,
'-H', `From: ${MAKEMKV_BETA_KEY_API_FROM}`,
'-H', 'Accept: text/plain, text/html;q=0.9, */*;q=0.8',
'-H', 'Accept-Language: de,en-US;q=0.7,en;q=0.3',
'-H', 'Cache-Control: no-cache',
'-H', 'Pragma: no-cache',
MAKEMKV_BETA_KEY_API_URL
],
curlArgs,
{
timeout: MAKEMKV_BETA_KEY_API_TIMEOUT_MS,
maxBuffer: 1024 * 1024
@@ -168,6 +595,12 @@ function fetchCurrentBetaKeyViaCurl() {
`curl fehlgeschlagen (${error.code || 'unknown'}): ${String(stderr || error.message || '').trim() || 'keine Details'}`
);
resolvedError.statusCode = 502;
if (/\b429\b/.test(String(stderr || ''))) {
resolvedError.httpStatus = 429;
}
if (/\b403\b/.test(String(stderr || ''))) {
resolvedError.httpStatus = 403;
}
reject(resolvedError);
return;
}
@@ -183,21 +616,28 @@ function fetchCurrentBetaKeyViaCurl() {
}
async function fetchCurrentBetaKeyViaFetch() {
const requestHeaders = {
'User-Agent': MAKEMKV_BETA_KEY_API_USER_AGENT,
'Accept': 'text/plain, text/html;q=0.9, */*;q=0.8',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
};
const fromHeader = String(MAKEMKV_BETA_KEY_API_FROM || '').trim();
if (fromHeader) {
requestHeaders.From = fromHeader;
}
const response = await fetch(MAKEMKV_BETA_KEY_API_URL, {
headers: {
'User-Agent': MAKEMKV_BETA_KEY_API_USER_AGENT,
'From': MAKEMKV_BETA_KEY_API_FROM,
'Accept': 'text/plain, text/html;q=0.9, */*;q=0.8',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
},
headers: requestHeaders,
signal: AbortSignal.timeout(MAKEMKV_BETA_KEY_API_TIMEOUT_MS)
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.statusCode = 502;
error.httpStatus = response.status;
error.retryAfterMs = parseRetryAfterToMs(response.headers.get('retry-after'));
throw error;
}
@@ -205,9 +645,96 @@ async function fetchCurrentBetaKeyViaFetch() {
return parseBetaKeyResponse(rawBody);
}
async function fetchCurrentBetaKeyViaForum() {
try {
return await fetchCurrentBetaKeyViaForumCurl();
} catch (error) {
const resolved = new Error(`Forum-Betakey konnte nicht geladen werden. Details: ${error?.message || String(error)}`);
resolved.statusCode = 502;
throw resolved;
}
}
function fetchCurrentBetaKeyViaForumCurl() {
return new Promise((resolve, reject) => {
const errors = [];
const urls = [...MAKEMKV_BETA_KEY_FORUM_URLS];
const runAttempt = (index) => {
if (index >= urls.length) {
const finalError = new Error(errors.join(' | ') || 'Keine Forum-URL verfügbar');
finalError.statusCode = 502;
reject(finalError);
return;
}
const forumUrl = urls[index];
let referer = 'https://www.makemkv.com/';
try {
referer = `${new URL(forumUrl).origin}/`;
} catch (_error) {
referer = 'https://www.makemkv.com/';
}
const curlArgs = [
'-fsSL',
'--max-time', String(Math.ceil(MAKEMKV_BETA_KEY_FORUM_TIMEOUT_MS / 1000)),
'-A', MAKEMKV_BETA_KEY_FORUM_USER_AGENT,
'-H', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'-H', 'Accept-Language: de,en-US;q=0.7,en;q=0.3',
'-H', 'Cache-Control: no-cache',
'-H', 'Pragma: no-cache',
'-H', `Referer: ${referer}`
];
const fromHeader = String(MAKEMKV_BETA_KEY_API_FROM || '').trim();
if (fromHeader) {
curlArgs.push('-H', `From: ${fromHeader}`);
}
curlArgs.push(forumUrl);
execFile(
'curl',
curlArgs,
{
timeout: MAKEMKV_BETA_KEY_FORUM_TIMEOUT_MS,
maxBuffer: 4 * 1024 * 1024
},
(error, stdout, stderr) => {
if (error) {
const resolvedError = new Error(
`Forum-curl ${forumUrl} fehlgeschlagen (${error.code || 'unknown'}): ${String(stderr || error.message || '').trim() || 'keine Details'}`
);
resolvedError.statusCode = 502;
if (/\b429\b/.test(String(stderr || ''))) {
resolvedError.httpStatus = 429;
}
if (/\b403\b/.test(String(stderr || ''))) {
resolvedError.httpStatus = 403;
}
errors.push(resolvedError.message);
runAttempt(index + 1);
return;
}
try {
resolve(parseBetaKeyFromForumHtml(stdout, forumUrl));
} catch (parseError) {
errors.push(`Forum-Parse ${forumUrl} fehlgeschlagen: ${parseError?.message || String(parseError)}`);
runAttempt(index + 1);
}
}
);
};
runAttempt(0);
});
}
module.exports = {
MAKEMKV_BETA_KEY_API_URL,
MAKEMKV_BETA_KEY_FORUM_URL,
fetchCurrentBetaKey,
ensureCurrentBetaKeyCacheOnStartup,
getMakeMKVConfigDir,
getMakeMKVSettingsFilePath,
normalizeRegistrationKey,
@@ -668,25 +668,32 @@ function isReadonlySetting(setting) {
function MakeMKVBetaKeyHint({ onApply, disabled = false }) {
const [loading, setLoading] = useState(true);
const [betaKey, setBetaKey] = useState('');
const [validUntil, setValidUntil] = useState('');
const [error, setError] = useState('');
const [warning, setWarning] = useState('');
useEffect(() => {
let active = true;
setLoading(true);
setError('');
setWarning('');
setValidUntil('');
api.getMakeMKVBetaKey({ forceRefresh: true })
api.getMakeMKVBetaKey()
.then((response) => {
if (!active) {
return;
}
setBetaKey(String(response?.betaKey || '').trim());
setValidUntil(String(response?.validUntil || '').trim());
setWarning(String(response?.warning || '').trim());
})
.catch((loadError) => {
if (!active) {
return;
}
setBetaKey('');
setValidUntil('');
setError(loadError?.message || 'Betakey konnte nicht geladen werden.');
})
.finally(() => {
@@ -719,6 +726,12 @@ function MakeMKVBetaKeyHint({ onApply, disabled = false }) {
{!loading && error ? (
<small className="error-text">{error}</small>
) : null}
{!loading && !error && warning ? (
<small className="setting-description">{warning}</small>
) : null}
{!loading && !error && validUntil ? (
<small className="setting-description">Gültig bis: {new Date(validUntil).toLocaleString('de-DE')}</small>
) : null}
{!loading && !error && betaKey ? (
<code className="makemkv-beta-key-value">{betaKey}</code>
) : null}