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