Initial commit: NZB → SABnzbd Chrome extensie

This commit is contained in:
Eddy de Vink 2026-06-13 09:14:37 +02:00
commit 0034676f52
11 changed files with 535 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
*.log

29
README.md Normal file
View file

@ -0,0 +1,29 @@
# NZB → SABnzbd Chrome Extensie
Klik op een NZB-link op elke website → wordt automatisch naar jouw SABnzbd gestuurd.
## Installatie
1. Open Chrome en ga naar `chrome://extensions/`
2. Zet **"Ontwikkelaarsmodus"** aan (rechtsboven)
3. Klik op **"Uitgepakte extensie laden"**
4. Selecteer de map `nzb-sabnzbd-extension`
## Configuratie
1. Klik op het extensie-icoon in de toolbar
2. Vul de **SABnzbd host** in (bijv. `http://localhost:8080`)
3. Vul je **API-sleutel** in (te vinden in SABnzbd → Config → General → API Key)
4. Optioneel: stel een **categorie** in
5. Klik op **"Test verbinding"** om te controleren
6. Klik op **"Opslaan"**
## Werking
De extensie onderschept klikken op links die lijken op NZB-downloadlinks, zoals:
- URLs met `?page=getnzb`
- URLs met `?action=display`
- URLs met `.nzb` in het pad
- URLs met `/getnzb/` in het pad
Vervolgens stuurt de extensie de URL via de SABnzbd API (`addurl`) naar jouw SABnzbd-installatie.

61
background.js Normal file
View file

@ -0,0 +1,61 @@
// background.js — luistert naar berichten van content.js en stuurt NZB naar SABnzbd
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "SEND_NZB") {
handleNzbUrl(message.url).then(sendResponse);
return true; // async response
}
});
async function handleNzbUrl(nzbUrl) {
const config = await chrome.storage.sync.get(["sabHost", "sabApiKey", "sabCategory"]);
const { sabHost, sabApiKey, sabCategory } = config;
if (!sabHost || !sabApiKey) {
showNotification("❌ Configuratie ontbreekt", "Stel de SABnzbd host en API-sleutel in via de extensie-popup.");
return { success: false, error: "Geen configuratie" };
}
// Bouw de SABnzbd API URL
// SABnzbd ondersteunt 'addurl' om een externe NZB-URL toe te voegen
const host = sabHost.replace(/\/$/, ""); // verwijder trailing slash
const category = sabCategory || "Default";
const params = new URLSearchParams({
output: "json",
apikey: sabApiKey,
mode: "addurl",
name: nzbUrl,
cat: category
});
const apiUrl = `${host}/api?${params.toString()}`;
try {
const response = await fetch(apiUrl, { method: "GET" });
const data = await response.json();
if (data.status === true) {
showNotification("✅ NZB toegevoegd!", "De download is toegevoegd aan SABnzbd.");
return { success: true };
} else {
const errMsg = data.error || "Onbekende fout van SABnzbd";
showNotification("❌ Fout van SABnzbd", errMsg);
return { success: false, error: errMsg };
}
} catch (err) {
const msg = `Kan SABnzbd niet bereiken: ${err.message}`;
showNotification("❌ Verbindingsfout", msg);
return { success: false, error: msg };
}
}
function showNotification(title, message) {
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon48.png",
title,
message
});
}

27
claude.io.md Normal file
View file

@ -0,0 +1,27 @@
kan ik een chrome extensie maken dat als ik op een .nzb link klik met bijvoorbeeld als url https://clubnzb.com/?page=getnzb&action=display&messageid=p2N86Yd7LPkjHEsagNMeQ%40spot.net
dat de url dan op mijn sabnzbd api wordt gepost
Ja, dat kan zeker! Je kunt een Chrome extensie bouwen die NZB-links onderschept en direct naar je SABnzbd API stuurt. Ik maak dit voor je — inclusief een nette popup voor de instellingen.De extensie is klaar! Zo installeer je hem:
**Stap 1 — Uitpakken**
Pak de ZIP uit naar een map op je computer.
**Stap 2 — Laden in Chrome**
1. Ga naar `chrome://extensions/`
2. Zet **"Ontwikkelaarsmodus"** aan (rechtsboven)
3. Klik op **"Uitgepakte extensie laden"**
4. Selecteer de uitgepakte map `nzb-sabnzbd-extension`
**Stap 3 — Configureren**
1. Klik op het extensie-icoon in je toolbar
2. Vul je SABnzbd host in, bijv. `http://localhost:8080`
3. Vul je API-sleutel in (SABnzbd → Config → General → API Key)
4. Klik op **"Test verbinding"** om te checken
5. Klik op **"Opslaan"**
**Hoe het werkt:**
- De extensie onderschept klikken op links met o.a. `?page=getnzb`, `?action=display`, of `.nzb` in de URL
- De NZB-URL wordt via de SABnzbd `addurl` API direct naar je downloader gestuurd
- Je krijgt een kleine toast-melding op de pagina of het gelukt is
- Je SABnzbd hoeft niet lokaal te draaien — een NAS of VPS werkt ook gewoon

