kooba-helper

For a better kooba<=>abook experience. This adds search links to Abook forums code boxes.

// ==UserScript==
// @name kooba-helper
// @namespace    kooba-helper@goobergoblin
// @description For a better kooba<=>abook experience. This adds search links to Abook forums code boxes.
// @author Shrek, rhymesagainsthumanity, goobergoblin, pushr (original creator)
// @version 2025.06.06.8
// @license MIT
// @supportURL https://abook.link/book/index.php?topic=54768
// @include *://abook.link/*
// @match *://abook.link/*
// @run-at document-end
// @grant none
// ==/UserScript==


function sanatize_common(code) {
    code = code.replace(/(?:abook|kooba)\.*(?:to|link|ws)*\s*(?:-|\||~)*\s*/gi, '');
    code = code.replace(/['"]+/g, '');
    code = code.replace(/\\&+/g, ' ');
    return code.trim();
}

const indexers = [
    {
        name: 'NZBIndex',
        url: 'https://DoNotRefer.Me/#https://nzbindex.com/search?max=25&minage=&maxage=&hidespam=1&hidepassword=0&sort=agedesc&minsize=&maxsize=&complete=0&hidecross=0&hasNFO=0&poster=&q={query}',
        codeFn: function(code) {
            return sanatize_common(code);
        }
    },
    {
        name: 'BinSearch',
        url: 'https://DoNotRefer.Me/#https://www.binsearch.info/?max=1000&&adv_age=&adv_sort=date&server=2&&q={query}',
        codeFn: function(code) {
            return sanatize_common(code);
        }
    },
    {
        name: 'BinSearch-Abook',
        url: 'https://DoNotRefer.Me/#https://www.binsearch.info/?max=100&adv_g=alt.binaries.mp3.abooks&adv_age=&adv_sort=date&q={query}',
        codeFn: function(code) {
            return sanatize_common(code);
        }
    },
    {
        name: 'NZBKing',
        url: 'https://DoNotRefer.Me/#http://nzbking.com/search/?q=%22{query}%22',
        codeFn: function(code) {
            return sanatize_common(code);
        }
    }
];

/**
 * Extracts the code text from a header element.
 * It handles cases where the code might be placed after a <br> tag or directly after the header.
 * @param {Element} header - The header element preceding the code.
 * @returns {string} The extracted code text, trimmed.
 */
function extractCodeFromHeader(header) {
    if (header.nextSibling && header.nextSibling.nodeName.toUpperCase() === 'BR') {
        return header.nextSibling.nextSibling?.textContent?.trim() || '';
    } else {
        return header.nextSibling?.textContent?.trim() || '';
    }
}

/**
 * Inserts a block of UI elements related to Kooba helper functionality.
 * This includes search links for the extracted code, a copy title link, and a NZBDonkey highlight text block.
 * @param {Element} header - The header element to insert after.
 * @param {string} title - The title of the page or topic.
 * @param {string} code - The extracted code string.
 * @param {string} password - The associated password, if any.
 */
function insertKoobaBlock(header, title, code, password) {
    const copyLink = document.createElement('a');
    copyLink.id = 'kooba-title-copy2';
    copyLink.classList.add('kooba-title-copy2');
    copyLink.dataset.cipboard = title;
    copyLink.title = `Copy "${title}" to clipboard`;
    copyLink.innerHTML = ` [Copy Title]`;
    copyLink.addEventListener('click', function(e) {
        kooba_copy_clipboard_data(e.target);
    });

    const searchContent = `
        <div class="kooba-search-links">
            <span>Search:</span>
            <ul>${buildLinks(code)}</ul>
        </div>
    `;
    header.classList.add('kooba_crunched');
    header.insertAdjacentHTML('beforeend', searchContent);
    header.insertAdjacentElement('beforeend', copyLink);

    const donkeyContent = `
        <div class="kooba-nzbdonkey">
            <div class="kooba-nzbdonkey-title">NZBDonkey Highlight Text <a class="kooba-nzbdonkey-help" target="_blank" href="https://tensai75.github.io/NZBDonkey/" title="This extension allows you to right click the text below and click 'Get NZB File' to automatically find and process the NZB.\n\nAnother extension is required.">?</a></div>
            <div class="kooba-nzbdonkey-text" onclick="window.getSelection().selectAllChildren(this);" oncontextmenu="window.getSelection().selectAllChildren(this);">
                <div>${title}</div>
                <div>Header: ${sanatize_common(code)}</div>
                <div>Password: ${password}</div>
            </div>
        </div>
    `;
    header.parentElement.insertAdjacentHTML('beforeend', donkeyContent);
}

/**
 * Determines if a header node likely represents a password label.
 * It checks up to 3 previous siblings for the presence of the word "password".
 * @param {Element} headerNode - The header element to check.
 * @returns {boolean} True if a password label is likely, false otherwise.
 */
function isLikelyPasswordLabel(headerNode) {
    let labelNode = headerNode.previousSibling;
    let count = 0;
    while (labelNode && count < 3) {
        if (labelNode.textContent?.toLowerCase().includes("password")) {
            return true;
        }
        labelNode = labelNode.previousSibling;
        count++;
    }
    return false;
}

function buildLinks(code){
    console.log("Build: ", code);
    let list = '';
    indexers.forEach(function (index) {
        console.log("IC: ", index.codeFn(code));
        const link = index.url.replace(/{query}/g, encodeURIComponent(index.codeFn(code)));

        list += `
      <li>
        <a rel="noreferrer" rel="noopener" target="_blank" href="${link}">
          ${index.name}
        </a>
      </li>
    `;
    });

    return list;
}

function checkHeader(header){
    // Check if any of the previous 5 siblings contain sting of "password"
    let heading = header.previousSibling;
    let i = 0;
    while (heading && i < 5) {
        i++;
        if (heading.textContent.toLowerCase().includes('password')) return true;
        heading = heading.previousSibling;
    }

    return false;
}

function checkCode(code){
    // Check if code itself contains string of "password"
    return code.toLowerCase().includes('password');
}


function process_kooba_search() {
    console.log('Process Kooba');
    const headers = document.querySelectorAll('.codeheader');
    if (!headers.length) {
        console.log("No .codeheader elements found.");
        return;
    }

    // Variables to store the main code block, associated password, and the header element for the main code
    let mainCode = '';
    let password = '';
    let mainHeader = null;

    // First pass: iterate over all headers to find the main code block and password
    headers.forEach(function (header) {
        if (header.classList.contains('kooba_crunched')) return;

        const cleanedCode = extractCodeFromHeader(header);
        console.log('Detected code block:', cleanedCode);

        if (isLikelyPasswordLabel(header)) {
            // This header likely labels a password, so store the password
            password = cleanedCode;
        } else if (!mainCode) {
            // Assign the first non-password code block as the main code
            mainCode = cleanedCode;
            mainHeader = header;
        }

        header.classList.add('kooba_crunched');
    });

    if (!mainHeader) {
        // Fallback handling for new style or unexpected page layouts where no main code header was found above
        // This block attempts to find code and passwords differently, ensuring compatibility with various page structures
        headers.forEach(function (header) {
            if (header.classList.contains('kooba_crunched')) return;

            const cleanedCode = extractCodeFromHeader(header);

            console.log('New Style');
            console.log('Code: ', cleanedCode);

            if (checkHeader(header) || checkCode(cleanedCode)) {
                console.log('Skipped password: ' + cleanedCode);
                header.classList.add('kooba_crunched');
                return;
            }

            console.log(header);

            let codes = header.parentElement.querySelectorAll('.bbc_code');
            if (codes.length >= 2) {
                for (let i = 1; i < codes.length; i++) {
                    let codeElement = codes[i];
                    console.log(codeElement);
                    let tmpCode = codeElement.textContent;

                    if (checkHeader(codeElement) || checkCode(tmpCode)) {
                        password = tmpCode;
                        console.log('Password: ', password);
                    }
                }
            }

            const page_author = document.querySelector('#author');
            const page_title = page_author.nextSibling.textContent.match(/Topic:(.*?)(?:\(Read)/i)[1].replace(/\[spot\]/gi,'').trim();

            insertKoobaBlock(header, page_title, cleanedCode, password);
        });
        return;
    } else {
        const page_author = document.querySelector('#author');
        const page_title = page_author.nextSibling.textContent.match(/Topic:(.*?)(?:\(Read)/i)[1].replace(/\[spot\]/gi,'').trim();

        insertKoobaBlock(mainHeader, page_title, mainCode, password);
    }
}

function inject_kooba_style() {
    document.querySelector('head').innerHTML += `
<style>

.kooba-search-links,
.kooba-search-links span,
.kooba-search-links ul,
.kooba-search-links li {
  display: inline-block;
}

.kooba-search-links span {
  margin-left: .5em;
}

.kooba-search-links ul {
  list-style: none;
  margin: 0;
  padding-left: 0;
}

.kooba-search-links li {
  margin: 0;
  padding: 0;
}

.kooba-search-links li:not(:last-child):after {
  color: #ccc;
  content:'|';
}

.kooba-search-links a {
  margin: 0;
  padding: 0 .5em;
}

.kooba-title-copy {
    color: #57aad2;
}
.kooba-title-copy:hover {
    text-decoration: underline;
}

.kooba-title-copy2 {
    margin-left: 30px;
    color: #9383e0;
    font-weight: normal;
}
.kooba-title-copy2:hover {
    text-decoration: underline;
}

.kooba-nzbdonkey {
    margin-top: 20px;
    border: 1px dotted yellow;
    padding: 10px;
}
.kooba-nzbdonkey-title {
    font-weight: bold;
    color: red;
}
a.kooba-nzbdonkey-help {
    text-decoration: none;
    color: #2196f3;
    border-bottom: 1px dotted #2196f3;
}
.kooba-nzbdonkey-text {
    font-size: 10px;
    font-weight: normal;
    font-family: monospace;
    color: #bb96e0;
    padding-left: 30px;
}
</style>

`;
}


window['kooba_copy_clipboard_str'] = str => {
    const el = document.createElement('textarea');
    el.value = str;
    el.setAttribute('readonly', '');
    el.style.position = 'absolute';
    el.style.left = '-9999px';
    document.body.appendChild(el);
    el.select();
    document.execCommand('copy');
    document.body.removeChild(el);
};


window['kooba_copy_clipboard_data'] = function kooba_copy_clipboard_data(obj) {
    kooba_copy_clipboard_str(obj.dataset.cipboard);

    var iDiv3 = document.createElement('div');
    iDiv3.className = 'copied_to_clipboard';
    iDiv3.style.border = '1px solid red';
    iDiv3.style.backgroundColor = 'red';
    iDiv3.style.color = 'yellow';
    iDiv3.style.textAlign = 'center';
    iDiv3.style.display = 'inline-block';
    iDiv3.style.position = 'absolute';
    iDiv3.style.marginLeft = '10px';
    // iDiv3.style.width = obj.offsetWidth + 'px';
    iDiv3.innerHTML = 'text copied to clipboard';
    kooba_insert_after(iDiv3, obj);
    setTimeout(function() {
        var elements = document.getElementsByClassName('copied_to_clipboard');
        while(elements.length > 0){ elements[0].parentNode.removeChild(elements[0]); }
    }, 2000);
}

window['kooba_insert_after'] = function kooba_insert_after(newNode, existingNode) {
    existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}

window['inject_kooba_title_copy'] = function inject_kooba_title_copy() {
    if ( document.querySelector('#kooba-title-copy') === null ) {
        // var page_title = document.querySelector('title').text.match(/Book Club - (.*)/i)[1].trim();
        var page_author = document.querySelector('#author');
        var page_title = page_author.nextSibling.textContent.match(/Topic:(.*?)(?:\(Read)/i)[1].replace(/\[spot\]/gi,'').trim();

        var iObjCopyTitle = document.createElement('a');
        iObjCopyTitle.classList.add('kooba-title-copy');
        iObjCopyTitle.id = 'kooba-title-copy';
        iObjCopyTitle.dataset.cipboard = page_title;
        iObjCopyTitle.title = 'Copy "' + page_title + '" to clipboard';
        iObjCopyTitle.innerHTML = ` [Copy]`;
        iObjCopyTitle.addEventListener('click', function(e) {
            kooba_copy_clipboard_data(e.target);
        });
        kooba_insert_after(iObjCopyTitle, page_author.nextSibling);
    }
}

if ((
    document.querySelector('a[href="https://abook.link/book/index.php#c3"]')
    || document.querySelector('a[href="https://abook.link/book/index.php?board=18.0"]')
)) {
    inject_kooba_title_copy();
    inject_kooba_style();
    process_kooba_search();
    console.log('Injecting Detour of Thank Function');
    // detour the original thank you click action
    window['orig_saythanks_handleThankClick'] = saythanks.prototype.handleThankClick;
    saythanks.prototype.handleThankClick = function (oInput) {
        console.log('Thank Detected'); // output to console that we intercepted the thank
        window['orig_saythanks_handleThankClick'](oInput); // call original thank action
        setTimeout(process_kooba_search, 200); // look for search boxes
        setTimeout(process_kooba_search, 1000); // it should catch after 200 ms but
        setTimeout(process_kooba_search, 2000); // here are a few more intervals to
        setTimeout(process_kooba_search, 5000); // keep trying, because it can't hurt,
        setTimeout(process_kooba_search, 10000); //  since we track injection now
    }
}