feat: auto-archivierung via archive.ph submit + Countdown
This commit is contained in:
@@ -8,10 +8,27 @@ const C = {
|
|||||||
muted: '#6b7280',
|
muted: '#6b7280',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wayback CDX – prüft ob Snapshot existiert (CORS: *)
|
||||||
|
async function checkWayback(targetUrl) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`
|
||||||
|
);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json();
|
||||||
|
if (Array.isArray(data) && data.length >= 2 && data[1]?.[0]) {
|
||||||
|
return `https://web.archive.org/web/${data[1][0]}/${targetUrl}`;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function PaywallKiller() {
|
export default function PaywallKiller() {
|
||||||
const [inputUrl, setInputUrl] = useState('');
|
const [inputUrl, setInputUrl] = useState('');
|
||||||
const [archiveUrl, setArchiveUrl] = useState('');
|
const [archiveUrl, setArchiveUrl] = useState('');
|
||||||
const [done, setDone] = useState(false);
|
// status: idle | checking | archiving | ready | notfound
|
||||||
|
const [status, setStatus] = useState('idle');
|
||||||
|
const [statusMsg, setStatusMsg] = useState('');
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
function getTargetUrl() {
|
function getTargetUrl() {
|
||||||
@@ -19,16 +36,57 @@ export default function PaywallKiller() {
|
|||||||
return url.startsWith('http') ? url : 'https://' + url;
|
return url.startsWith('http') ? url : 'https://' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
async function handleSearch() {
|
||||||
if (!inputUrl.trim()) return;
|
if (!inputUrl.trim()) return;
|
||||||
setArchiveUrl(`https://archive.ph/newest/${getTargetUrl()}`);
|
const targetUrl = getTargetUrl();
|
||||||
setDone(false);
|
setStatus('checking');
|
||||||
|
setStatusMsg('Prüfe ob Archiv existiert...');
|
||||||
|
setArchiveUrl('');
|
||||||
|
|
||||||
|
// Schritt 1: Prüfen ob archive.ph schon einen Snapshot hat
|
||||||
|
// archive.ph /newest/ leitet weiter wenn gefunden, sonst Suchseite
|
||||||
|
// Wir nutzen CDX der Wayback Machine als schnelle Vorab-Prüfung
|
||||||
|
// und bauen immer die archive.ph URL auf
|
||||||
|
const newestUrl = `https://archive.ph/newest/${targetUrl}`;
|
||||||
|
|
||||||
|
// Parallel: Wayback prüfen (liefert uns Info ob überhaupt archiviert)
|
||||||
|
// Aber primär: archive.ph direkt versuchen
|
||||||
|
// Falls "No results" → automatisch archivieren via no-cors submit
|
||||||
|
|
||||||
|
setArchiveUrl(newestUrl);
|
||||||
|
|
||||||
|
// Schritt 2: archive.ph Submit (fire-and-forget, no-cors)
|
||||||
|
// Das triggert die Archivierung falls noch nicht vorhanden
|
||||||
|
setStatus('archiving');
|
||||||
|
setStatusMsg('Archiviere Artikel (falls noch nicht vorhanden)...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// archive.ph Submit via no-cors – kein Response lesbar aber Request geht raus
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('url', targetUrl);
|
||||||
|
formData.append('anyway', '1');
|
||||||
|
fetch('https://archive.ph/submit/', {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'no-cors',
|
||||||
|
body: formData,
|
||||||
|
}).catch(() => {});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Schritt 3: 8s warten dann Artikel öffnen
|
||||||
|
// (archive.ph braucht ~5-15s zum Archivieren)
|
||||||
|
for (let i = 8; i > 0; i--) {
|
||||||
|
setStatusMsg(`Archivierung läuft... öffnet in ${i}s`);
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setInputUrl('');
|
setInputUrl('');
|
||||||
setArchiveUrl('');
|
setArchiveUrl('');
|
||||||
setDone(false);
|
setStatus('idle');
|
||||||
|
setStatusMsg('');
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +98,10 @@ export default function PaywallKiller() {
|
|||||||
<span style={{ fontSize: 20 }}>🔓</span>
|
<span style={{ fontSize: 20 }}>🔓</span>
|
||||||
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>PAYWALL-KILLER</span>
|
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>PAYWALL-KILLER</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>archive.ph · Als PDF speichern</div>
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginBottom: 10 }}>archive.ph · Als PDF speichern</div>
|
||||||
|
|
||||||
{/* Getestet mit */}
|
{/* Getestet-Badges */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 10, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
||||||
<span style={{ color: 'rgba(255,255,255,0.25)', fontFamily: 'monospace', fontSize: 9, letterSpacing: 0.5 }}>GETESTET MIT</span>
|
<span style={{ color: 'rgba(255,255,255,0.25)', fontFamily: 'monospace', fontSize: 9, letterSpacing: 0.5 }}>GETESTET MIT</span>
|
||||||
{[
|
{[
|
||||||
{ name: 'waz.de', favicon: 'https://www.google.com/s2/favicons?domain=waz.de&sz=16' },
|
{ name: 'waz.de', favicon: 'https://www.google.com/s2/favicons?domain=waz.de&sz=16' },
|
||||||
@@ -65,26 +123,42 @@ 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' && handleSearch()}
|
onKeyDown={e => e.key === 'Enter' && status === 'idle' && handleSearch()}
|
||||||
placeholder="URL 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={handleSearch} disabled={!inputUrl.trim()}
|
{status === 'idle' ? (
|
||||||
style={{ ...S.btn(C.accent), flex: 1, opacity: !inputUrl.trim() ? 0.45 : 1, cursor: !inputUrl.trim() ? 'default' : 'pointer' }}>
|
<button onClick={handleSearch} disabled={!inputUrl.trim()}
|
||||||
🔍 Archiv suchen
|
style={{ ...S.btn(C.accent), flex: 1, opacity: !inputUrl.trim() ? 0.45 : 1, cursor: !inputUrl.trim() ? 'default' : 'pointer' }}>
|
||||||
</button>
|
🔍 Archiv suchen
|
||||||
{archiveUrl && (
|
</button>
|
||||||
|
) : (
|
||||||
<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 }}>
|
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 }}>
|
||||||
✕
|
✕ Reset
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Lade-Status */}
|
||||||
|
{(status === 'checking' || status === 'archiving') && (
|
||||||
|
<div style={{ ...S.card, borderColor: `${C.accent}44`, marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<span style={{ fontSize: 16 }}>⏳</span>
|
||||||
|
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 12 }}>{statusMsg}</span>
|
||||||
|
</div>
|
||||||
|
{status === 'archiving' && (
|
||||||
|
<div style={{ color: C.muted, fontSize: 10, fontFamily: 'monospace', marginTop: 8, lineHeight: 1.5 }}>
|
||||||
|
archive.ph archiviert den Artikel. Das dauert einen Moment. Anschließend wird er automatisch geöffnet.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Ergebnis */}
|
{/* Ergebnis */}
|
||||||
{archiveUrl && (
|
{status === 'ready' && archiveUrl && (
|
||||||
<div style={{ ...S.card, borderColor: `${C.accent}55`, marginBottom: 12 }}>
|
<div style={{ ...S.card, borderColor: `${C.accent}55`, marginBottom: 12 }}>
|
||||||
<div style={{ ...S.head, marginBottom: 12 }}>ARTIKEL ÖFFNEN & ALS PDF SPEICHERN</div>
|
<div style={{ ...S.head, marginBottom: 12 }}>ARTIKEL ÖFFNEN & ALS PDF SPEICHERN</div>
|
||||||
|
|
||||||
@@ -107,7 +181,7 @@ export default function PaywallKiller() {
|
|||||||
<span style={{ background: `${C.success}20`, border: `1px solid ${C.success}44`, borderRadius: '50%', width: 22, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, color: C.success, fontFamily: 'monospace', flexShrink: 0, marginTop: 1 }}>2</span>
|
<span style={{ background: `${C.success}20`, border: `1px solid ${C.success}44`, borderRadius: '50%', width: 22, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, color: C.success, fontFamily: 'monospace', flexShrink: 0, marginTop: 1 }}>2</span>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 11, marginBottom: 8 }}>
|
<div style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 11, marginBottom: 8 }}>
|
||||||
Wenn der Artikel sichtbar ist, als PDF speichern:
|
Als PDF speichern:
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
{[
|
{[
|
||||||
@@ -132,13 +206,13 @@ export default function PaywallKiller() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Idle-Info */}
|
{/* Idle-Info */}
|
||||||
{!archiveUrl && (
|
{status === 'idle' && (
|
||||||
<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 einfügen'],
|
['1', 'URL des gesperrten Artikels einfügen'],
|
||||||
['2', 'Artikel auf archive.ph öffnen'],
|
['2', 'Archiv wird automatisch gesucht und erstellt falls nötig'],
|
||||||
['3', 'CAPTCHA einmalig lösen (danach dauerhaft gespeichert)'],
|
['3', 'Artikel auf archive.ph öffnen – CAPTCHA einmalig lösen'],
|
||||||
['4', 'Über Drucken-Funktion als PDF speichern'],
|
['4', 'Über Drucken-Funktion 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 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user