76
content.js Normal file
View file

@ -0,0 +1,76 @@
// content.js — onderschept klikken op NZB-links
// Patronen die duiden op een NZB-downloadlink
const NZB_PATTERNS = [
/[?&]action=display/i,
/[?&]page=getnzb/i,
/\.nzb(\?|$)/i,
/\/getnzb\//i,
/\/download.*nzb/i,
/nzb.*download/i
];
function isNzbLink(url) {
return NZB_PATTERNS.some(pattern => pattern.test(url));
}
function showToast(message, success = true) {
const existing = document.getElementById("nzb-sabnzbd-toast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.id = "nzb-sabnzbd-toast";
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
z-index: 999999;
background: ${success ? "#1a1a2e" : "#3d0000"};
color: ${success ? "#7ee8a2" : "#ff6b6b"};
border: 1px solid ${success ? "#7ee8a2" : "#ff6b6b"};
border-radius: 8px;
padding: 12px 20px;
font-family: system-ui, sans-serif;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
transition: opacity 0.3s ease;
opacity: 1;
max-width: 320px;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 300);
}, 3500);
}
document.addEventListener("click", async (e) => {
// Zoek het dichtstbijzijnde <a>-element
const anchor = e.target.closest("a");
if (!anchor) return;
const href = anchor.href;
if (!href) return;
if (!isNzbLink(href)) return;
// Onderschep de klik
e.preventDefault();
e.stopPropagation();
showToast("⏳ NZB wordt verstuurd naar SABnzbd...", true);
const response = await chrome.runtime.sendMessage({
type: "SEND_NZB",
url: href
});
if (response?.success) {
showToast("✅ Toegevoegd aan SABnzbd!", true);
} else {
showToast(`❌ Mislukt: ${response?.error || "Onbekende fout"}`, false);
}
}, true); // useCapture = true zodat we vóór andere handlers zitten

BIN
icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

BIN
icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

BIN
icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

32
manifest.json Normal file
View file

