您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Import comics from MangaUpdates JSON export
// ==UserScript== // @name Comick MangaUpdates Import // @namespace https://github.com/GooglyBlox // @version 1.2 // @description Import comics from MangaUpdates JSON export // @author GooglyBlox // @match https://comick.io/import // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; const API_ENDPOINTS = { search: 'https://api.comick.fun/v1.0/search/', follow: 'https://api.comick.io/follow' }; const READING_LISTS = { 1: 'Reading', 2: 'Completed', 3: 'On Hold', 4: 'Dropped', 5: 'Plan to Read' }; const state = { observer: null, buttonAdded: false, iconsAdded: false, headingUpdated: false, isProcessing: false }; function addMangaUpdatesIcon() { if (state.iconsAdded && document.querySelector('img[alt="MangaUpdates"]')) { return; } const iconContainer = document.querySelector('.flex.items-center .bg-auto.bg-al'); if (!iconContainer) return; const existingMangaUpdatesIcon = iconContainer.parentNode.querySelector('img[alt="MangaUpdates"]'); if (existingMangaUpdatesIcon) { state.iconsAdded = true; return; } const mangaUpdatesIcon = document.createElement('div'); mangaUpdatesIcon.className = 'h-6 w-6 ml-2 rounded overflow-hidden'; mangaUpdatesIcon.innerHTML = '<img src="https://www.mangaupdates.com/images/manga-updates.svg" class="h-full w-full object-cover" alt="MangaUpdates">'; iconContainer.parentNode.insertBefore(mangaUpdatesIcon, iconContainer.nextSibling); state.iconsAdded = true; } function updateHeading() { const heading = document.querySelector('h3'); if (!heading || !heading.textContent.includes('Import comics - manga from Myanimelist, Anilist')) return; if (heading.textContent.includes('MangaUpdates')) { state.headingUpdated = true; return; } heading.textContent = 'Import comics - manga from Myanimelist, Anilist, MangaUpdates'; state.headingUpdated = true; } function createMangaUpdatesButton() { const container = document.createElement('div'); container.className = 'flex items-center mt-3'; container.innerHTML = ` <button id="mangaupdates-import-btn" class="btn flex w-44 justify-start"> <img src="https://www.mangaupdates.com/images/manga-updates.svg" class="h-6 w-6 mx-2 rounded" alt="MangaUpdates"> <div>MangaUpdates</div> </button> <input type="file" id="mangaupdates-file-input" accept=".json" style="display: none;"> `; const selector = createReadingListSelector(); container.appendChild(selector); return container; } function createReadingListSelector() { const selector = document.createElement('div'); selector.className = 'flex items-center ml-4'; const options = Object.entries(READING_LISTS) .map(([value, text]) => `<option value="${value}">${text}</option>`) .join(''); selector.innerHTML = ` <label for="reading-list-select" class="text-sm text-gray-300 mr-2">Add to:</label> <select id="reading-list-select" class="bg-gray-700 border border-gray-600 text-white text-sm rounded px-3 py-1 focus:outline-none focus:border-blue-500"> ${options} </select> `; return selector; } function createProgressSection() { const section = document.createElement('div'); section.id = 'mangaupdates-progress-section'; section.className = 'mt-4 hidden'; section.innerHTML = ` <div class="p-4 bg-gray-800 rounded-lg border border-gray-600"> <div class="flex justify-between text-sm text-gray-300 mb-2"> <span id="mangaupdates-progress-text">Processing MangaUpdates import...</span> <span id="mangaupdates-progress-count">0/0</span> </div> <div class="w-full bg-gray-700 rounded-full h-2"> <div id="mangaupdates-progress-bar" class="bg-blue-600 h-2 rounded-full" style="width: 0%"></div> </div> <div id="mangaupdates-results" class="mt-4 max-h-64 overflow-y-auto"></div> </div> `; return section; } function addMangaUpdatesButton() { if (state.buttonAdded || document.getElementById('mangaupdates-import-btn')) { return; } const importContainer = document.querySelector('.xl\\:container'); if (!importContainer) return; const lastButtonContainer = importContainer.querySelector('.flex.items-center.mt-3:last-of-type'); if (!lastButtonContainer) return; const mangaUpdatesButton = createMangaUpdatesButton(); const progressSection = createProgressSection(); lastButtonContainer.insertAdjacentElement('afterend', mangaUpdatesButton); mangaUpdatesButton.insertAdjacentElement('afterend', progressSection); state.buttonAdded = true; setupEventListeners(); } function setupEventListeners() { const importBtn = document.getElementById('mangaupdates-import-btn'); const fileInput = document.getElementById('mangaupdates-file-input'); if (!importBtn || !fileInput) return; importBtn.addEventListener('click', () => { if (!state.isProcessing) { fileInput.click(); } }); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file && !state.isProcessing) { await processMangaUpdatesFile(file); e.target.value = ''; } }); } async function processMangaUpdatesFile(file) { state.isProcessing = true; const importBtn = document.getElementById('mangaupdates-import-btn'); const progressSection = document.getElementById('mangaupdates-progress-section'); const originalBtnContent = importBtn.innerHTML; importBtn.textContent = 'Processing...'; importBtn.disabled = true; progressSection.classList.remove('hidden'); try { const fileContent = await readFileAsText(file); const mangaUpdatesData = JSON.parse(fileContent); if (!Array.isArray(mangaUpdatesData)) { throw new Error('Invalid MangaUpdates file format. Expected JSON array.'); } await importFromMangaUpdates(mangaUpdatesData); } catch (error) { console.error('MangaUpdates import error:', error); showError(`Error processing MangaUpdates file: ${error.message}`); } finally { state.isProcessing = false; importBtn.disabled = false; importBtn.innerHTML = originalBtnContent; } } async function importFromMangaUpdates(mangaData) { const elements = { progressText: document.getElementById('mangaupdates-progress-text'), progressCount: document.getElementById('mangaupdates-progress-count'), progressBar: document.getElementById('mangaupdates-progress-bar'), resultsDiv: document.getElementById('mangaupdates-results'), readingListSelect: document.getElementById('reading-list-select') }; const selectedListType = parseInt(elements.readingListSelect.value); const listName = READING_LISTS[selectedListType]; const stats = { total: mangaData.length, processed: 0, successful: 0, failed: 0 }; elements.resultsDiv.innerHTML = `<div class="text-sm text-gray-300 mb-2 font-semibold">MangaUpdates Import Results (Adding to: ${listName}):</div>`; for (const manga of mangaData) { updateProgress(elements, manga.title, stats); const result = await processSingleManga(manga, selectedListType, listName); if (result.success) { stats.successful++; addResultItem(manga.title, 'success', result.message); } else { stats.failed++; addResultItem(manga.title, 'error', result.message); } stats.processed++; await delay(200); } finalizeImport(elements, stats); } async function processSingleManga(manga, listType, listName) { try { const searchResults = await searchComic(manga.title); if (!searchResults || searchResults.length === 0) { return { success: false, message: 'No matches found on Comick' }; } const bestMatch = searchResults[0]; const followResult = await followComic(bestMatch.id, listType); if (followResult.success) { return { success: true, message: `Added to ${listName}: ${bestMatch.title}` }; } return { success: false, message: `Failed to follow (Status: ${followResult.status})` }; } catch (error) { return { success: false, message: error.message }; } } async function searchComic(title) { const params = new URLSearchParams({ page: 1, limit: 15, showall: false, q: title, t: false }); const response = await fetch(`${API_ENDPOINTS.search}?${params}`); if (!response.ok) { throw new Error(`Comick search failed: HTTP ${response.status}`); } return response.json(); } async function followComic(comicId, listType = 1) { try { const response = await fetch(API_ENDPOINTS.follow, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: comicId, t: listType }), credentials: 'include' }); return { success: response.ok, status: response.status, data: response.ok ? await response.json() : null }; } catch (error) { console.error('Follow API error:', error); return { success: false, error: error.message }; } } function updateProgress(elements, title, stats) { elements.progressText.textContent = `Processing: ${title}`; elements.progressCount.textContent = `${stats.processed}/${stats.total}`; elements.progressBar.style.width = `${(stats.processed / stats.total) * 100}%`; } function finalizeImport(elements, stats) { elements.progressText.textContent = `MangaUpdates import complete: ${stats.successful} successful, ${stats.failed} failed`; elements.progressCount.textContent = `${stats.processed}/${stats.total}`; elements.progressBar.style.width = '100%'; } function addResultItem(title, type, message) { const resultsDiv = document.getElementById('mangaupdates-results'); const resultItem = document.createElement('div'); const colorClass = type === 'success' ? 'text-green-400 bg-green-900/20' : 'text-red-400 bg-red-900/20'; resultItem.className = `flex justify-between items-center py-1 px-2 text-sm rounded mb-1 ${colorClass}`; resultItem.innerHTML = ` <span class="truncate flex-1 mr-2">${escapeHtml(title)}</span> <span class="text-xs">${escapeHtml(message)}</span> `; resultsDiv.appendChild(resultItem); resultsDiv.scrollTop = resultsDiv.scrollHeight; } function showError(message) { const resultsDiv = document.getElementById('mangaupdates-results'); resultsDiv.innerHTML = `<div class="text-red-400 text-sm p-2 bg-red-900/20 rounded">${escapeHtml(message)}</div>`; } function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsText(file); }); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function checkElementsExist() { const iconExists = document.querySelector('img[alt="MangaUpdates"]'); const headingExists = document.querySelector('h3')?.textContent.includes('MangaUpdates'); if (!iconExists) { state.iconsAdded = false; } if (!headingExists) { state.headingUpdated = false; } } function checkAndAddButton() { const isImportPage = window.location.pathname === '/import'; if (!isImportPage) { cleanupElements(); return; } const hasRequiredElements = document.querySelector('.xl\\:container') && document.querySelector('h1')?.textContent.includes('Import Your Comics'); if (hasRequiredElements) { checkElementsExist(); addMangaUpdatesIcon(); updateHeading(); if (!state.buttonAdded) { setTimeout(addMangaUpdatesButton, 100); } } } function cleanupElements() { state.buttonAdded = false; state.iconsAdded = false; state.headingUpdated = false; const elements = [ document.getElementById('mangaupdates-import-btn')?.closest('.flex'), document.getElementById('mangaupdates-progress-section') ]; elements.forEach(el => el?.remove()); } function startObserver() { if (state.observer) { state.observer.disconnect(); } state.observer = new MutationObserver(() => { checkAndAddButton(); }); state.observer.observe(document.body, { childList: true, subtree: true }); } function init() { checkAndAddButton(); startObserver(); window.addEventListener('popstate', () => { state.buttonAdded = false; state.iconsAdded = false; state.headingUpdated = false; setTimeout(checkAndAddButton, 200); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();