您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); } })();