@ -0,0 +1,32 @@
{
"manifest_version": 3,
"name": "NZB → SABnzbd",
"version": "1.0.0",
"description": "Stuur NZB-links automatisch naar SABnzbd via de API.",
"permissions": [
"activeTab",
"storage",
"notifications"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"action": {
"default_popup": "popup.html",
"default_title": "NZB → SABnzbd instellingen"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}

227
popup.html Normal file
View file

@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8" />
<title>NZB → SABnzbd</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
width: 320px;
font-family: 'Segoe UI', system-ui, sans-serif;
background: #0f0f1a;
color: #e2e2f0;
}
.header {
background: linear-gradient(135deg, #1a1a3e 0%, #0f0f1a 100%);
padding: 20px 20px 16px;
border-bottom: 1px solid #2a2a4a;
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #7ee8a2, #3abde0);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.header-text h1 {
font-size: 15px;
font-weight: 700;
color: #fff;
letter-spacing: 0.02em;
}
.header-text p {
font-size: 11px;
color: #7a7a9a;
margin-top: 2px;
}
.body {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #7a7a9a;
}
input {
background: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 6px;
color: #e2e2f0;
font-size: 13px;
padding: 9px 12px;
outline: none;
transition: border-color 0.15s;
width: 100%;
}
input:focus {
border-color: #7ee8a2;
}
input::placeholder {
color: #3a3a5a;
}
.hint {
font-size: 11px;
color: #4a4a6a;
margin-top: 2px;
}
.divider {
height: 1px;
background: #1a1a2e;
}
.btn-row {
display: flex;
gap: 8px;
}
button {
flex: 1;
padding: 10px;
border-radius: 6px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
#saveBtn {
background: linear-gradient(135deg, #7ee8a2, #3abde0);
color: #0f0f1a;
}
#saveBtn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
#testBtn {
background: #1a1a2e;
color: #7a7a9a;
border: 1px solid #2a2a4a;
}
#testBtn:hover {
border-color: #7ee8a2;
color: #7ee8a2;
}
#status {
font-size: 12px;
padding: 8px 12px;
border-radius: 6px;
display: none;
text-align: center;
}
#status.success {
display: block;
background: #0a2e18;
color: #7ee8a2;
border: 1px solid #1a5a30;
}
#status.error {
display: block;
background: #2e0a0a;
color: #ff6b6b;
border: 1px solid #5a1a1a;
}
.footer {
padding: 12px 20px;
border-top: 1px solid #1a1a2e;
font-size: 11px;
color: #3a3a5a;
text-align: center;
}
.status-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #3a3a5a;
margin-right: 5px;
vertical-align: middle;
}
.status-dot.active { background: #7ee8a2; }
</style>
</head>
<body>
<div class="header">
<div class="logo">⬇</div>
<div class="header-text">
<h1>NZB → SABnzbd</h1>
<p>Klik op een NZB-link → direct downloaden</p>
</div>
</div>
<div class="body">
<div class="field">
<label>SABnzbd Host</label>
<input id="sabHost" type="url" placeholder="http://localhost:8080" />
<span class="hint">Bijv. http://192.168.1.10:8080 of https://sabnzbd.jouwserver.nl</span>
</div>
<div class="field">
<label>API-sleutel</label>
<input id="sabApiKey" type="text" placeholder="Jouw SABnzbd API-sleutel" />
<span class="hint">Te vinden in SABnzbd → Config → General → API Key</span>
</div>
<div class="field">
<label>Categorie (optioneel)</label>
<input id="sabCategory" type="text" placeholder="Default" />
<span class="hint">Laat leeg voor geen categorie</span>
</div>
<div class="divider"></div>
<div id="status"></div>
<div class="btn-row">
<button id="testBtn">🔌 Test verbinding</button>
<button id="saveBtn">Opslaan</button>
</div>
</div>
<div class="footer">
<span class="status-dot" id="statusDot"></span>
<span id="footerText">Nog niet geconfigureerd</span>
</div>
<script src="popup.js"></script>
</body>
</html>

81
popup.js Normal file
View file

@ -0,0 +1,81 @@
// popup.js — instellingen opslaan en verbinding testen
const sabHost = document.getElementById("sabHost");
const sabApiKey = document.getElementById("sabApiKey");
const sabCategory = document.getElementById("sabCategory");
const saveBtn = document.getElementById("saveBtn");
const testBtn = document.getElementById("testBtn");
const statusEl = document.getElementById("status");
const statusDot = document.getElementById("statusDot");
const footerText = document.getElementById("footerText");
// Laad opgeslagen instellingen
chrome.storage.sync.get(["sabHost", "sabApiKey", "sabCategory"], (config) => {
if (config.sabHost) sabHost.value = config.sabHost;
if (config.sabApiKey) sabApiKey.value = config.sabApiKey;
if (config.sabCategory) sabCategory.value = config.sabCategory;
updateFooter(!!config.sabHost && !!config.sabApiKey);
});
function updateFooter(configured) {
if (configured) {
statusDot.classList.add("active");
footerText.textContent = "Geconfigureerd — klaar om NZB-links te onderscheppen";
} else {
statusDot.classList.remove("active");
footerText.textContent = "Nog niet geconfigureerd";
}
}
function showStatus(message, isError = false) {
statusEl.textContent = message;
statusEl.className = isError ? "error" : "success";
}
saveBtn.addEventListener("click", () => {
const host = sabHost.value.trim();
const key = sabApiKey.value.trim();
const cat = sabCategory.value.trim();
if (!host || !key) {
showStatus("⚠️ Vul de host én API-sleutel in.", true);
return;
}
chrome.storage.sync.set({ sabHost: host, sabApiKey: key, sabCategory: cat }, () => {
showStatus("✅ Instellingen opgeslagen!");
updateFooter(true);
});
});
testBtn.addEventListener("click", async () => {
const host = sabHost.value.trim();
const key = sabApiKey.value.trim();
if (!host || !key) {
showStatus("⚠️ Vul eerst de host en API-sleutel in.", true);
return;
}
testBtn.textContent = "⏳ Bezig...";
testBtn.disabled = true;
try {
const cleanHost = host.replace(/\/$/, "");
const url = `${cleanHost}/api?output=json&apikey=${key}&mode=version`;
const response = await fetch(url);
const data = await response.json();
if (data.version) {
showStatus(`✅ Verbinding OK — SABnzbd versie ${data.version}`);
} else {
showStatus("❌ Reactie ontvangen maar geen versie. Controleer je API-sleutel.", true);
}
} catch (err) {
showStatus(`❌ Verbinding mislukt: ${err.message}`, true);
}
testBtn.textContent = "🔌 Test verbinding";
testBtn.disabled = false;
});