feat: Wayback auto-save wenn kein Snapshot existiert
This commit is contained in:
@@ -231,7 +231,8 @@ router.post('/pdf', authenticate, async (req, res) => {
|
|||||||
|
|
||||||
|
|
||||||
// ── GET /api/tools/paywallkiller/wayback?url=... ──────────────────────────
|
// ── GET /api/tools/paywallkiller/wayback?url=... ──────────────────────────
|
||||||
// Proxyt die Wayback CDX API – umgeht CORS/CSP Probleme im Browser
|
// 1. Sucht vorhandenen Snapshot via CDX API
|
||||||
|
// 2. Falls keiner → erstellt neuen Snapshot via Save API
|
||||||
router.get('/wayback', authenticate, async (req, res) => {
|
router.get('/wayback', authenticate, async (req, res) => {
|
||||||
const { url } = req.query;
|
const { url } = req.query;
|
||||||
if (!url || typeof url !== 'string') return res.status(400).json({ error: 'url Parameter fehlt' });
|
if (!url || typeof url !== 'string') return res.status(400).json({ error: 'url Parameter fehlt' });
|
||||||
@@ -239,24 +240,62 @@ router.get('/wayback', authenticate, async (req, res) => {
|
|||||||
let targetUrl = url.trim();
|
let targetUrl = url.trim();
|
||||||
if (!targetUrl.startsWith('http')) targetUrl = 'https://' + targetUrl;
|
if (!targetUrl.startsWith('http')) targetUrl = 'https://' + targetUrl;
|
||||||
|
|
||||||
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`;
|
// ── Schritt 1: Vorhandenen Snapshot suchen ──────────────────────────────
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fetchPage(cdxUrl);
|
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`;
|
||||||
if (result.statusCode !== 200) {
|
const cdxResult = await fetchPage(cdxUrl);
|
||||||
return res.status(502).json({ error: `Wayback API Fehler: ${result.statusCode}` });
|
if (cdxResult.statusCode === 200 && cdxResult.body) {
|
||||||
|
let data;
|
||||||
|
try { data = JSON.parse(cdxResult.body); } catch { data = null; }
|
||||||
|
if (Array.isArray(data) && data.length >= 2 && data[1]?.[0]) {
|
||||||
|
const timestamp = data[1][0];
|
||||||
|
return res.json({ found: true, waybackUrl: `https://web.archive.org/web/${timestamp}/${targetUrl}`, created: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let data;
|
} catch { /* weiter zu Save API */ }
|
||||||
try { data = JSON.parse(result.body); } catch { return res.status(502).json({ error: 'Ungültige Wayback-Antwort' }); }
|
|
||||||
|
|
||||||
// data = [["timestamp"],["20260608201434"]] oder [] wenn nichts gefunden
|
// ── Schritt 2: Keinen Snapshot gefunden → neu erstellen ─────────────────
|
||||||
if (!Array.isArray(data) || data.length < 2 || !data[1]?.[0]) {
|
return res.json({ found: false, canSave: true });
|
||||||
return res.json({ found: false });
|
});
|
||||||
|
|
||||||
|
// ── POST /api/tools/paywallkiller/wayback-save ────────────────────────────
|
||||||
|
// Erstellt einen neuen Wayback-Snapshot und wartet auf das Ergebnis
|
||||||
|
router.post('/wayback-save', authenticate, async (req, res) => {
|
||||||
|
const { url } = req.body;
|
||||||
|
if (!url || typeof url !== 'string') return res.status(400).json({ error: 'url fehlt' });
|
||||||
|
|
||||||
|
let targetUrl = url.trim();
|
||||||
|
if (!targetUrl.startsWith('http')) targetUrl = 'https://' + targetUrl;
|
||||||
|
|
||||||
|
// Save API aufrufen: GET https://web.archive.org/save/URL
|
||||||
|
// Antwort: 302 Redirect auf https://web.archive.org/web/TIMESTAMP/URL
|
||||||
|
try {
|
||||||
|
const saveUrl = `https://web.archive.org/save/${targetUrl}`;
|
||||||
|
const saveResult = await fetchPage(saveUrl); // fetchPage folgt Redirects
|
||||||
|
|
||||||
|
// Nach Redirect landet man auf web.archive.org/web/TIMESTAMP/URL
|
||||||
|
const final = saveResult.finalUrl || '';
|
||||||
|
const match = final.match(/web\.archive\.org\/web\/(\d{14})\//);
|
||||||
|
if (match) {
|
||||||
|
return res.json({ success: true, waybackUrl: final, timestamp: match[1] });
|
||||||
}
|
}
|
||||||
const timestamp = data[1][0];
|
|
||||||
return res.json({ found: true, waybackUrl: `https://web.archive.org/web/${timestamp}/${targetUrl}` });
|
// Fallback: CDX nochmal prüfen (Save kann asynchron sein)
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`;
|
||||||
|
const cdxResult = await fetchPage(cdxUrl);
|
||||||
|
if (cdxResult.statusCode === 200) {
|
||||||
|
let data;
|
||||||
|
try { data = JSON.parse(cdxResult.body); } catch { data = null; }
|
||||||
|
if (Array.isArray(data) && data.length >= 2 && data[1]?.[0]) {
|
||||||
|
const timestamp = data[1][0];
|
||||||
|
return res.json({ success: true, waybackUrl: `https://web.archive.org/web/${timestamp}/${targetUrl}`, timestamp });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(502).json({ error: 'Snapshot konnte nicht erstellt werden. Die Seite blockiert möglicherweise die Wayback Machine.' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res.status(500).json({ error: 'Fehler: ' + err.message });
|
return res.status(500).json({ error: 'Save fehlgeschlagen: ' + err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const C = {
|
|||||||
success: '#4ade80',
|
success: '#4ade80',
|
||||||
error: '#f87171',
|
error: '#f87171',
|
||||||
muted: '#6b7280',
|
muted: '#6b7280',
|
||||||
|
info: '#60a5fa',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDomain(url) {
|
function getDomain(url) {
|
||||||
@@ -15,29 +16,59 @@ function getDomain(url) {
|
|||||||
|
|
||||||
export default function PaywallKiller() {
|
export default function PaywallKiller() {
|
||||||
const [inputUrl, setInputUrl] = useState('');
|
const [inputUrl, setInputUrl] = useState('');
|
||||||
|
// status: idle | checking | saving | done | error
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
const [waybackUrl, setWaybackUrl] = useState('');
|
const [waybackUrl, setWaybackUrl] = useState('');
|
||||||
|
const [wasCreated, setWasCreated] = useState(false);
|
||||||
const [errMsg, setErrMsg] = useState('');
|
const [errMsg, setErrMsg] = useState('');
|
||||||
const [pdfLoading, setPdfLoading] = useState(false);
|
const [pdfLoading, setPdfLoading] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
function getTargetUrl() {
|
||||||
|
const url = inputUrl.trim();
|
||||||
|
return url.startsWith('http') ? url : 'https://' + url;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCheck() {
|
async function handleCheck() {
|
||||||
const url = inputUrl.trim();
|
const url = inputUrl.trim();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const targetUrl = url.startsWith('http') ? url : 'https://' + url;
|
|
||||||
setStatus('checking');
|
setStatus('checking');
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
setWaybackUrl('');
|
setWaybackUrl('');
|
||||||
|
setWasCreated(false);
|
||||||
try {
|
try {
|
||||||
const data = await api(`/tools/paywallkiller/wayback?url=${encodeURIComponent(targetUrl)}`, { method: 'GET' });
|
const data = await api(`/tools/paywallkiller/wayback?url=${encodeURIComponent(getTargetUrl())}`, { method: 'GET' });
|
||||||
setWaybackUrl(data.found ? data.waybackUrl : '');
|
if (data.found) {
|
||||||
setStatus('done');
|
setWaybackUrl(data.waybackUrl);
|
||||||
|
setStatus('done');
|
||||||
|
} else {
|
||||||
|
// Kein Snapshot → direkt Save starten
|
||||||
|
await handleSave();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrMsg(err.message || 'Fehler beim Suchen');
|
setErrMsg(err.message || 'Fehler beim Suchen');
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setStatus('saving');
|
||||||
|
setErrMsg('');
|
||||||
|
try {
|
||||||
|
const data = await api('/tools/paywallkiller/wayback-save', { body: { url: getTargetUrl() } });
|
||||||
|
if (data.success) {
|
||||||
|
setWaybackUrl(data.waybackUrl);
|
||||||
|
setWasCreated(true);
|
||||||
|
setStatus('done');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'Snapshot konnte nicht erstellt werden');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setErrMsg(err.message || 'Snapshot-Erstellung fehlgeschlagen');
|
||||||
|
setStatus('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePdf() {
|
async function handlePdf() {
|
||||||
if (!waybackUrl) return;
|
if (!waybackUrl) return;
|
||||||
setPdfLoading(true);
|
setPdfLoading(true);
|
||||||
@@ -76,16 +107,18 @@ export default function PaywallKiller() {
|
|||||||
setInputUrl('');
|
setInputUrl('');
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
setWaybackUrl('');
|
setWaybackUrl('');
|
||||||
|
setWasCreated(false);
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
setPdfLoading(false);
|
setPdfLoading(false);
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isChecking = status === 'checking';
|
const isChecking = status === 'checking';
|
||||||
|
const isSaving = status === 'saving';
|
||||||
|
const isBusy = isChecking || isSaving;
|
||||||
const isDone = status === 'done';
|
const isDone = status === 'done';
|
||||||
const hasWayback = isDone && !!waybackUrl;
|
const archivePhUrl = inputUrl.trim()
|
||||||
const archivePhUrl = isDone
|
? `https://archive.ph/newest/${getTargetUrl()}`
|
||||||
? `https://archive.ph/newest/${(inputUrl.startsWith('http') ? inputUrl : 'https://' + inputUrl).trim()}`
|
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -109,14 +142,14 @@ export default function PaywallKiller() {
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={inputUrl}
|
value={inputUrl}
|
||||||
onChange={e => setInputUrl(e.target.value)}
|
onChange={e => setInputUrl(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && !isChecking && handleCheck()}
|
onKeyDown={e => e.key === 'Enter' && !isBusy && handleCheck()}
|
||||||
placeholder="URL hier einfügen (Long-Press → Einfügen)"
|
placeholder="URL einfügen (Long-Press → Einfügen)"
|
||||||
style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 10 }}
|
style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 10 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<button onClick={handleCheck} disabled={isChecking || !inputUrl.trim()}
|
<button onClick={handleCheck} disabled={isBusy || !inputUrl.trim()}
|
||||||
style={{ ...S.btn(C.accent), flex: 1, opacity: (isChecking || !inputUrl.trim()) ? 0.45 : 1, cursor: (isChecking || !inputUrl.trim()) ? 'default' : 'pointer' }}>
|
style={{ ...S.btn(C.accent), flex: 1, opacity: (isBusy || !inputUrl.trim()) ? 0.45 : 1, cursor: (isBusy || !inputUrl.trim()) ? 'default' : 'pointer' }}>
|
||||||
{isChecking ? '⏳ Suche in Wayback Machine...' : '🔍 Archiv suchen'}
|
{isChecking ? '⏳ Suche Archiv...' : isSaving ? '⏳ Erstelle Snapshot...' : '🔍 Archiv suchen'}
|
||||||
</button>
|
</button>
|
||||||
{(isDone || status === 'error') && (
|
{(isDone || status === 'error') && (
|
||||||
<button onClick={handleReset}
|
<button onClick={handleReset}
|
||||||
@@ -125,49 +158,44 @@ export default function PaywallKiller() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isSaving && (
|
||||||
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 10, lineHeight: 1.5 }}>
|
||||||
|
Kein Snapshot gefunden – die Wayback Machine archiviert den Artikel jetzt. Das dauert 15–30 Sekunden...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ergebnisse */}
|
{/* Ergebnis: Wayback Machine */}
|
||||||
{isDone && (
|
{isDone && (
|
||||||
<>
|
<>
|
||||||
{/* Wayback Machine */}
|
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 12 }}>
|
||||||
<div style={{ ...S.card, borderColor: hasWayback ? `${C.success}55` : 'rgba(255,255,255,0.1)', marginBottom: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: hasWayback ? 10 : 0 }}>
|
|
||||||
<span>🏛</span>
|
<span>🏛</span>
|
||||||
<span style={{ color: hasWayback ? C.success : C.muted, fontFamily: 'monospace', fontSize: 12 }}>Wayback Machine</span>
|
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>Wayback Machine</span>
|
||||||
<span style={{
|
<span style={{ background: `${C.success}20`, border: `1px solid ${C.success}44`, borderRadius: 4, padding: '1px 6px', fontSize: 10, color: C.success, fontFamily: 'monospace' }}>
|
||||||
background: hasWayback ? `${C.success}20` : 'rgba(255,255,255,0.06)',
|
{wasCreated ? 'neu archiviert ✨' : 'Snapshot gefunden'}
|
||||||
border: `1px solid ${hasWayback ? C.success + '44' : 'rgba(255,255,255,0.12)'}`,
|
|
||||||
borderRadius: 4, padding: '1px 6px', fontSize: 10,
|
|
||||||
color: hasWayback ? C.success : C.muted, fontFamily: 'monospace',
|
|
||||||
}}>
|
|
||||||
{hasWayback ? 'Snapshot gefunden' : 'kein Snapshot'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{hasWayback && (
|
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 6, padding: '7px 10px', marginBottom: 10, wordBreak: 'break-all' }}>
|
||||||
<>
|
<a href={waybackUrl} target="_blank" rel="noreferrer"
|
||||||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 6, padding: '7px 10px', marginBottom: 10, wordBreak: 'break-all' }}>
|
style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>{waybackUrl}</a>
|
||||||
<a href={waybackUrl} target="_blank" rel="noreferrer"
|
</div>
|
||||||
style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>{waybackUrl}</a>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
</div>
|
<a href={waybackUrl} target="_blank" rel="noreferrer"
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
style={{ ...S.btn(C.success), textDecoration: 'none', fontSize: 12 }}>🌐 Öffnen</a>
|
||||||
<a href={waybackUrl} target="_blank" rel="noreferrer"
|
<button onClick={handlePdf} disabled={pdfLoading}
|
||||||
style={{ ...S.btn(C.success), textDecoration: 'none', fontSize: 12 }}>🌐 Öffnen</a>
|
style={{ ...S.btn(C.accent), opacity: pdfLoading ? 0.5 : 1, cursor: pdfLoading ? 'default' : 'pointer', fontSize: 12 }}>
|
||||||
<button onClick={handlePdf} disabled={pdfLoading}
|
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF'}
|
||||||
style={{ ...S.btn(C.accent), opacity: pdfLoading ? 0.5 : 1, cursor: pdfLoading ? 'default' : 'pointer', fontSize: 12 }}>
|
</button>
|
||||||
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF'}
|
</div>
|
||||||
</button>
|
{pdfLoading && (
|
||||||
</div>
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 8 }}>
|
||||||
{pdfLoading && (
|
Seite wird serverseitig gerendert (15–30s)...
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 8 }}>
|
</div>
|
||||||
Seite wird serverseitig gerendert (15–30s)...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* archive.ph */}
|
{/* archive.ph – immer anzeigen, nur Öffnen */}
|
||||||
<div style={{ ...S.card, borderColor: 'rgba(255,255,255,0.1)', marginBottom: 12 }}>
|
<div style={{ ...S.card, borderColor: 'rgba(255,255,255,0.1)', marginBottom: 12 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||||
<span>📦</span>
|
<span>📦</span>
|
||||||
@@ -197,10 +225,10 @@ export default function PaywallKiller() {
|
|||||||
<div style={{ ...S.card }}>
|
<div style={{ ...S.card }}>
|
||||||
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
||||||
{[
|
{[
|
||||||
['1', 'URL des gesperrten Artikels ins Textfeld einfügen (Long-Press → Einfügen)'],
|
['1', 'URL des gesperrten Artikels einfügen (Long-Press → Einfügen)'],
|
||||||
['2', 'Wayback Machine (web.archive.org) wird serverseitig durchsucht'],
|
['2', 'Vorhandenen Snapshot in der Wayback Machine suchen'],
|
||||||
['3', 'Snapshot öffnen oder als PDF speichern'],
|
['3', 'Falls keiner existiert: Artikel wird automatisch archiviert'],
|
||||||
['4', 'Alternativ: archive.ph direkt im Browser öffnen'],
|
['4', 'Snapshot öffnen oder als PDF speichern'],
|
||||||
].map(([num, text]) => (
|
].map(([num, text]) => (
|
||||||
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 8 }}>
|
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 8 }}>
|
||||||
<span style={{ background: `${C.accent}20`, border: `1px solid ${C.accent}44`, borderRadius: '50%', width: 20, height: 20, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, color: C.accent, fontFamily: 'monospace', marginTop: 2 }}>{num}</span>
|
<span style={{ background: `${C.accent}20`, border: `1px solid ${C.accent}44`, borderRadius: '50%', width: 20, height: 20, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, color: C.accent, fontFamily: 'monospace', marginTop: 2 }}>{num}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user