您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export multiple tags from Danbooru's page to .txt files with flexible naming options, ordered export(descending/ascending)
// ==UserScript== // @name Enhanced Danbooru Tag Exporter // @namespace http://tampermonkey.net/ // @version 2.0.1 // @description Export multiple tags from Danbooru's page to .txt files with flexible naming options, ordered export(descending/ascending) // @author iMrdx // @match https://danbooru.donmai.us/* // @grant GM_download // @grant GM.addStyle // @grant GM_setValue // @grant GM_getValue // @license Apache License // @icon https://cdn.donmai.us/sample/d2/4d/__hatsune_miku_vocaloid_drawn_by_icon_315__sample-d24d819e95764ab46d5cb147294bb941.jpg // ==/UserScript== (function () { 'use strict'; const DELAY = 2000; const THEME_KEY = 'danbooru-exporter-theme'; // Core functionality const escapeBracketsAndFormatTags = (tag) => tag.replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/_/g, " "); const sortAndFormatTags = (post) => { const tags = { artist: post.tag_string_artist ? post.tag_string_artist.split(" ") : [], copyright: post.tag_string_copyright ? post.tag_string_copyright.split(" ") : [], character: post.tag_string_character ? post.tag_string_character.split(" ") : [], general: post.tag_string_general ? post.tag_string_general.split(" ") : [] }; const formattedSections = Object.entries(tags) .filter(([, tagArray]) => tagArray.length > 0) .map(([category, tagArray]) => ({ category, formatted: tagArray.map(escapeBracketsAndFormatTags).join(", ") })) .map(section => section.formatted); return formattedSections.join(", "); }; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Enhanced fetch function with progress tracking async function fetchTagsDescending(startPage, endPage, namingOption, isDescending = true, progressCallback) { let currentPage = startPage; let postCounter = 1; const totalPages = Math.abs(endPage - startPage) + 1; let processedPages = 0; while (isDescending ? currentPage >= endPage : currentPage <= endPage) { console.log(`Fetching page ${currentPage}...`); processedPages++; progressCallback((processedPages / totalPages) * 100); const urlParams = new URLSearchParams(window.location.search); urlParams.set("page", currentPage); const url = `${window.location.origin}/posts.json?${urlParams.toString()}&limit=20&order=id_${isDescending ? "desc" : "asc"}`; try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const posts = await response.json(); if (posts.length === 0) { console.log("No more posts found."); break; } for (const post of posts) { const postId = post.id; const formattedTags = sortAndFormatTags(post); let fileName; switch (namingOption) { case "Post ID": fileName = `${postId}.txt`; break; case "Post ID with Tags": fileName = `${postId}_${formattedTags.replace(/, /g, "_").replace(/ /g, "_")}.txt`; break; case "Number from Latest": fileName = `(${postCounter}).txt`; break; case "Number from Oldest": const totalPosts = Math.abs(startPage - endPage + 1) * 20; fileName = `(${totalPosts - postCounter + 1}).txt`; break; default: fileName = `post_${postId}.txt`; } GM_download({ url: `data:text/plain;charset=utf-8,${encodeURIComponent(formattedTags)}`, name: fileName, saveAs: false }); console.log(`Saved tags for post ${postId} to ${fileName}`); postCounter++; } await sleep(DELAY); currentPage += isDescending ? -1 : 1; } catch (error) { console.error(`Error processing page ${currentPage}:`, error); showNotification('error', `Error processing page ${currentPage}: ${error.message}`); break; } } showNotification('success', 'Tag export complete!'); progressCallback(100); } // Enhanced notification system with new styling function showNotification(type, message) { const notification = document.createElement('div'); notification.className = `exporter-notification ${type}`; notification.innerHTML = ` <i class="fa ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> <span>${message}</span> `; document.body.appendChild(notification); requestAnimationFrame(() => { notification.style.transform = 'translateX(0)'; notification.style.opacity = '1'; }); setTimeout(() => { notification.style.transform = 'translateX(100%)'; notification.style.opacity = '0'; setTimeout(() => notification.remove(), 300); }, 3000); } // Enhanced theme management with transition effects function toggleTheme() { const container = document.getElementById('tag-exporter-ui'); const isDark = container.classList.toggle('dark-theme'); GM_setValue(THEME_KEY, isDark); return isDark; } // Enhanced UI creation with new layout const createUI = () => { const container = document.createElement('div'); container.id = "tag-exporter-ui"; container.className = GM_getValue(THEME_KEY, false) ? 'dark-theme' : ''; container.innerHTML = ` <div id="exporter-header"> <div class="header-left"> <h3><i class="fa fa-tags"></i> Tag Exporter</h3> <span class="version-badge">v2.0.1</span> </div> <div class="header-right"> <button id="theme-toggle" class="icon-button" title="Toggle Theme"> <i class="fa fa-moon"></i> </button> <button id="minimize-button" class="icon-button" title="Minimize"> <i class="fa fa-minus"></i> </button> </div> </div> <div id="exporter-body"> <div class="input-group"> <label for="start-page">Start Page</label> <input type="number" id="start-page" min="1" placeholder="Enter start page"> </div> <div class="input-group"> <label for="end-page">End Page</label> <input type="number" id="end-page" min="1" placeholder="Enter end page"> </div> <div class="input-group"> <label for="naming-option">File Naming</label> <select id="naming-option"> <option value="Post ID">Post ID</option> <option value="Post ID with Tags">Post ID with Tags</option> <option value="Number from Latest">Number from Latest</option> <option value="Number from Oldest">Number from Oldest</option> </select> </div> <div class="input-group"> <label for="order-option">Order</label> <select id="order-option"> <option value="Descending">Descending</option> <option value="Ascending">Ascending</option> </select> </div> <div class="progress-container hidden"> <div class="progress-bar"> <div class="progress-fill"></div> </div> <span class="progress-text">0%</span> </div> <button id="export-button" class="pulse"> <i class="fa fa-download"></i> <span>Export Tags</span> </button> </div> `; document.body.appendChild(container); initializeEventListeners(); }; // Event listeners initialization function initializeEventListeners() { const container = document.getElementById('tag-exporter-ui'); const minimizeButton = document.querySelector('#minimize-button'); const exporterBody = document.querySelector('#exporter-body'); const exportButton = document.querySelector('#export-button'); const themeToggle = document.querySelector('#theme-toggle'); const header = document.querySelector('#exporter-header'); minimizeButton.addEventListener('click', () => { const isMinimized = exporterBody.classList.toggle('hidden'); minimizeButton.innerHTML = isMinimized ? '<i class="fa fa-plus"></i>' : '<i class="fa fa-minus"></i>'; container.classList.toggle('minimized'); }); themeToggle.addEventListener('click', () => { const isDark = toggleTheme(); themeToggle.innerHTML = isDark ? '<i class="fa fa-sun"></i>' : '<i class="fa fa-moon"></i>'; }); // Enhanced drag functionality let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { if (e.target.closest('#exporter-header')) { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; isDragging = true; container.style.transition = 'none'; } } function drag(e) { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; setTranslate(currentX, currentY, container); } } function dragEnd() { initialX = currentX; initialY = currentY; isDragging = false; container.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; } function setTranslate(xPos, yPos, el) { el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`; } // Export functionality with enhanced feedback exportButton.addEventListener('click', async () => { const startPage = parseInt(document.querySelector('#start-page').value, 10); const endPage = parseInt(document.querySelector('#end-page').value, 10); const namingOption = document.querySelector('#naming-option').value; const isDescending = document.querySelector('#order-option').value === 'Descending'; if (isNaN(startPage) || isNaN(endPage) || startPage < 1 || endPage < 1) { showNotification('error', 'Please enter valid page numbers.'); return; } const progressContainer = document.querySelector('.progress-container'); const progressBar = document.querySelector('.progress-fill'); const progressText = document.querySelector('.progress-text'); progressContainer.classList.remove('hidden'); exportButton.disabled = true; exportButton.classList.remove('pulse'); try { await fetchTagsDescending(startPage, endPage, namingOption, isDescending, (percent) => { progressBar.style.width = `${percent}%`; progressText.textContent = `${Math.round(percent)}%`; }); } catch (error) { showNotification('error', 'Export failed: ' + error.message); } finally { exportButton.disabled = false; exportButton.classList.add('pulse'); setTimeout(() => { progressContainer.classList.add('hidden'); progressBar.style.width = '0%'; progressText.textContent = '0%'; }, 2000); } }); } // Enhanced styles with glassmorphism and animations GM.addStyle(` @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap'); @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css'); #tag-exporter-ui { position: fixed; top: 20px; right: 20px; width: 340px; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); font-family: 'Poppins', sans-serif; z-index: 10000; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); border: 1px solid rgba(255, 255, 255, 0.18); } #tag-exporter-ui.dark-theme { background: rgba(45, 45, 45, 0.85); color: #ffffff; border-color: rgba(255, 255, 255, 0.08); } #tag-exporter-ui.minimized { width: 220px; } #exporter-header { background: linear-gradient(135deg, #4CAF50, #45a049); padding: 16px 20px; border-radius: 16px 16px 0 0; cursor: move; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .dark-theme #exporter-header { background: linear-gradient(135deg, #388E3C, #2E7D32); } .header-left { display: flex; align-items: center; gap: 10px; } .header-left h3 { color: white; font-size: 16px; font-weight: 600; margin: 0; display: flex; align-items: center; gap: 8px; } .version-badge { background: rgba(255, 255, 255, 0.2); padding: 2px 6px; border-radius: 4px; font-size: 10px; color: white; } #exporter-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; } .input-group { display: flex; flex-direction: column; gap: 6px; } .input-group label { color: #555; font-size: 13px; font-weight: 500; margin-left: 2px; } .dark-theme .input-group label { color: #ddd; } input, select { padding: 10px 14px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 8px; font-size: 14px; font-family: 'Poppins', sans-serif; background: rgba(255, 255, 255, 0.9); color: #333; transition: all 0.2s; } .dark-theme input, .dark-theme select { background: rgba(60, 60, 60, 0.9); border-color: rgba(255, 255, 255, 0.1); color: #fff; } input:focus, select:focus { border-color: #4CAF50; box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2); outline: none; } .icon-button { background: rgba(255, 255, 255, 0.2); border: none; width: 32px; height: 32px; border-radius: 8px; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .icon-button:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-1px); } #export-button { background: linear-gradient(135deg, #4CAF50, #45a049); color: white; border: none; padding: 12px; border-radius: 8px; cursor: pointer; font-weight: 500; font-family: 'Poppins', sans-serif; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } #export-button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(76, 175, 80, 0.2); } #export-button:disabled { background: linear-gradient(135deg, #ccc, #bbb); cursor: not-allowed; transform: none; box-shadow: none; } .dark-theme #export-button:disabled { background: linear-gradient(135deg, #555, #444); } .progress-container { background: rgba(245, 245, 245, 0.9); border-radius: 8px; padding: 2px; position: relative; height: 24px; transition: all 0.3s; } .dark-theme .progress-container { background: rgba(60, 60, 60, 0.9); } .progress-bar { width: 100%; height: 100%; background: rgba(240, 240, 240, 0.9); border-radius: 6px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(135deg, #4CAF50, #45a049); transition: width 0.3s ease; border-radius: 6px; } .progress-text { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); font-size: 12px; font-weight: 500; color: #666; } .dark-theme .progress-text { color: #fff; } .exporter-notification { position: fixed; bottom: 20px; right: 20px; padding: 14px 20px; border-radius: 12px; color: white; font-size: 14px; z-index: 10001; transform: translateX(100%); opacity: 0; transition: all 0.3s ease; display: flex; align-items: center; gap: 10px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); font-family: 'Poppins', sans-serif; } .exporter-notification.success { background: rgba(76, 175, 80, 0.95); } .exporter-notification.error { background: rgba(244, 67, 54, 0.95); } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); } 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } } .pulse { animation: pulse 2s infinite; } .hidden { display: none !important; } `); // Initialize the UI createUI(); })();