Rule34DLNamer (modified version of SankakuDLNamer)

File naming for rule34

// ==UserScript==
// @name        Rule34DLNamer (modified version of SankakuDLNamer)
// @namespace   Rule34DLNamer
// @description File naming for rule34
// @author      SlimeySlither, sanchan, Dramorian
// @match       http*://rule34.xxx/index.php?page=post*
// @icon        https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @run-at      document-end
// @version     1.0
// @grant       GM_download
// @license     MIT
// ==/UserScript==

(function() {
    'use strict';

    const usePostId = false; // replaces hash with post ID if true
    const prefixPostId = false; // put post ID in front if true
    const maxEntries = 4;
    const showCopyFilenameButton = false; // workaround in case GM_download fails
    const debug = false;
    const tagDelimiter = ', ';

    function main() {
        const tags = getSidebarTags();
        if (debug) console.debug('tags', tags);

        const imageData = getImageData();
        if (debug) console.debug('imageData', imageData);

        const postId = getPostId();
        if (debug) console.debug('postId', postId);

        const downloadName = generateFilename(tags, imageData, postId);
        if (debug) console.debug('downloadName', downloadName);

        const details = getDLDetails(imageData, downloadName);
        if (debug) console.debug(details);

        if (showCopyFilenameButton) insertUnderDetails(createCopyFilenameButton(downloadName));
        insertUnderDetails(createDownloadButton(details));

    }

    function createDownloadButton(details) {
        const a = document.createElement('a');
        a.href = '#';
        a.innerText = 'Download';
        a.onclick = function() {
            console.log('downloading...');
            details.onload = () => {
                console.log('download complete');
            };
            details.ontimeout = () => {
                console.error('download timeout');
            };
            details.onerror = (error, errorDetails) => {
                console.error('download failed', error, errorDetails);
                alert('download failed with ' + error);
            };
            details.onprogress = () => {
                if (debug) console.debug('.');
            };
            GM_download(details);
            return false;
        };

        return a;
    }

    function createCopyFilenameButton(downloadName) {
        const a = document.createElement('a');
        a.href = '#';
        a.innerText = 'Copy Filename';
        a.onclick = function() {
            navigator.clipboard.writeText(downloadName);
            return false;
        };

        return a;
    }

    function insertUnderDetails(el) {
        const tagSearchDiv = document.querySelector('div.tag-search');
        if (!tagSearchDiv) throw new Error('couldn\'t find .tag-search div');

        const li = document.createElement('li');
        li.appendChild(el);

        tagSearchDiv.appendChild(li);
    }

    function tagSearchDiv(node, ref_node) {
        ref_node.parentNode.insertBefore(node, ref_node.nextSibling);
    }

    function getPostId() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('id') || ''; // Return an empty string if 'id' is not found
    }

    function cleanText(text) {
        // replace illegal filename characters https://stackoverflow.com/a/42210346
        return text.replaceAll(/[/\\?%*:|"<>]/g, '-');
    }

    function getSidebarTags() {
        const tagSidebar = document.getElementById('tag-sidebar');
        if (!tagSidebar) throw new Error('couldn\'t find tag-sidebar');

        const cats = {}; // category -> [tags]
        for (const tagItem of tagSidebar.getElementsByTagName('li')) {
            let tag;

            // find the second <a> tag within the <li> element
            const tagLinks = tagItem.getElementsByTagName('a');
            if (tagLinks.length > 1) {
                tag = cleanText(tagLinks[1].innerText); // get text from the second <a> tag
            }

            if (tag) {
                let cat = cleanText(tagItem.className);

                // insert tag in its category
                if (!(cat in cats)) {
                    cats[cat] = [tag];
                } else {
                    cats[cat].push(tag);
                }
            }
        }

        return cats;
    }

    function getImageData() {
        const imageLink = document.querySelector('a[href*="/images/"]');
        if (!imageLink) throw new Error('couldn\'t find image link');

        const url = new URL(imageLink.getAttribute('href'), document.baseURI);
        if (debug) console.log('image url', url);

        const filename = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
        if (debug) console.log('filename', filename);

        const j = filename.lastIndexOf('.');
        const hash = filename.substring(0, j);
        const extension = filename.substring(j); // including '.'

        return {
            url,
            hash,
            extension
        };
    }

    function sortAndShortenTagList(tags) {
        if (!tags) return;

        tags.sort();

        if (tags.length > maxEntries) {
            tags.splice(maxEntries);
            tags.push('...');
        }
    }

    function generateFilename(tags, imageData, postId) {
    let characters = tags['tag-type-character tag'];
    const copyrights = tags['tag-type-copyright tag'];
    const artists = tags['tag-type-artist tag'];

    if (characters) {
        // Remove round brackets from character tags
        for (let i = 0; i < characters.length; i++) {
            let j = characters[i].indexOf('(');
            if (j > 0) {
                if ([' ', '_'].includes(characters[i][j - 1])) j--;
                characters[i] = characters[i].substring(0, j);
            }
        }

        // Deduplicate
        characters = [...new Set(characters)];
    }

    sortAndShortenTagList(characters);
    sortAndShortenTagList(copyrights);
    sortAndShortenTagList(artists);

    const tokens = [];

    if (usePostId && prefixPostId) {
        tokens.push(postId);
        tokens.push('-');
    }

    if (characters) tokens.push(characters.join(tagDelimiter));
    if (copyrights) tokens.push('(' + copyrights.join(tagDelimiter) + ')');
    if (artists) {
        tokens.push('drawn by');
        tokens.push(artists.join(tagDelimiter));
    }

    if (!usePostId) {
        tokens.push('-'); // Add dash before the hash
        tokens.push(imageData.hash);
    } else if (!prefixPostId) {
        tokens.push('-'); // Add dash before the postId
        tokens.push(postId);
    }

    // Remove '-' if there's nothing after it
    if (tokens[tokens.length - 1] === '-') {
        tokens.splice(-1);
    }

    // Join tokens into a filename string, convert to lowercase, and append extension
    const filename = tokens.join(' ') + imageData.extension;

    return filename;
}

    function getDLDetails(imageData, downloadName) {
        return {
            url: imageData.url.href,
            name: downloadName,
            saveAs: true,
        };
    }

    if (document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') {
        main();
    } else {
        document.addEventListener('DOMContentLoaded', main, false);
    }

})();