0.10.1-1 AAX Encode
This commit is contained in:
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -7,6 +7,8 @@ const pipelineService = require('../services/pipelineService');
|
||||
const diskDetectionService = require('../services/diskDetectionService');
|
||||
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
||||
const logger = require('../services/logger').child('PIPELINE_ROUTE');
|
||||
const activationBytesService = require('../services/activationBytesService');
|
||||
const { getDb } = require('../db/database');
|
||||
|
||||
const router = express.Router();
|
||||
const audiobookUpload = multer({
|
||||
@@ -155,6 +157,25 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/audiobook/pending-activation',
|
||||
asyncHandler(async (req, res) => {
|
||||
const db = await getDb();
|
||||
// Jobs die eine Checksum haben, aber noch keine Activation Bytes im Cache
|
||||
const pending = await db.all(`
|
||||
SELECT j.id AS jobId, j.aax_checksum AS checksum
|
||||
FROM jobs j
|
||||
WHERE j.aax_checksum IS NOT NULL
|
||||
AND j.status NOT IN ('DONE', 'ERROR', 'CANCELLED')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM aax_activation_bytes ab WHERE ab.checksum = j.aax_checksum
|
||||
)
|
||||
ORDER BY j.created_at DESC
|
||||
`);
|
||||
res.json({ pending });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/audiobook/start/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
@@ -385,4 +385,19 @@ router.get(
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/activation-bytes',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { checksum, activationBytes } = req.body || {};
|
||||
if (!checksum || !activationBytes) {
|
||||
const error = new Error('checksum und activationBytes sind erforderlich');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
logger.debug('post:settings:activation-bytes', { reqId: req.reqId, checksum });
|
||||
const saved = await activationBytesService.saveActivationBytes(checksum, activationBytes);
|
||||
res.json({ success: true, checksum, activationBytes: saved });
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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();
|
||||
@@ -41,69 +39,36 @@ async function lookupCached(checksum) {
|
||||
return row ? row.activation_bytes : null;
|
||||
}
|
||||
|
||||
async function saveToCache(checksum, activationBytes) {
|
||||
async function saveActivationBytes(checksum, activationBytesHex) {
|
||||
const normalized = String(activationBytesHex || '').trim().toLowerCase();
|
||||
if (!/^[0-9a-f]{8}$/.test(normalized)) {
|
||||
throw new Error('Activation Bytes müssen genau 8 Hex-Zeichen (4 Bytes) sein');
|
||||
}
|
||||
if (!verifyActivationBytes(normalized, checksum)) {
|
||||
throw new Error('Activation Bytes passen nicht zur Checksum – bitte nochmals prüfen');
|
||||
}
|
||||
const db = await getDb();
|
||||
await db.run(
|
||||
'INSERT OR IGNORE INTO aax_activation_bytes (checksum, activation_bytes) VALUES (?, ?)',
|
||||
'INSERT OR REPLACE INTO aax_activation_bytes (checksum, activation_bytes) VALUES (?, ?)',
|
||||
checksum,
|
||||
activationBytes
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
logger.info({ checksum, activationBytes: normalized }, 'Activation Bytes manuell gespeichert');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
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' };
|
||||
return { checksum, activationBytes: cached };
|
||||
}
|
||||
|
||||
// 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' };
|
||||
logger.info({ checksum }, 'Keine Activation Bytes im Cache – manuelle Eingabe erforderlich');
|
||||
return { checksum, activationBytes: null };
|
||||
}
|
||||
|
||||
async function listCachedEntries() {
|
||||
@@ -111,4 +76,4 @@ async function listCachedEntries() {
|
||||
return db.all('SELECT checksum, activation_bytes, created_at FROM aax_activation_bytes ORDER BY created_at DESC');
|
||||
}
|
||||
|
||||
module.exports = { resolveActivationBytes, readAaxChecksum, listCachedEntries };
|
||||
module.exports = { resolveActivationBytes, readAaxChecksum, saveActivationBytes, verifyActivationBytes, listCachedEntries };
|
||||
|
||||
@@ -618,6 +618,7 @@ function buildEncodeCommand(ffmpegCommand, inputPath, outputPath, outputFormat =
|
||||
const extra = options && typeof options === 'object' ? options : {};
|
||||
const commonArgs = [
|
||||
'-y',
|
||||
...(extra.activationBytes ? ['-activation_bytes', extra.activationBytes] : []),
|
||||
'-i', inputPath
|
||||
];
|
||||
if (extra.chapterMetadataPath) {
|
||||
@@ -657,11 +658,13 @@ function buildChapterEncodeCommand(
|
||||
formatOptions = {},
|
||||
metadata = {},
|
||||
chapter = {},
|
||||
chapterTotal = 1
|
||||
chapterTotal = 1,
|
||||
options = {}
|
||||
) {
|
||||
const cmd = String(ffmpegCommand || 'ffmpeg').trim() || 'ffmpeg';
|
||||
const format = normalizeOutputFormat(outputFormat);
|
||||
const normalizedOptions = normalizeFormatOptions(format, formatOptions);
|
||||
const extra = options && typeof options === 'object' ? options : {};
|
||||
const safeChapter = normalizeChapterList([chapter], {
|
||||
durationMs: metadata?.durationMs,
|
||||
fallbackTitle: metadata?.title || 'Kapitel',
|
||||
@@ -679,6 +682,7 @@ function buildChapterEncodeCommand(
|
||||
cmd,
|
||||
args: [
|
||||
'-y',
|
||||
...(extra.activationBytes ? ['-activation_bytes', extra.activationBytes] : []),
|
||||
'-i', inputPath,
|
||||
'-ss', formatSecondsArg(safeChapter?.startSeconds),
|
||||
'-t', formatSecondsArg(durationSeconds),
|
||||
|
||||
@@ -10947,25 +10947,22 @@ class PipelineService extends EventEmitter {
|
||||
stagedRawFilePath
|
||||
});
|
||||
|
||||
// Activation Bytes: erst Cache prüfen, dann einmalig Azure-API anfragen und persistent speichern
|
||||
// Activation Bytes: Cache prüfen und Checksum am Job speichern
|
||||
let aaxChecksum = null;
|
||||
let aaxNeedsActivationBytes = false;
|
||||
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 });
|
||||
const abResult = await activationBytesService.resolveActivationBytes(stagedRawFilePath);
|
||||
aaxChecksum = abResult.checksum;
|
||||
await historyService.updateJob(job.id, { aax_checksum: aaxChecksum });
|
||||
if (abResult.activationBytes) {
|
||||
await historyService.appendLog(job.id, 'SYSTEM', `Activation Bytes im Cache gefunden: checksum=${abResult.checksum}`);
|
||||
logger.info('audiobook:upload:activation-bytes', { jobId: job.id, checksum: abResult.checksum, source: 'cache' });
|
||||
} else {
|
||||
aaxNeedsActivationBytes = true;
|
||||
logger.info('audiobook:upload:activation-bytes-needed', { jobId: job.id, checksum: abResult.checksum });
|
||||
}
|
||||
} 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(() => {});
|
||||
logger.warn('audiobook:upload:activation-bytes-failed', { jobId: job.id, error: errorToMeta(abError) });
|
||||
}
|
||||
|
||||
let detectedAsin = null;
|
||||
@@ -11104,7 +11101,8 @@ class PipelineService extends EventEmitter {
|
||||
jobId: job.id,
|
||||
started: false,
|
||||
queued: false,
|
||||
stage: 'READY_TO_START'
|
||||
stage: 'READY_TO_START',
|
||||
...(aaxNeedsActivationBytes ? { needsActivationBytes: true, checksum: aaxChecksum } : {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11401,6 +11399,22 @@ class PipelineService extends EventEmitter {
|
||||
|
||||
let temporaryChapterMetadataPath = null;
|
||||
|
||||
// Activation Bytes für AAX-Dateien aus Cache lesen
|
||||
let encodeActivationBytes = null;
|
||||
if (path.extname(inputPath).toLowerCase() === '.aax') {
|
||||
try {
|
||||
const abResult = await activationBytesService.resolveActivationBytes(inputPath);
|
||||
encodeActivationBytes = abResult.activationBytes || null;
|
||||
if (!encodeActivationBytes) {
|
||||
throw new Error('Activation Bytes nicht im Cache – bitte zuerst über den Upload-Dialog eintragen');
|
||||
}
|
||||
logger.info('audiobook:encode:activation-bytes', { jobId, checksum: abResult.checksum });
|
||||
} catch (abError) {
|
||||
logger.error('audiobook:encode:activation-bytes-failed', { jobId, error: errorToMeta(abError) });
|
||||
throw abError;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let ffmpegRunInfo = null;
|
||||
if (isSplitOutput) {
|
||||
@@ -11451,7 +11465,8 @@ class PipelineService extends EventEmitter {
|
||||
formatOptions,
|
||||
metadata,
|
||||
chapter,
|
||||
outputFiles.length
|
||||
outputFiles.length,
|
||||
{ activationBytes: encodeActivationBytes }
|
||||
);
|
||||
const baseParser = audiobookService.buildProgressParser(chapter?.durationMs || 0);
|
||||
const scaledParser = baseParser
|
||||
@@ -11525,7 +11540,8 @@ class PipelineService extends EventEmitter {
|
||||
formatOptions,
|
||||
{
|
||||
chapterMetadataPath: temporaryChapterMetadataPath,
|
||||
metadata
|
||||
metadata,
|
||||
activationBytes: encodeActivationBytes
|
||||
}
|
||||
);
|
||||
logger.info('audiobook:encode:command', { jobId, cmd: ffmpegConfig.cmd, args: ffmpegConfig.args });
|
||||
|
||||
@@ -47,6 +47,7 @@ CREATE TABLE jobs (
|
||||
encode_plan_json TEXT,
|
||||
encode_input_path TEXT,
|
||||
encode_review_confirmed INTEGER DEFAULT 0,
|
||||
aax_checksum TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_job_id) REFERENCES jobs(id) ON DELETE SET NULL
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -247,6 +247,17 @@ export const api = {
|
||||
forceRefresh: options.forceRefresh ?? true
|
||||
});
|
||||
},
|
||||
async saveActivationBytes(checksum, activationBytes) {
|
||||
const result = await request('/settings/activation-bytes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ checksum, activationBytes })
|
||||
});
|
||||
afterMutationInvalidate(['/settings/activation-bytes']);
|
||||
return result;
|
||||
},
|
||||
getPendingActivation() {
|
||||
return request('/pipeline/audiobook/pending-activation');
|
||||
},
|
||||
getHandBrakePresets(options = {}) {
|
||||
return requestCachedGet('/settings/handbrake-presets', {
|
||||
ttlMs: 10 * 60 * 1000,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { api } from '../api/client';
|
||||
import PipelineStatusCard from '../components/PipelineStatusCard';
|
||||
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
|
||||
@@ -805,6 +806,10 @@ export default function DashboardPage({
|
||||
const [dashboardJobs, setDashboardJobs] = useState([]);
|
||||
const [expandedJobId, setExpandedJobId] = useState(undefined);
|
||||
const [audiobookUploadFile, setAudiobookUploadFile] = useState(null);
|
||||
const [activationBytesDialog, setActivationBytesDialog] = useState({ visible: false, checksum: null, jobId: null });
|
||||
const [activationBytesInput, setActivationBytesInput] = useState('');
|
||||
const [activationBytesBusy, setActivationBytesBusy] = useState(false);
|
||||
const [pendingActivationJobIds, setPendingActivationJobIds] = useState(() => new Set());
|
||||
const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
|
||||
const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
|
||||
const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
|
||||
@@ -912,6 +917,25 @@ export default function DashboardPage({
|
||||
}
|
||||
|
||||
setDashboardJobs(deduped);
|
||||
|
||||
// Prüfen ob Audiobook-Jobs auf Activation Bytes warten
|
||||
try {
|
||||
const { pending } = await api.getPendingActivation();
|
||||
if (Array.isArray(pending) && pending.length > 0) {
|
||||
setPendingActivationJobIds(new Set(pending.map((p) => p.jobId)));
|
||||
// Modal automatisch öffnen wenn noch nicht sichtbar
|
||||
setActivationBytesDialog((prev) => {
|
||||
if (prev.visible) return prev;
|
||||
const first = pending[0];
|
||||
setActivationBytesInput('');
|
||||
return { visible: true, checksum: first.checksum, jobId: first.jobId };
|
||||
});
|
||||
} else {
|
||||
setPendingActivationJobIds(new Set());
|
||||
}
|
||||
} catch (_err) {
|
||||
// ignorieren
|
||||
}
|
||||
} catch (_error) {
|
||||
setDashboardJobs([]);
|
||||
} finally {
|
||||
@@ -1376,8 +1400,12 @@ export default function DashboardPage({
|
||||
}
|
||||
try {
|
||||
const response = await onAudiobookUpload?.(audiobookUploadFile, { startImmediately: false });
|
||||
const uploadedJobId = normalizeJobId(response?.result?.jobId);
|
||||
if (uploadedJobId) {
|
||||
const result = response?.result || {};
|
||||
const uploadedJobId = normalizeJobId(result.jobId);
|
||||
if (result.needsActivationBytes && result.checksum) {
|
||||
setActivationBytesInput('');
|
||||
setActivationBytesDialog({ visible: true, checksum: result.checksum, jobId: uploadedJobId });
|
||||
} else if (uploadedJobId) {
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Audiobook importiert',
|
||||
@@ -1391,11 +1419,42 @@ export default function DashboardPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveActivationBytes = async () => {
|
||||
const { checksum, jobId } = activationBytesDialog;
|
||||
const bytes = activationBytesInput.trim().toLowerCase();
|
||||
setActivationBytesBusy(true);
|
||||
try {
|
||||
await api.saveActivationBytes(checksum, bytes);
|
||||
setPendingActivationJobIds(new Set());
|
||||
setActivationBytesDialog({ visible: false, checksum: null, jobId: null });
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Activation Bytes gespeichert',
|
||||
detail: jobId ? `Job #${jobId} kann jetzt gestartet werden.` : 'Bytes wurden lokal gespeichert.',
|
||||
life: 4000
|
||||
});
|
||||
await loadDashboardJobs();
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setActivationBytesBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAudiobookStart = async (jobId, audiobookConfig) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (!normalizedJobId) {
|
||||
return;
|
||||
}
|
||||
if (pendingActivationJobIds.has(normalizedJobId)) {
|
||||
setActivationBytesInput('');
|
||||
const pending = await api.getPendingActivation().catch(() => ({ pending: [] }));
|
||||
const entry = (pending?.pending || []).find((p) => p.jobId === normalizedJobId);
|
||||
if (entry) {
|
||||
setActivationBytesDialog({ visible: true, checksum: entry.checksum, jobId: normalizedJobId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
const response = await api.startAudiobook(normalizedJobId, audiobookConfig || {});
|
||||
@@ -2604,14 +2663,26 @@ export default function DashboardPage({
|
||||
);
|
||||
}
|
||||
if (isAudiobookJob) {
|
||||
const needsBytes = pendingActivationJobIds.has(jobId);
|
||||
return (
|
||||
<AudiobookConfigPanel
|
||||
pipeline={pipelineForJob}
|
||||
onStart={(config) => handleAudiobookStart(jobId, config)}
|
||||
onCancel={() => handleCancel(jobId, jobState)}
|
||||
onRetry={() => handleRetry(jobId)}
|
||||
busy={busyJobIds.has(jobId)}
|
||||
/>
|
||||
<>
|
||||
{needsBytes && (
|
||||
<div style={{ padding: '0.75rem 1rem', marginBottom: '0.5rem', background: 'var(--yellow-100)', border: '1px solid var(--yellow-400)', borderRadius: '6px', color: 'var(--yellow-900)', fontSize: '0.875rem' }}>
|
||||
<i className="pi pi-lock" style={{ marginRight: '0.5rem' }} />
|
||||
<strong>Activation Bytes fehlen.</strong>{' '}
|
||||
<button type="button" style={{ background: 'none', border: 'none', color: 'var(--primary-color)', cursor: 'pointer', textDecoration: 'underline', padding: 0 }} onClick={() => handleAudiobookStart(jobId, null)}>
|
||||
Jetzt eintragen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<AudiobookConfigPanel
|
||||
pipeline={pipelineForJob}
|
||||
onStart={(config) => handleAudiobookStart(jobId, config)}
|
||||
onCancel={() => handleCancel(jobId, jobState)}
|
||||
onRetry={() => handleRetry(jobId)}
|
||||
busy={busyJobIds.has(jobId) || needsBytes}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -2916,6 +2987,62 @@ export default function DashboardPage({
|
||||
) : null}
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
header="Audible Activation Bytes eintragen"
|
||||
visible={activationBytesDialog.visible}
|
||||
onHide={() => setActivationBytesDialog({ visible: false, checksum: null, jobId: null })}
|
||||
style={{ width: '36rem', maxWidth: '96vw' }}
|
||||
modal
|
||||
footer={(
|
||||
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||
<Button label="Abbrechen" severity="secondary" outlined onClick={() => setActivationBytesDialog({ visible: false, checksum: null, jobId: null })} disabled={activationBytesBusy} />
|
||||
<Button label="Speichern" icon="pi pi-check" onClick={() => void handleSaveActivationBytes()} loading={activationBytesBusy} disabled={activationBytesInput.trim().length !== 8} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.375rem', fontWeight: 600 }}>Checksum (aus der AAX-Datei)</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<InputText
|
||||
value={activationBytesDialog.checksum || ''}
|
||||
readOnly
|
||||
style={{ flex: 1, fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-copy"
|
||||
severity="secondary"
|
||||
outlined
|
||||
tooltip="Kopieren"
|
||||
onClick={() => navigator.clipboard.writeText(activationBytesDialog.checksum || '')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.375rem', fontWeight: 600 }}>Activation Bytes (8 Hex-Zeichen)</label>
|
||||
<InputText
|
||||
value={activationBytesInput}
|
||||
onChange={(e) => setActivationBytesInput(e.target.value)}
|
||||
placeholder="z.B. 1a2b3c4d"
|
||||
style={{ width: '100%', fontFamily: 'monospace' }}
|
||||
maxLength={8}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ background: 'var(--surface-100)', borderRadius: '6px', padding: '0.875rem', fontSize: '0.875rem', lineHeight: 1.6 }}>
|
||||
<strong>So bekommst du die Activation Bytes:</strong>
|
||||
<ol style={{ margin: '0.5rem 0 0 1.25rem', padding: 0 }}>
|
||||
<li>Öffne <strong>audible-tools.kamsker.at</strong></li>
|
||||
<li>Aktiviere den <strong>Experten-Modus</strong></li>
|
||||
<li>Gib die obige Checksum ein</li>
|
||||
<li>Kopiere die zurückgegebenen Activation Bytes hier rein</li>
|
||||
</ol>
|
||||
<p style={{ margin: '0.5rem 0 0', color: 'var(--text-color-secondary)' }}>
|
||||
Die Bytes werden lokal gespeichert und für alle weiteren AAX-Dateien desselben Accounts wiederverwendet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.1-1",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
Reference in New Issue
Block a user