fix: PDF nur Wayback Machine, archive.ph nur Browser-Link
This commit is contained in:
@@ -166,8 +166,8 @@ router.post('/check', authenticate, async (req, res) => {
|
|||||||
router.post('/pdf', authenticate, async (req, res) => {
|
router.post('/pdf', authenticate, async (req, res) => {
|
||||||
const { archiveUrl } = req.body;
|
const { archiveUrl } = req.body;
|
||||||
if (!archiveUrl || typeof archiveUrl !== 'string') return res.status(400).json({ error: 'archiveUrl fehlt' });
|
if (!archiveUrl || typeof archiveUrl !== 'string') return res.status(400).json({ error: 'archiveUrl fehlt' });
|
||||||
if (!/^https?:\/\/(archive\.(is|ph|today|fo|li|vn|md|gg)|web\.archive\.org)\//.test(archiveUrl)) {
|
if (!archiveUrl.startsWith('https://web.archive.org/')) {
|
||||||
return res.status(400).json({ error: 'Ungültige Archiv-URL' });
|
return res.status(400).json({ error: 'PDF nur für Wayback Machine URLs (web.archive.org) verfügbar' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let browser;
|
let browser;
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ function getDomain(url) {
|
|||||||
catch { return url; }
|
catch { return url; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Wayback Machine CDX API – direkt im Browser (CORS: *) ─────────────────
|
// Wayback CDX API – direkt im Browser (CORS: *)
|
||||||
async function checkWayback(targetUrl) {
|
async function checkWayback(targetUrl) {
|
||||||
// Neuesten Snapshot suchen (limit=1, sortiert newest-first via fl+limit)
|
|
||||||
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`;
|
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`;
|
||||||
const res = await fetch(cdxUrl, { signal: AbortSignal.timeout(10000) });
|
const res = await fetch(cdxUrl, { signal: AbortSignal.timeout(12000) });
|
||||||
if (!res.ok) return null;
|
if (!res.ok) throw new Error(`CDX API Fehler: ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// data = [["timestamp"],["20260608201434"]] oder [] wenn nichts gefunden
|
|
||||||
if (!Array.isArray(data) || data.length < 2) return null;
|
if (!Array.isArray(data) || data.length < 2) return null;
|
||||||
const timestamp = data[1]?.[0];
|
const timestamp = data[1]?.[0];
|
||||||
if (!timestamp) return null;
|
if (!timestamp) return null;
|
||||||
@@ -31,48 +29,32 @@ export default function PaywallKiller() {
|
|||||||
const [inputUrl, setInputUrl] = useState('');
|
const [inputUrl, setInputUrl] = useState('');
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
const [waybackUrl, setWaybackUrl] = useState('');
|
const [waybackUrl, setWaybackUrl] = useState('');
|
||||||
const [archiveUrl, setArchiveUrl] = useState('');
|
|
||||||
const [errMsg, setErrMsg] = useState('');
|
const [errMsg, setErrMsg] = useState('');
|
||||||
const [pdfLoading, setPdfLoading] = useState(false);
|
const [pdfLoading, setPdfLoading] = useState(false);
|
||||||
const [pdfTarget, setPdfTarget] = useState('');
|
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
async function handleCheck() {
|
async function handleCheck() {
|
||||||
const url = inputUrl.trim();
|
const url = inputUrl.trim();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
let targetUrl = url.startsWith('http') ? url : 'https://' + url;
|
const targetUrl = url.startsWith('http') ? url : 'https://' + url;
|
||||||
|
|
||||||
setStatus('checking');
|
setStatus('checking');
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
setWaybackUrl('');
|
setWaybackUrl('');
|
||||||
setArchiveUrl('');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// archive.ph URL immer konstruieren (ohne Prüfung – direkt nutzbar)
|
const wb = await checkWayback(targetUrl);
|
||||||
const archivePh = `https://archive.ph/newest/${targetUrl}`;
|
|
||||||
|
|
||||||
// Wayback CDX parallel prüfen
|
|
||||||
let wb = null;
|
|
||||||
try { wb = await checkWayback(targetUrl); } catch { /* ignorieren */ }
|
|
||||||
|
|
||||||
setWaybackUrl(wb || '');
|
setWaybackUrl(wb || '');
|
||||||
setArchiveUrl(archivePh);
|
// Immer "found" zeigen – Wayback-Treffer oder nicht, archive.ph Link immer anbieten
|
||||||
|
setStatus('done');
|
||||||
if (wb) {
|
|
||||||
setStatus('found_both');
|
|
||||||
} else {
|
|
||||||
// Auch ohne Wayback-Treffer archive.ph anbieten
|
|
||||||
setStatus('found_archive');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrMsg(err.message || 'Fehler beim Suchen');
|
setErrMsg(err.message || 'Fehler beim Suchen');
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePdf(url) {
|
async function handlePdf() {
|
||||||
setPdfTarget(url);
|
if (!waybackUrl) return;
|
||||||
setPdfLoading(true);
|
setPdfLoading(true);
|
||||||
|
setErrMsg('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('sk_token');
|
const token = localStorage.getItem('sk_token');
|
||||||
const res = await fetch('/api/tools/paywallkiller/pdf', {
|
const res = await fetch('/api/tools/paywallkiller/pdf', {
|
||||||
@@ -81,7 +63,7 @@ export default function PaywallKiller() {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ archiveUrl: url }),
|
body: JSON.stringify({ archiveUrl: waybackUrl }),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
@@ -100,7 +82,6 @@ export default function PaywallKiller() {
|
|||||||
setErrMsg(err.message || 'PDF konnte nicht erstellt werden');
|
setErrMsg(err.message || 'PDF konnte nicht erstellt werden');
|
||||||
} finally {
|
} finally {
|
||||||
setPdfLoading(false);
|
setPdfLoading(false);
|
||||||
setPdfTarget('');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +89,6 @@ export default function PaywallKiller() {
|
|||||||
setInputUrl('');
|
setInputUrl('');
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
setWaybackUrl('');
|
setWaybackUrl('');
|
||||||
setArchiveUrl('');
|
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
setPdfLoading(false);
|
setPdfLoading(false);
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
@@ -122,24 +102,16 @@ export default function PaywallKiller() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isChecking = status === 'checking';
|
const isChecking = status === 'checking';
|
||||||
const hasResult = status.startsWith('found') || status === 'error';
|
const isDone = status === 'done';
|
||||||
const hasWayback = status === 'found_both' && waybackUrl;
|
const hasWayback = isDone && !!waybackUrl;
|
||||||
const hasArchive = (status === 'found_both' || status === 'found_archive') && archiveUrl;
|
const archivePhUrl = isDone
|
||||||
|
? `https://archive.ph/newest/${(inputUrl.startsWith('http') ? inputUrl : 'https://' + inputUrl).trim()}`
|
||||||
// PDF-Button für eine bestimmte URL
|
: '';
|
||||||
function PdfBtn({ url }) {
|
|
||||||
const loading = pdfLoading && pdfTarget === url;
|
|
||||||
return (
|
|
||||||
<button onClick={() => handlePdf(url)} disabled={pdfLoading}
|
|
||||||
style={{ ...S.btn(C.accent), opacity: pdfLoading ? 0.5 : 1, cursor: pdfLoading ? 'default' : 'pointer', fontSize: 12 }}>
|
|
||||||
{loading ? '⏳ PDF...' : '⬇ PDF'}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||||||
<span style={{ fontSize: 20 }}>🔓</span>
|
<span style={{ fontSize: 20 }}>🔓</span>
|
||||||
@@ -172,7 +144,7 @@ export default function PaywallKiller() {
|
|||||||
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: (isChecking || !inputUrl.trim()) ? 0.45 : 1, cursor: (isChecking || !inputUrl.trim()) ? 'default' : 'pointer' }}>
|
||||||
{isChecking ? '⏳ Suche Archiv...' : '🔍 Archiv suchen'}
|
{isChecking ? '⏳ Suche Archiv...' : '🔍 Archiv suchen'}
|
||||||
</button>
|
</button>
|
||||||
{hasResult && (
|
{(isDone || status === 'error') && (
|
||||||
<button onClick={handleReset}
|
<button onClick={handleReset}
|
||||||
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.14)', borderRadius: 6, padding: '8px 14px', color: 'rgba(255,255,255,0.6)', cursor: 'pointer', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'nowrap' }}>
|
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.14)', borderRadius: 6, padding: '8px 14px', color: 'rgba(255,255,255,0.6)', cursor: 'pointer', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||||
✕ Reset
|
✕ Reset
|
||||||
@@ -181,60 +153,74 @@ export default function PaywallKiller() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Wayback Machine Treffer */}
|
{/* Ergebnisse */}
|
||||||
{hasWayback && (
|
{isDone && (
|
||||||
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 12 }}>
|
<>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
{/* Wayback Machine */}
|
||||||
<span>🏛</span>
|
<div style={{ ...S.card, borderColor: hasWayback ? `${C.success}55` : 'rgba(255,255,255,0.1)', marginBottom: 12 }}>
|
||||||
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>Wayback Machine</span>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: hasWayback ? 10 : 0 }}>
|
||||||
<span style={{ background: `${C.success}20`, border: `1px solid ${C.success}44`, borderRadius: 4, padding: '1px 6px', fontSize: 10, color: C.success, fontFamily: 'monospace' }}>gefunden</span>
|
<span>🏛</span>
|
||||||
</div>
|
<span style={{ color: hasWayback ? C.success : C.muted, fontFamily: 'monospace', fontSize: 12 }}>
|
||||||
<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' }}>
|
Wayback Machine
|
||||||
<a href={waybackUrl} target="_blank" rel="noreferrer" style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>{waybackUrl}</a>
|
</span>
|
||||||
</div>
|
<span style={{
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
background: hasWayback ? `${C.success}20` : 'rgba(255,255,255,0.06)',
|
||||||
<a href={waybackUrl} target="_blank" rel="noreferrer" style={{ ...S.btn(C.success), textDecoration: 'none', fontSize: 12 }}>🌐 Öffnen</a>
|
border: `1px solid ${hasWayback ? C.success + '44' : 'rgba(255,255,255,0.12)'}`,
|
||||||
<PdfBtn url={waybackUrl} />
|
borderRadius: 4, padding: '1px 6px', fontSize: 10,
|
||||||
</div>
|
color: hasWayback ? C.success : C.muted, fontFamily: 'monospace',
|
||||||
</div>
|
}}>
|
||||||
)}
|
{hasWayback ? 'Snapshot gefunden' : 'kein Snapshot'}
|
||||||
|
</span>
|
||||||
{/* archive.ph Link */}
|
|
||||||
{hasArchive && (
|
|
||||||
<div style={{ ...S.card, borderColor: hasWayback ? 'rgba(255,255,255,0.1)' : `${C.success}55`, marginBottom: 12 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
|
||||||
<span>📦</span>
|
|
||||||
<span style={{ color: hasWayback ? 'rgba(255,255,255,0.6)' : C.success, fontFamily: 'monospace', fontSize: 12 }}>archive.ph</span>
|
|
||||||
{!hasWayback && <span style={{ background: `${C.success}20`, border: `1px solid ${C.success}44`, borderRadius: 4, padding: '1px 6px', fontSize: 10, color: C.success, fontFamily: 'monospace' }}>neuester Snapshot</span>}
|
|
||||||
</div>
|
|
||||||
<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={archiveUrl} target="_blank" rel="noreferrer" style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>{archiveUrl}</a>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
|
||||||
<a href={archiveUrl} target="_blank" rel="noreferrer" style={{ ...S.btn(C.success), textDecoration: 'none', fontSize: 12 }}>🌐 Öffnen</a>
|
|
||||||
<PdfBtn url={archiveUrl} />
|
|
||||||
</div>
|
|
||||||
{!hasWayback && (
|
|
||||||
<div style={{ color: C.muted, fontSize: 10, fontFamily: 'monospace', marginTop: 8 }}>
|
|
||||||
Kein Wayback-Snapshot gefunden. archive.ph-Link öffnet direkt den neuesten Snapshot (falls vorhanden).
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* PDF-Ladehinweis */}
|
{hasWayback && (
|
||||||
{pdfLoading && (
|
<>
|
||||||
<div style={{ ...S.card, borderColor: `${C.accent}44`, marginBottom: 12 }}>
|
<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' }}>
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>
|
<a href={waybackUrl} target="_blank" rel="noreferrer"
|
||||||
⏳ Seite wird serverseitig gerendert, bitte warten (15–30s)...
|
style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>{waybackUrl}</a>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
<a href={waybackUrl} target="_blank" rel="noreferrer"
|
||||||
|
style={{ ...S.btn(C.success), textDecoration: 'none', fontSize: 12 }}>
|
||||||
|
🌐 Öffnen
|
||||||
|
</a>
|
||||||
|
<button onClick={handlePdf} disabled={pdfLoading}
|
||||||
|
style={{ ...S.btn(C.accent), opacity: pdfLoading ? 0.5 : 1, cursor: pdfLoading ? 'default' : 'pointer', fontSize: 12 }}>
|
||||||
|
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{pdfLoading && (
|
||||||
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 8 }}>
|
||||||
|
Seite wird serverseitig gerendert (15–30s)...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* archive.ph – nur Öffnen, kein PDF (CAPTCHA-Schutz) */}
|
||||||
|
<div style={{ ...S.card, borderColor: 'rgba(255,255,255,0.1)', marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||||
|
<span>📦</span>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.55)', fontFamily: 'monospace', fontSize: 12 }}>archive.ph</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
<a href={archivePhUrl} target="_blank" rel="noreferrer"
|
||||||
|
style={{ ...S.btn(C.muted), textDecoration: 'none', fontSize: 12 }}>
|
||||||
|
🌐 Öffnen
|
||||||
|
</a>
|
||||||
|
<span style={{ color: C.muted, fontSize: 10, fontFamily: 'monospace' }}>
|
||||||
|
Kein PDF — CAPTCHA-geschützt
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fehler */}
|
{/* Fehler */}
|
||||||
{errMsg && (
|
{errMsg && (
|
||||||
<div style={{ ...S.card, borderColor: `${C.error}44`, marginBottom: 12 }}>
|
<div style={{ ...S.card, borderColor: `${C.error}44`, marginTop: 8 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>{errMsg}</span>
|
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>{errMsg}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,8 +233,9 @@ export default function PaywallKiller() {
|
|||||||
<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 einfügen'],
|
['1', 'URL des gesperrten Artikels einfügen'],
|
||||||
['2', 'Wayback Machine + archive.ph werden geprüft'],
|
['2', 'Wayback Machine wird durchsucht (web.archive.org)'],
|
||||||
['3', 'Im Browser öffnen oder als PDF speichern'],
|
['3', 'Snapshot öffnen oder als PDF speichern'],
|
||||||
|
['4', 'Alternativ: archive.ph im Browser öffnen'],
|
||||||
].map(([num, text]) => (
|
].map(([num, text]) => (
|
||||||
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 8 }}>
|
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'center', 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' }}>{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' }}>{num}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user