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       http*://idol.sankakucomplex.com/*posts/*
// @match       http*://beta.sankakucomplex.com/*posts/*
// @run-at      document-end
// @version     1.4.1
// @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() {
		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));

         // Convert text links in <li> elements to lowercase
        convertSidebarTagsToLowercase();
	}

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

    const liElements = sidebar.querySelectorAll('ul > li');
    liElements.forEach(li => {
        const sidebarTags = li.querySelectorAll('a');
        sidebarTags.forEach(link => {
            link.textContent = link.textContent.toLowerCase();
        });
    });
}


	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 imageLink = document.getElementById('highres');
		if (!imageLink) throw new Error('couldn\'t find image link');

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

		insertNodeAfter(li, imageLink.parentNode);
	}

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

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

	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 tag
        for (const tagLink of tagItem.getElementsByTagName('a')) {
            if (tagLink.hasAttribute('id')) {
                tag = cleanText(tagLink.innerText);
                break;
            }
        }

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

            // Convert category name to lowercase if it's 'tag-type-general'
            if (cat === 'tag-type-general') {
                cat = cat.toLowerCase();
            }

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

    return cats;
}

	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);
		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'];
    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
    const filename = tokens.join(' ').toLowerCase() + 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);
	}

})();