0.10.1 Audbile EncBytes
This commit is contained in:
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const pipelineService = require('../services/pipelineService');
|
|||||||
const wsService = require('../services/websocketService');
|
const wsService = require('../services/websocketService');
|
||||||
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
||||||
const userPresetService = require('../services/userPresetService');
|
const userPresetService = require('../services/userPresetService');
|
||||||
|
const activationBytesService = require('../services/activationBytesService');
|
||||||
const logger = require('../services/logger').child('SETTINGS_ROUTE');
|
const logger = require('../services/logger').child('SETTINGS_ROUTE');
|
||||||
|
|
||||||
const router = express.Router();
|
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;
|
module.exports = router;
|
||||||
|
|||||||
114
backend/src/services/activationBytesService.js
Normal file
114
backend/src/services/activationBytesService.js
Normal file
@@ -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 };
|
||||||
@@ -25,6 +25,7 @@ const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/pl
|
|||||||
const { errorToMeta } = require('../utils/errorMeta');
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
const userPresetService = require('./userPresetService');
|
const userPresetService = require('./userPresetService');
|
||||||
const thumbnailService = require('./thumbnailService');
|
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 RUNNING_STATES = new Set(['ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING']);
|
||||||
const REVIEW_REFRESH_SETTING_PREFIXES = [
|
const REVIEW_REFRESH_SETTING_PREFIXES = [
|
||||||
@@ -10946,6 +10947,27 @@ class PipelineService extends EventEmitter {
|
|||||||
stagedRawFilePath
|
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 detectedAsin = null;
|
||||||
let audnexChapters = [];
|
let audnexChapters = [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ CREATE TABLE user_presets (
|
|||||||
|
|
||||||
CREATE INDEX idx_user_presets_media_type ON user_presets(media_type);
|
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
|
-- Default Settings Seed
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -241,6 +241,12 @@ export const api = {
|
|||||||
forceRefresh: options.forceRefresh
|
forceRefresh: options.forceRefresh
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getActivationBytes(options = {}) {
|
||||||
|
return requestCachedGet('/settings/activation-bytes', {
|
||||||
|
ttlMs: 0,
|
||||||
|
forceRefresh: options.forceRefresh ?? true
|
||||||
|
});
|
||||||
|
},
|
||||||
getHandBrakePresets(options = {}) {
|
getHandBrakePresets(options = {}) {
|
||||||
return requestCachedGet('/settings/handbrake-presets', {
|
return requestCachedGet('/settings/handbrake-presets', {
|
||||||
ttlMs: 10 * 60 * 1000,
|
ttlMs: 10 * 60 * 1000,
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ export default function SettingsPage() {
|
|||||||
const [chainEditorErrors, setChainEditorErrors] = useState({});
|
const [chainEditorErrors, setChainEditorErrors] = useState({});
|
||||||
const [chainDragSource, setChainDragSource] = useState(null);
|
const [chainDragSource, setChainDragSource] = useState(null);
|
||||||
|
|
||||||
|
// Activation Bytes state
|
||||||
|
const [activationBytes, setActivationBytes] = useState([]);
|
||||||
|
|
||||||
// User presets state
|
// User presets state
|
||||||
const [userPresets, setUserPresets] = useState([]);
|
const [userPresets, setUserPresets] = useState([]);
|
||||||
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
|
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
|
||||||
@@ -356,6 +359,8 @@ export default function SettingsPage() {
|
|||||||
setErrors({});
|
setErrors({});
|
||||||
loadEffectivePaths({ silent: true });
|
loadEffectivePaths({ silent: true });
|
||||||
|
|
||||||
|
api.getActivationBytes().then(r => setActivationBytes(Array.isArray(r?.entries) ? r.entries : [])).catch(() => {});
|
||||||
|
|
||||||
const presetsPromise = api.getHandBrakePresets();
|
const presetsPromise = api.getHandBrakePresets();
|
||||||
const scriptsPromise = api.getScripts();
|
const scriptsPromise = api.getScripts();
|
||||||
const chainsPromise = api.getScriptChains();
|
const chainsPromise = api.getScriptChains();
|
||||||
@@ -1738,6 +1743,33 @@ export default function SettingsPage() {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabView>
|
</TabView>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{activationBytes.length > 0 && (
|
||||||
|
<Card
|
||||||
|
title="Activation Bytes Cache"
|
||||||
|
subTitle="Lokal gespeicherte AAX-Activation Bytes. Werden beim ersten Upload automatisch über die Audible-Tools API ermittelt."
|
||||||
|
style={{ marginTop: '1.5rem' }}
|
||||||
|
>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '1px solid var(--surface-border)' }}>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem' }}>Checksum</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem' }}>Activation Bytes</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem' }}>Gespeichert am</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{activationBytes.map((entry) => (
|
||||||
|
<tr key={entry.checksum} style={{ borderBottom: '1px solid var(--surface-border)' }}>
|
||||||
|
<td style={{ padding: '0.5rem 0.75rem', color: 'var(--text-color-secondary)' }}>{entry.checksum}</td>
|
||||||
|
<td style={{ padding: '0.5rem 0.75rem', fontWeight: 'bold' }}>{entry.activation_bytes}</td>
|
||||||
|
<td style={{ padding: '0.5rem 0.75rem', color: 'var(--text-color-secondary)' }}>{new Date(entry.created_at).toLocaleString('de-DE')}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.0-8",
|
"version": "0.10.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
|
|||||||
Reference in New Issue
Block a user