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