1.0.0-rc3 rc3
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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(/ /gi, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/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,
|
||||
|
||||
Reference in New Issue
Block a user