diff --git a/backend/package-lock.json b/backend/package-lock.json index 2111643..fb953f7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.10.0-8", + "version": "0.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.10.0-8", + "version": "0.10.1", "dependencies": { "cors": "^2.8.5", "dotenv": "^16.4.7", diff --git a/backend/package.json b/backend/package.json index 48b8739..26ee788 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.10.0-8", + "version": "0.10.1", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index 9ceea5e..8c4ecf3 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -8,6 +8,7 @@ const pipelineService = require('../services/pipelineService'); const wsService = require('../services/websocketService'); const hardwareMonitorService = require('../services/hardwareMonitorService'); const userPresetService = require('../services/userPresetService'); +const activationBytesService = require('../services/activationBytesService'); const logger = require('../services/logger').child('SETTINGS_ROUTE'); const router = express.Router(); @@ -375,4 +376,13 @@ router.post( }) ); +router.get( + '/activation-bytes', + asyncHandler(async (req, res) => { + logger.debug('get:settings:activation-bytes', { reqId: req.reqId }); + const entries = await activationBytesService.listCachedEntries(); + res.json({ entries }); + }) +); + module.exports = router; diff --git a/backend/src/services/activationBytesService.js b/backend/src/services/activationBytesService.js new file mode 100644 index 0000000..ff04dbf --- /dev/null +++ b/backend/src/services/activationBytesService.js @@ -0,0 +1,114 @@ +const fs = require('fs'); +const crypto = require('crypto'); +const https = require('https'); +const { getDb } = require('../db/database'); +const logger = require('./logger').child('ActivationBytes'); + +const FIXED_KEY = Buffer.from([0x77, 0x21, 0x4d, 0x4b, 0x19, 0x6a, 0x87, 0xcd, 0x52, 0x00, 0x45, 0xfd, 0x20, 0xa5, 0x1d, 0x67]); +const AAX_CHECKSUM_OFFSET = 653; +const AAX_CHECKSUM_LENGTH = 20; +const AUDIBLE_TOOLS_API = 'https://aaxapiserverfunction20220831180001.azurewebsites.net'; + +function sha1(data) { + return crypto.createHash('sha1').update(data).digest(); +} + +function verifyActivationBytes(activationBytesHex, expectedChecksumHex) { + const bytes = Buffer.from(activationBytesHex, 'hex'); + const ik = sha1(Buffer.concat([FIXED_KEY, bytes])); + const iv = sha1(Buffer.concat([FIXED_KEY, ik, bytes])); + const checksum = sha1(Buffer.concat([ik.subarray(0, 16), iv.subarray(0, 16)])); + return checksum.toString('hex') === expectedChecksumHex; +} + +function readAaxChecksum(filePath) { + const fd = fs.openSync(filePath, 'r'); + try { + const buf = Buffer.alloc(AAX_CHECKSUM_LENGTH); + const bytesRead = fs.readSync(fd, buf, 0, AAX_CHECKSUM_LENGTH, AAX_CHECKSUM_OFFSET); + if (bytesRead !== AAX_CHECKSUM_LENGTH) { + throw new Error(`Konnte Checksum nicht lesen (nur ${bytesRead} Bytes)`); + } + return buf.toString('hex'); + } finally { + fs.closeSync(fd); + } +} + +async function lookupCached(checksum) { + const db = await getDb(); + const row = await db.get('SELECT activation_bytes FROM aax_activation_bytes WHERE checksum = ?', checksum); + return row ? row.activation_bytes : null; +} + +async function saveToCache(checksum, activationBytes) { + const db = await getDb(); + await db.run( + 'INSERT OR IGNORE INTO aax_activation_bytes (checksum, activation_bytes) VALUES (?, ?)', + checksum, + activationBytes + ); +} + +function fetchFromApi(checksum) { + return new Promise((resolve, reject) => { + const url = `${AUDIBLE_TOOLS_API}/api/v2/activation/${checksum}`; + https.get(url, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch { + reject(new Error('Ungültige API-Antwort')); + } + }); + }).on('error', reject); + }); +} + +async function resolveActivationBytes(filePath) { + const checksum = readAaxChecksum(filePath); + logger.info({ checksum }, 'AAX Checksum gelesen'); + + // 1. Cache prüfen + const cached = await lookupCached(checksum); + if (cached) { + logger.info({ checksum }, 'Activation Bytes aus lokalem Cache'); + return { checksum, activationBytes: cached, source: 'cache' }; + } + + // 2. Audible-Tools API anfragen + logger.info({ checksum }, 'Frage Audible-Tools API an...'); + let activationBytes = null; + try { + const result = await fetchFromApi(checksum); + if (result.success === true && result.activationBytes) { + if (verifyActivationBytes(result.activationBytes, checksum)) { + activationBytes = result.activationBytes; + logger.info({ checksum, activationBytes }, 'Activation Bytes via API verifiziert'); + } else { + logger.warn({ checksum }, 'API-Antwort konnte nicht verifiziert werden'); + } + } else { + logger.warn({ checksum }, 'Checksum der API unbekannt'); + } + } catch (err) { + logger.warn({ checksum, err: err.message }, 'API nicht erreichbar'); + } + + if (!activationBytes) { + throw new Error(`Activation Bytes für Checksum ${checksum} nicht gefunden (API unbekannt oder nicht erreichbar)`); + } + + // 3. Lokal cachen + await saveToCache(checksum, activationBytes); + return { checksum, activationBytes, source: 'api' }; +} + +async function listCachedEntries() { + const db = await getDb(); + return db.all('SELECT checksum, activation_bytes, created_at FROM aax_activation_bytes ORDER BY created_at DESC'); +} + +module.exports = { resolveActivationBytes, readAaxChecksum, listCachedEntries }; diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index ac158dc..26fadc3 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -25,6 +25,7 @@ const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/pl const { errorToMeta } = require('../utils/errorMeta'); const userPresetService = require('./userPresetService'); const thumbnailService = require('./thumbnailService'); +const activationBytesService = require('./activationBytesService'); const RUNNING_STATES = new Set(['ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING']); const REVIEW_REFRESH_SETTING_PREFIXES = [ @@ -10946,6 +10947,27 @@ class PipelineService extends EventEmitter { stagedRawFilePath }); + // Activation Bytes: erst Cache prüfen, dann einmalig Azure-API anfragen und persistent speichern + try { + const { checksum, activationBytes, source } = await activationBytesService.resolveActivationBytes(stagedRawFilePath); + await historyService.appendLog( + job.id, + 'SYSTEM', + `Activation Bytes aufgelöst (${source}): checksum=${checksum} bytes=${activationBytes}` + ); + logger.info('audiobook:upload:activation-bytes', { jobId: job.id, checksum, activationBytes, source }); + } catch (abError) { + logger.warn('audiobook:upload:activation-bytes-failed', { + jobId: job.id, + error: errorToMeta(abError) + }); + await historyService.appendLog( + job.id, + 'SYSTEM', + `Activation Bytes konnten nicht aufgelöst werden: ${abError?.message || 'unknown'}` + ).catch(() => {}); + } + let detectedAsin = null; let audnexChapters = []; try { diff --git a/db/schema.sql b/db/schema.sql index 8cfd2e3..f705ca4 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -164,6 +164,12 @@ CREATE TABLE user_presets ( CREATE INDEX idx_user_presets_media_type ON user_presets(media_type); +CREATE TABLE IF NOT EXISTS aax_activation_bytes ( + checksum TEXT PRIMARY KEY, + activation_bytes TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + -- ============================================================================= -- Default Settings Seed -- ============================================================================= diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6fa92ed..2551bbc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.10.0-8", + "version": "0.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.10.0-8", + "version": "0.10.1", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 351b90a..47a2894 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.10.0-8", + "version": "0.10.1", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 54bacbe..d6c1672 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -241,6 +241,12 @@ export const api = { forceRefresh: options.forceRefresh }); }, + getActivationBytes(options = {}) { + return requestCachedGet('/settings/activation-bytes', { + ttlMs: 0, + forceRefresh: options.forceRefresh ?? true + }); + }, getHandBrakePresets(options = {}) { return requestCachedGet('/settings/handbrake-presets', { ttlMs: 10 * 60 * 1000, diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index 69cccd6..6b06604 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -184,6 +184,9 @@ export default function SettingsPage() { const [chainEditorErrors, setChainEditorErrors] = useState({}); const [chainDragSource, setChainDragSource] = useState(null); + // Activation Bytes state + const [activationBytes, setActivationBytes] = useState([]); + // User presets state const [userPresets, setUserPresets] = useState([]); const [userPresetsLoading, setUserPresetsLoading] = useState(false); @@ -356,6 +359,8 @@ export default function SettingsPage() { setErrors({}); loadEffectivePaths({ silent: true }); + api.getActivationBytes().then(r => setActivationBytes(Array.isArray(r?.entries) ? r.entries : [])).catch(() => {}); + const presetsPromise = api.getHandBrakePresets(); const scriptsPromise = api.getScripts(); const chainsPromise = api.getScriptChains(); @@ -1738,6 +1743,33 @@ export default function SettingsPage() { + + {activationBytes.length > 0 && ( + + + + + + + + + + + {activationBytes.map((entry) => ( + + + + + + ))} + +
ChecksumActivation BytesGespeichert am
{entry.checksum}{entry.activation_bytes}{new Date(entry.created_at).toLocaleString('de-DE')}
+
+ )} ); } diff --git a/package-lock.json b/package-lock.json index 8ac1612..4ce68fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster", - "version": "0.10.0-8", + "version": "0.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster", - "version": "0.10.0-8", + "version": "0.10.1", "devDependencies": { "concurrently": "^9.1.2" } diff --git a/package.json b/package.json index a3cfa4e..4bc971d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ripster", "private": true, - "version": "0.10.0-8", + "version": "0.10.1", "scripts": { "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev:backend": "npm run dev --prefix backend",