Sleazy Fork is available in English.

SankakuDLNamer

Help with DL naming

// ==UserScript==
// @name        SankakuDLNamer
// @namespace   SankakuDLNamer
// @description Help with DL naming
// @author      SlimeySlither, sanchan, Dramorian
// @match       http*://chan.sankakucomplex.com/*posts/*
// @match       https://chan.sankakucomplex.com/en/?tags=*
// @match       http*://idol.sankakucomplex.com/*posts/*
// @match       http*://beta.sankakucomplex.com/*posts/*
// @icon        https://www.google.com/s2/favicons?sz=64&domain=sankakucomplex.com
// @run-at      document-end
// @version     1.4.2
// @grant       GM_download
// ==/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() {
        try {
            // Retrieve and log necessary data
            const tags = getSidebarTags();
            logDebug('tags', tags);

            const imageData = getImageData();
            logDebug('imageData', imageData);

            const postId = getPostId();
            logDebug('postId', postId);

            // Generate filename and download details
            const downloadName = generateFilename(tags, imageData, postId);
            logDebug('downloadName', downloadName);

            const details = getDLDetails(imageData, downloadName);
            logDebug('details', details);

            // Insert buttons if needed
            if (showCopyFilenameButton) {
                insertUnderDetails(createCopyFilenameButton(downloadName));
            }
            insertUnderDetails(createDownloadButton(details));

            // Perform additional UI adjustments
            convertSidebarTagsToLowercase();
            observeBodyForAutocomplete();
        } catch (error) {
            console.error('Error in main function:', error);
        }
    }

    function logDebug(label, data) {
        if (debug) {
            console.debug(label, data);
        }
    }

    function convertSidebarTagsToLowercase() {
        const sidebar = document.getElementById('tag-sidebar');
        if (!sidebar) return; // Exit if the sidebar element is not found

        // Select all <a> elements within <li> elements in the sidebar
        sidebar.querySelectorAll('ul > li a').forEach(link => {
            link.textContent = link.textContent.toLowerCase();
        });
    }

    function observeBodyForAutocomplete() {
        const bodyObserver = new MutationObserver((mutationsList, observer) => {
            // Check for added nodes and look for the #autocomplete element
            if (mutationsList.some(mutation => mutation.type === 'childList')) {
                const autocomplete = document.getElementById('autocomplete');
                if (autocomplete) {
                    console.log('#autocomplete element detected.');

                    // Set up another observer to monitor text changes within #autocomplete
                    observeAutocompleteText(autocomplete);

                    // Stop observing the body once #autocomplete is found
                    observer.disconnect();
                }
            }
        });

        // Start observing the entire body for added nodes
        bodyObserver.observe(document.body, {childList: true, subtree: true});
    }

    function observeAutocompleteText(autocomplete) {
        const observer = new MutationObserver((mutationsList) => {
            mutationsList.forEach(mutation => {
                if (mutation.type === 'childList') {
                    // Convert text content to lowercase in newly added <b> and <span> tags within <a> tags
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            node.querySelectorAll('a b, a span').forEach(tag => {
                                tag.textContent = tag.textContent.toLowerCase();
                            });
                        }
                    });
                } else if (mutation.type === 'characterData') {
                    // Handle changes in text nodes within <b> or <span> inside <a> tags
                    const parent = mutation.target.parentNode;
                    if (parent.tagName === 'B' || parent.tagName === 'SPAN') {
                        const parentA = mutation.target.closest('a');
                        if (parentA) {
                            mutation.target.nodeValue = mutation.target.nodeValue.toLowerCase();
                        }
                    }
                }
            });
        });

        // Start observing changes within the #autocomplete element
        observer.observe(autocomplete, {childList: true, subtree: true, characterData: true});
    }

    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) {
        // Create the anchor element
        const button = document.createElement('a');
        button.href = '#';
        button.innerText = 'Copy Filename';

        // Add a click event listener for copying the filename
        button.addEventListener('click', (event) => {
            event.preventDefault();
            navigator.clipboard.writeText(downloadName)
                .then(() => console.log('Filename copied to clipboard'))
                .catch(error => console.error('Failed to copy filename', error));
        });

        return button;
    }


    function insertUnderDetails(element) {
        const imageLink = document.getElementById('highres');
        if (!imageLink) {
            throw new Error("Couldn't find image link");
        }

        // Create a new <li> element and append the provided element to it
        const listItem = document.createElement('li');
        listItem.appendChild(element);

        // Insert the new <li> after the imageLink's parent
        insertNodeAfter(listItem, imageLink.parentNode);
    }

    function insertNodeAfter(newNode, referenceNode) {
        if (!referenceNode) {
            throw new Error("Reference node is required");
        }
        referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
    }

    function getPostId() {
        const pathname = window.location.pathname;
        const cleanedPathname = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
        const lastSlashIndex = cleanedPathname.lastIndexOf('/');
        return cleanedPathname.substring(lastSlashIndex + 1);
    }

    function cleanText(text) {
        // Define a regular expression to match illegal filename characters
        const illegalCharsRegex = /[/\\?%*:|"<>]/g;

        // Replace illegal characters with a hyphen
        return text.replace(illegalCharsRegex, '-');
    }

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

        const tagsByCategory = {};

        // Iterate over each <li> element within the sidebar
        for (const tagItem of tagSidebar.getElementsByTagName('li')) {
            // Find the tag link within the <li> item
            const tagLink = Array.from(tagItem.getElementsByTagName('a')).find(link => link.hasAttribute('id'));
            if (tagLink) {
                const tag = cleanText(tagLink.innerText);
                const category = cleanText(tagItem.className);

                // Convert category name to lowercase if it's 'tag-type-general'
                const normalizedCategory = category === 'tag-type-general' ? category.toLowerCase() : category;

                // Initialize or append the tag to its category
                if (!tagsByCategory[normalizedCategory]) {
                    tagsByCategory[normalizedCategory] = [];
                }
                tagsByCategory[normalizedCategory].push(tag);
            }
        }

        return tagsByCategory;
    }


    function getImageData() {
        const imageLink = document.getElementById('highres');
        if (!imageLink) {
            throw new Error("Couldn't find image link");
        }

        const url = new URL(imageLink.getAttribute('href'), document.baseURI);
        const filename = url.pathname.split('/').pop();

        const [hash, extension = ''] = filename.split(/(\.[^.]+)$/);

        if (debug) {
            console.log('Image URL:', url);
            console.log('Filename:', filename);
        }

        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'];
        const copyrights = tags['tag-type-copyright'];
        const artists = tags['tag-type-artist'];

        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(imageData.hash);
        } else if (!prefixPostId) {
            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
        return tokens.join(' ').toLowerCase() + imageData.extension;
    }

    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);
    }

})();