NZB-Loader by LordBex

Automatically downloads NZB files from nzbindex.nl when links with the "nzblnk:" scheme are clicked.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();