Initial commit: NZB → SABnzbd Chrome extensie
This commit is contained in:
commit
0034676f52
11 changed files with 535 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.DS_Store
|
||||
*.log
|
||||
29
README.md
Normal file
29
README.md
Normal 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
61
background.js
Normal 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
27
claude.io.md
Normal 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
76
content.js
Normal 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
BIN
icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 578 B |
BIN
icons/icon16.png
Normal file
BIN
icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 B |
BIN
icons/icon48.png
Normal file
BIN
icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 B |
32
manifest.json
Normal file
32
manifest.json
Normal 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
227
popup.html
Normal 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
81
popup.js
Normal 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;
|
||||
});
|
||||
Loading…
Reference in a new issue