您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically downloads NZB files from nzbindex.nl when links with the "nzblnk:" scheme are clicked.
// ==UserScript== // @name NZB-Loader by LordBex // @description Automatically downloads NZB files from nzbindex.nl when links with the "nzblnk:" scheme are clicked. // @description:de_DE Lädt NZB-Dateien automatisch von nzbindex.nl herunter, wenn auf Links mit dem Schema "nzblnk:" geklickt wird. // @author LordBex // @version v1.0.2 // @match *://*/* // @grant GM_xmlhttpRequest // @connect nzbindex.com // @connect www.nzbking.com // @connect localhost // @icon https://i.imgur.com/O1ao7fL.png // @license MIT // @namespace LordBex/UserScripts/NZB-Loader // ==/UserScript== // ------------------------------------------------------------ //- Default Config: const AUSGABE = 'download' // mögliche Werte: // - download // - menu // - URLtoSABnzb // - NZBtoSABnzb (nicht kompatible mit Safari) // - custom (um deinen eigenen Handler `customHandler()` zu verwenden) const DISABLE_SUCCESS_ALERT = false; // default: false // ------------------------------------------------------------ //- SabNzbd Config: const SAB_API_KEY = '....'; const SAB_URL = 'http://localhost:8080/api'; // z.B. 'http://localhost:8080/sabnzbd/api' // Für Output im Menu notwendig: const SAB_CATEGORIES = [] // leer lassen, um sie direkt von SABnzbd zu holen / hierzu den api-key und nicht den nzb-key verwenden! // Für URLtoSABnzb und NZBtoSABnzb const SAB_DEFAULT_CATEGORY = '*' // default: * // Sab Buttons als Untermenü const SAB_SUB_MENU = false // default: false // ------------------------------------------------------------ // menu buttons async function openMenu(parameters) { const sabButtons = await getCategoriesButtons(parameters) const buttons = [ { name: 'Download', f: () => { downloadAndSave(parameters) }, bgColor: '#0D4715', icon: 'https://raw.githubusercontent.com/sabnzbd/sabnzbd/refs/heads/develop/icons/nzb.ico' } ] if (SAB_SUB_MENU) { buttons.push({ name: 'Zu Sabnzbd', f: () => { modal.showModal([ { name: 'Zurück', bgColor: '#4F959D', f: () => { modal.showModal(buttons) } }, ...sabButtons ]) }, icon: 'https://raw.githubusercontent.com/sabnzbd/sabnzbd/refs/heads/develop/icons/sabnzbd.ico' }) } else { buttons.push(...sabButtons) } infoModal.closeModal() modal.showModal(buttons) } function customHandler({downloadLink, fileName, password}) { // wird ausgeführt, wenn AUSGABE auf 'custom' gesetzt ist alert("Custom Handler") // Hier kann eigener Code eingefügt werden } // ------------------------------------------------------------ // sab-code function successAlert(message) { if (!DISABLE_SUCCESS_ALERT) { alert(message) } } function addNZBtoSABnzbd({downloadLink, fileName, password, category = SAB_DEFAULT_CATEGORY}) { const mode = 'addurl'; const name = encodeURIComponent(downloadLink); const eu_name = encodeURIComponent(fileName); const eu_pass = encodeURIComponent(password); const requestURL = `${SAB_URL}?output=json&mode=${mode}&name=${name}&nzbname=${eu_name}&cat=${category}&password=${eu_pass}&apikey=${SAB_API_KEY}`; console.log('Link to Sab:', requestURL); infoModal.print("Sende Link zu Sab ...") GM_xmlhttpRequest({ method: "GET", url: requestURL, headers: { "User-Agent": "Mozilla/5.0", "Accept": "application/json" }, onload: function (response) { console.log(response.responseText); let result = JSON.parse(response.responseText); if (result.status === true) { infoModal.print("Erfolg! NZB hinzugefügt. ID: " + result.nzo_ids.join(', ')) infoModal.closeIn(3000) } else { infoModal.showModal() infoModal.error('Fehler beim Hinzufügen der NZB-Datei zu SABnzbd.\n' + result.error); } }, onerror: function (response) { console.error('Anfrage fehlgeschlagen', response); alert("Anfrage an SABnzb schlug fail ! (mehr im Log)") } }); } function uploadNZBtoSABnzbd({responseText, fileName, password, category = SAB_DEFAULT_CATEGORY}) { let formData = new FormData(); let blob = new Blob([responseText], {type: "text/xml"}); formData.append('name', blob, fileName); formData.append('mode', 'addfile'); formData.append('nzbname', fileName); formData.append('password', password); formData.append('output', 'json'); formData.append('cat', category); formData.append('apikey', SAB_API_KEY); console.log('Upload Nzb to Sab:', formData); infoModal.print("Lade Nzb zu SABnzbd hoch ...") GM_xmlhttpRequest({ method: "POST", url: SAB_URL, data: formData, onload: function (response) { console.log('Upload response', response.status, response.statusText); console.log('Response body', response.responseText); let result = JSON.parse(response.responseText); if (result.status === true) { successAlert('Success! NZB added. ID: ' + result.nzo_ids.join(', ')); } else { alert('Error adding NZB file to SABnzbd.\n' + (result.error || 'Unknown error')); } }, onerror: function (response) { console.error('Error during file upload', response.status, response.statusText); alert("Could not upload NZB! (more in log)"); } }); } function getCatSabButton(parameters, category) { return { name: category.charAt(0).toUpperCase() + category.slice(1), value: category, f: () => { parameters.category = category addNZBtoSABnzbd(parameters) } } } async function getCategoriesButtons(parameters) { if (SAB_CATEGORIES.length > 0) { return SAB_CATEGORIES.map(item => { return getCatSabButton(parameters, item); }) } const mode = 'get_cats' const requestURL = new URL(SAB_URL); requestURL.searchParams.append('output', 'json'); requestURL.searchParams.append('mode', mode); requestURL.searchParams.append('apikey', SAB_API_KEY); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: requestURL.toString(), headers: { "User-Agent": "Mozilla/5.0", "Accept": "application/json" }, onload: function(response) { const data = JSON.parse(response.responseText); const categories = data.categories; resolve(categories.map(item => { return getCatSabButton(parameters, item); })); }, onerror: function(error) { console.error('Error during file upload', error); alert("Could not get Categories from SAB! (more in log)"); reject(error); } }); }); } // ------------------------------------------------------------ // download file-code function saveFile({responseText, fileName}) { let blob = new Blob([responseText], {type: "application/x-nzb"}); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = fileName // Dateiname ändern link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function downloadFile({downloadLink, fileName, password, callback}) { if (!fileName.endsWith('.nzb')) { fileName = fileName + '.nzb' } console.log("Download Nzb von " + downloadLink) GM_xmlhttpRequest({ method: "GET", url: downloadLink, onload: function (nzbResponse) { callback({ responseText: nzbResponse.responseText, fileName, password }) }, onerror: function () { console.error("Failed Download for " + downloadLink) alert("Nzb könnte nicht geladen werden !") } }); } function downloadAndSave({downloadLink, fileName, password}) { fileName = `${fileName}{{${password}}}.nzb` downloadFile({ downloadLink, fileName, password, callback: (args) => { saveFile(args) infoModal.print("Nzb gespeichert.") setTimeout(() => { infoModal.closeModal() }, 3000) } }) } function downloadAndSab({downloadLink, fileName, password, category = SAB_DEFAULT_CATEGORY}) { downloadFile({ downloadLink, fileName, password, callback: (parameter) => { parameter.category = category uploadNZBtoSABnzbd(parameter) } }) } // ------------------------------------------------------------ // handle menu customElements.define('menu-select-modal', class MenuSelectModal extends HTMLElement { constructor() { super(); } connectedCallback() { // Create a shadow root const shadow = this.attachShadow({mode: "open"}); shadow.innerHTML = ` <style> .btn { --_bg-color: var(--bg-color, #06f); align-items: center; background-color: var(--_bg-color); border: 2px solid var(--_bg-color); box-sizing: border-box; color: #fff; cursor: pointer; display: inline-flex; fill: #000; font-size: 24px; font-weight: 400; height: 48px; justify-content: center; line-height: 24px; width: 100%; outline: 0; padding: 0 17px; text-align: center; text-decoration: none; transition: all .3s; user-select: none; -webkit-user-select: none; touch-action: manipulation; border-radius: 5px; gap: 5px; } .btn:hover { filter: brightness(70%); } dialog { border: none !important; border-radius: calc(5px * 3.74); box-shadow: 0 0 #0000, 0 0 #0000, 0 25px 50px -12px rgba(0, 0, 0, 0.25); background-color: rgb(33, 37, 41); padding: 1.6rem; max-height: 70%; max-width: max(400px, 100vw); } .dialog-header { color: white; font-family: Inter, sans-serif; font-size: 20px; font-weight: 600; display: flex; justify-content: space-between; padding-bottom: 10px; } .buttons { display: flex; flex-direction: column; gap: 10px; min-width: 400px; } .close { all: initial; background: unset; padding: 5px; margin: 0; border: unset; } .close:not(:hover) { opacity: 0.3; /* Leichte Transparenz bei Hover */ } @media screen and (max-width: 450px) { .buttons { display: flex; flex-direction: column; gap: 10px; min-width: 300px; } } @media screen and (max-width: 350px) { .buttons { display: flex; flex-direction: column; gap: 10px; min-width: 150px; } } </style> <div data-bs-theme="dark"> <dialog id="dialog-1"> <form method="dialog"> <div class="dialog-header"> <span>Wähle ...</span> <button class="close"> <svg xmlns='http://www.w3.org/2000/svg' width="16" height="16" viewBox='0 0 16 16' fill='#CCC'> <path d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/> </svg> </button> </div> <div class="buttons buttons-here"> <p>...</p> </div> </form> </dialog> </div> ` this.dialog = shadow.querySelector('dialog') } showModal(items) { this.dialog.showModal() const modalContent = this.shadowRoot.querySelector('.buttons-here'); modalContent.innerHTML = ''; items.forEach(item => { const button = document.createElement('button'); button.className = 'btn'; if (item.innerHTML) { button.innerHTML = item.innerHTML; } if (item.icon) { const img = document.createElement('img'); img.src = item.icon; img.style.marginRight = '8px'; img.style.width = '24px'; img.style.height = '24px'; button.appendChild(img); } button.appendChild(document.createTextNode(item.name)); if (item.bgColor) { button.style.setProperty('--bg-color', item.bgColor); } button.onclick = () => { item.f(); // call function }; modalContent.appendChild(button); }); } closeModal() { this.dialog.close() } }); const modal = document.createElement('menu-select-modal'); document.body.appendChild(modal); // ------------------------------------------------------------ // info dialog handler customElements.define('nzblnk-info-modal', class NzbInfoModal extends HTMLElement { constructor() { super(); this.createModal() this.closeTimer = null } createModal() { // Create a shadow root const shadow = this.attachShadow({mode: "open"}); // language=HTML shadow.innerHTML = ` <style> .btn { align-items: center; background-color: #06f; border: 2px solid #06f; box-sizing: border-box; color: #fff; cursor: pointer; display: inline-flex; fill: #000; font-size: 24px; font-weight: 400; height: 48px; justify-content: center; line-height: 24px; width: 100%; outline: 0; padding: 0 17px; text-align: center; text-decoration: none; transition: all .3s; user-select: none; -webkit-user-select: none; touch-action: manipulation; border-radius: 5px; } .btn:hover { background-color: #3385ff; border-color: #3385ff; fill: #06f; } dialog { border: none !important; border-radius: calc(5px * 3.74); box-shadow: 0 0 #0000, 0 0 #0000, 0 25px 50px -12px rgba(0, 0, 0, 0.25); background-color: rgb(33, 37, 41); max-width: max(400px, 100vw); padding: 1.6rem; width: min(400px, 90vw); } .dialog-header { color: white; font-family: Inter, sans-serif; font-size: 20px; font-weight: 600; display: flex; justify-content: space-between; padding-bottom: 10px; } .close { all: initial; background: unset; padding: 5px; margin: 0; border: unset; } .close:not(:hover) { opacity: 0.3; /* Leichte Transparenz bei Hover */ } .dialog-content { color: whitesmoke; } </style> <div data-bs-theme="dark"> <dialog id="dialog-2"> <form method="dialog"> <div class="dialog-header"> <span>Info</span> <button class="close"> <svg xmlns='http://www.w3.org/2000/svg' width="16" height="16" viewBox='0 0 16 16' fill='#CCC'> <path d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/> </svg> </button> </div> <div class="dialog-content"> </div> </form> </dialog> </div> ` this.dialog = shadow.querySelector('dialog') this.modalContent = shadow.querySelector('.dialog-content') } showModal(callback) { this.dialog.showModal() } resetModal() { this.modalContent.innerHTML = ''; } print(message) { let p = document.createElement('p'); console.log("Info:", message) p.innerHTML = message this.modalContent.appendChild(p) } error(message) { let p = document.createElement('p'); p.style.color = 'red' console.error("Error:", message) p.innerHTML = message this.modalContent.appendChild(p) } closeModal() { this.dialog.close() clearTimeout(this.closeTimer) } closeIn(time) { this.closeTimer = setTimeout(() => { this.closeModal() }, time) } }); const infoModal = document.createElement('nzblnk-info-modal'); document.body.appendChild(infoModal); // ------------------------------------------------------------ // nzb handler function handleNzb(downloadLink, fileName, password) { infoModal.print(`Nzb wurde gefunden: <a href='${downloadLink}'>Link</a> (Fallback)`) const actions = { download: () => { downloadAndSave({downloadLink, fileName, password}) }, menu: () => { openMenu({downloadLink, fileName, password}).then( () => { console.log('menu opened') } ) }, URLtoSABnzb: () => { addNZBtoSABnzbd({downloadLink, fileName, password}) }, NZBtoSABnzb: () => { downloadAndSab({downloadLink, fileName, password}) }, custom: () => { customHandler({downloadLink, fileName, password}) } } let selected_action = actions[AUSGABE] if (!selected_action) { console.error("Ungültige AUSGABE Konfiguration") alert("Ungültige AUSGABE Konfiguration") return; } selected_action() } function parseNzblnkUrl(url) { // Entferne 'nzblnk:?' vom Anfang der URL let paramsString = url.slice(url.indexOf("?") + 1); // Analysiere die Parameter const params = new URLSearchParams(paramsString); // Füge die Parameter zu einem Objekt hinzu let result = {}; for (let param of params) { result[param[0]] = param[1]; } return result; } // ------------------------------------------------------------ // load from ... function loadFromNzbKing(nzb_info, when_failed) { console.log("Suche auf nzbking.com") GM_xmlhttpRequest({ method: "GET", url: "https://www.nzbking.com/?q=" + nzb_info.h, onload: function (response) { console.log("King:", response) let parser = new DOMParser(); let doc = parser.parseFromString(response.responseText, 'text/html'); const nzbLink = doc.querySelector('a[href^="/nzb:"]'); if (nzbLink) { console.log("Auf nzbking gefunden"); handleNzb("https://www.nzbking.com" + nzbLink.getAttribute('href'), nzb_info.t, nzb_info.p) return; } return when_failed() }, onerror: function () { console.error("Request zu NzbKing fehlgeschlagen") when_failed() } }); } function loadFromNzbIndex(nzb_info, when_failed) { console.log("Suche auf nzbindex.com") let url = `https://nzbindex.com/api/search?q=${nzb_info.h}&max=5&sort=agedesc` GM_xmlhttpRequest({ method: "GET", url: url, onload: function (response) { console.log("betaindex:", response) let data = JSON.parse(response.responseText); if (!data?.data) { console.error("Irgengendwas ist komisch bei nzbindex.com") console.log(data) return when_failed() } data = data.data if (data.page.totalElements === 0) { console.log("Nichts auf nzbindex.com gefunden") return when_failed() } if (data.content === undefined) { console.log("Keine Ergebnisse auf nzbindex.com gefunden - result is undefined") return when_failed() } if (data.content.length === 0) { console.log("Keine Ergebnisse auf nzbindex.com gefunden - result is empty") return when_failed() } if (!data.content[0]?.id) { console.log("Id ist nicht gesetzt bei nzbindex.com") return when_failed() } let ids = data.content.map(item => item.id).join('%2C'); console.log("Auf nzbindex.com gefunden") handleNzb("https://nzbindex.com/download?ids=" + ids, nzb_info.t, nzb_info.p) }, onerror: function (response) { console.log("Request zu nzbindex.com fehlgeschlagen") console.error(response) return when_failed() } }); } function loadNzb(nzblnk) { let nzb_info = parseNzblnkUrl(nzblnk) infoModal.resetModal() infoModal.showModal() const loadFunctions = [ { info: "NzbIndex", func: loadFromNzbIndex, }, { info: "NzbKing", func: loadFromNzbKing, } ] let load = function () { infoModal.print(`Keine Nzb gefunden :( `) setTimeout(() => { infoModal.closeModal() }, 6000) }; Array.from(loadFunctions).reverse().forEach(function (f) { const old_load = load load = function () { infoModal.print(`Versuche ${f.info} ....`) return f.func(nzb_info, old_load) } }) load() } // ------------------------------------------------------------ // svg-icons const downloadIcon = ` <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" style="width: 20px; height: 20px;"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /> </svg> ` const regexIcon = ` <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" style="width: 20px; height: 20px;"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-2.25Z" /> </svg> ` // ------------------------------------------------------------ // trigger // Event-Delegation für alle nzblnk-Links document.body.addEventListener('click', (event) => { const link = event.target.closest('a[href^="nzblnk"]'); if (link) { event.preventDefault(); loadNzb(link.href); } }); // Findet bereits vorhandene Links function processExistingLinks() { document.querySelectorAll('a[href^="nzblnk"]').forEach((link) => { // Optional: Link-Styling oder andere Anpassungen link.setAttribute('title', 'NZB herunterladen'); }); } // Überwacht DOM-Änderungen für dynamisch hinzugefügte Links function observeSiteChanges() { const observer = new MutationObserver((mutationsList) => { const hasNewContent = mutationsList.some(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0); if (hasNewContent) { // Nur bei tatsächlichen Änderungen Links verarbeiten processExistingLinks(); } }); observer.observe(document.body, { childList: true, subtree: true }); } processExistingLinks(); observeSiteChanges();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址