commit 0034676f5247d8eb874b225b4e816f15e25bfd6b Author: Eddy de Vink Date: Sat Jun 13 09:14:37 2026 +0200 Initial commit: NZB → SABnzbd Chrome extensie diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fa88ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..afc85bb --- /dev/null +++ b/README.md @@ -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. diff --git a/background.js b/background.js new file mode 100644 index 0000000..a87d6d7 --- /dev/null +++ b/background.js @@ -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 + }); +} diff --git a/claude.io.md b/claude.io.md new file mode 100644 index 0000000..ba5c37e --- /dev/null +++ b/claude.io.md @@ -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 + diff --git a/content.js b/content.js new file mode 100644 index 0000000..b690751 --- /dev/null +++ b/content.js @@ -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 -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 diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..f8d28f2 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..dc4199b Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..fd688dd Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f392640 --- /dev/null +++ b/manifest.json @@ -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": [ + "" + ], + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [""], + "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" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..5c11005 --- /dev/null +++ b/popup.html @@ -0,0 +1,227 @@ + + + + + NZB → SABnzbd + + + +
+ +
+

NZB → SABnzbd

+

Klik op een NZB-link → direct downloaden

+
+
+ +
+
+ + + Bijv. http://192.168.1.10:8080 of https://sabnzbd.jouwserver.nl +
+ +
+ + + Te vinden in SABnzbd → Config → General → API Key +
+ +
+ + + Laat leeg voor geen categorie +
+ +
+ +
+ +
+ + +
+
+ + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..37e48b5 --- /dev/null +++ b/popup.js @@ -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; +});