SankakuDLNamer

Help with DL naming

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();