// ==UserScript== // @name MangaDex Cover Mass-Downloader // @namespace https://git.ponywave.de/Akamaru/Userscripts // @version 1.2 // @description Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter // @author Akamaru // @match https://mangadex.org/title/* // @icon https://www.google.com/s2/favicons?domain=mangadex.org&sz=32 // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/mangadex-cover-downloader.user.js // @downloadURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/mangadex-cover-downloader.user.js // ==/UserScript== (function() { 'use strict'; // Konfiguration const CONFIG = { apiBase: 'https://api.mangadex.org', coverBase: 'https://mangadex.org/covers' }; // Warte bis Seite geladen ist if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { // Warte bis die Seite vollständig geladen ist const checkAndAddButton = () => { const buttonContainer = document.querySelector('.flex.gap-2.sm\\:mb-0.mb-2.flex-wrap'); if (buttonContainer) { addDownloadButton(); } else { setTimeout(checkAndAddButton, 500); } }; checkAndAddButton(); // Beobachte DOM-Änderungen für SPA-Navigation (z.B. Tab-Wechsel) const observer = new MutationObserver(() => { checkAndAddButton(); }); observer.observe(document.body, { childList: true, subtree: true }); } function addDownloadButton() { // Prüfe ob Button bereits existiert if (document.getElementById('cover-download-btn')) { return; } // Finde den Button-Container const buttonContainer = document.querySelector('.flex.gap-2.sm\\:mb-0.mb-2.flex-wrap'); if (!buttonContainer) { return; } // Erstelle Download-Button im Stil von "Start Reading" const downloadBtn = document.createElement('button'); downloadBtn.id = 'cover-download-btn'; downloadBtn.className = 'flex-grow sm:flex-grow-0 rounded custom-opacity relative md-btn flex items-center px-3 overflow-hidden accent flex-grow sm:flex-grow-0'; downloadBtn.style.cssText = 'min-height: 3rem; min-width: 3rem;'; downloadBtn.title = 'Cover herunterladen'; downloadBtn.innerHTML = ` `; downloadBtn.addEventListener('click', showDownloadModal); // Füge Button am Ende des Containers hinzu (ganz rechts) buttonContainer.appendChild(downloadBtn); } function showDownloadModal() { // Erstelle Modal-Overlay const overlay = document.createElement('div'); overlay.id = 'cover-download-modal'; overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;'; // Erstelle Modal-Content const modal = document.createElement('div'); modal.style.cssText = 'background: #1a1d1f; color: #fff; padding: 30px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 20px rgba(0,0,0,0.3);'; const title = document.createElement('h2'); title.textContent = 'Cover herunterladen'; title.style.cssText = 'margin: 0 0 20px 0; font-size: 24px; font-weight: bold;'; const description = document.createElement('p'); description.textContent = 'Gib die Volume-Nummern ein, die du herunterladen möchtest:'; description.style.cssText = 'margin: 0 0 15px 0; color: #ccc;'; // Input für Bereich const inputContainer = document.createElement('div'); inputContainer.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 15px; justify-content: center;'; const fromInput = document.createElement('input'); fromInput.type = 'number'; fromInput.placeholder = 'Von'; fromInput.min = '1'; fromInput.style.cssText = 'width: 80px; padding: 8px; background: #2a2d31; border: 1px solid #444; border-radius: 4px; color: #fff; text-align: center;'; const toInput = document.createElement('input'); toInput.type = 'number'; toInput.placeholder = 'Bis'; toInput.min = '1'; toInput.style.cssText = 'width: 80px; padding: 8px; background: #2a2d31; border: 1px solid #444; border-radius: 4px; color: #fff; text-align: center;'; inputContainer.appendChild(fromInput); inputContainer.appendChild(document.createTextNode('–')); inputContainer.appendChild(toInput); // Checkbox für Dezimal-Volumes const checkboxContainer = document.createElement('div'); checkboxContainer.style.cssText = 'margin-bottom: 15px; display: flex; align-items: center; justify-content: center; gap: 8px;'; const decimalCheckbox = document.createElement('input'); decimalCheckbox.type = 'checkbox'; decimalCheckbox.id = 'include-decimal-volumes'; decimalCheckbox.style.cssText = 'cursor: pointer; width: 18px; height: 18px;'; const checkboxLabel = document.createElement('label'); checkboxLabel.htmlFor = 'include-decimal-volumes'; checkboxLabel.textContent = 'Dezimal-Volumes einschließen (z.B. 1.2, 2.5)'; checkboxLabel.style.cssText = 'color: #ccc; cursor: pointer; user-select: none;'; checkboxContainer.appendChild(decimalCheckbox); checkboxContainer.appendChild(checkboxLabel); // Progress-Container (initially hidden) const progressContainer = document.createElement('div'); progressContainer.id = 'download-progress'; progressContainer.style.cssText = 'display: none; margin: 20px 0;'; const progressBar = document.createElement('div'); progressBar.style.cssText = 'width: 100%; height: 30px; background: #2a2d31; border-radius: 4px; overflow: hidden; position: relative;'; const progressFill = document.createElement('div'); progressFill.id = 'progress-fill'; progressFill.style.cssText = 'height: 100%; background: linear-gradient(90deg, #4CAF50, #45a049); width: 0%; transition: width 0.3s;'; const progressText = document.createElement('div'); progressText.id = 'progress-text'; progressText.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: bold; color: #fff;'; progressText.textContent = '0%'; progressBar.appendChild(progressFill); progressBar.appendChild(progressText); const statusText = document.createElement('div'); statusText.id = 'status-text'; statusText.style.cssText = 'margin-top: 10px; text-align: center; color: #ccc;'; statusText.textContent = 'Lade Cover...'; progressContainer.appendChild(progressBar); progressContainer.appendChild(statusText); // Error-Container (initially hidden) const errorContainer = document.createElement('div'); errorContainer.id = 'error-container'; errorContainer.style.cssText = 'display: none; margin: 15px 0; padding: 15px; background: #d32f2f; border-radius: 4px; color: #fff;'; // Button-Container const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;'; const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Abbrechen'; cancelBtn.style.cssText = 'padding: 10px 20px; background: #444; border: none; border-radius: 4px; color: #fff; cursor: pointer; font-size: 14px;'; cancelBtn.addEventListener('mouseover', () => cancelBtn.style.background = '#555'); cancelBtn.addEventListener('mouseout', () => cancelBtn.style.background = '#444'); cancelBtn.onclick = () => overlay.remove(); const downloadBtn = document.createElement('button'); downloadBtn.id = 'start-download-btn'; downloadBtn.textContent = 'Herunterladen'; downloadBtn.style.cssText = 'padding: 10px 20px; background: #4CAF50; border: none; border-radius: 4px; color: #fff; cursor: pointer; font-size: 14px; font-weight: bold;'; downloadBtn.addEventListener('mouseover', () => downloadBtn.style.background = '#45a049'); downloadBtn.addEventListener('mouseout', () => downloadBtn.style.background = '#4CAF50'); downloadBtn.onclick = () => { const from = parseInt(fromInput.value); const to = parseInt(toInput.value); const includeDecimals = decimalCheckbox.checked; if (!from || !to || from > to) { showError('Bitte gültige Volume-Nummern eingeben (Von ≤ Bis).'); return; } startDownload(from, to, includeDecimals); }; buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(downloadBtn); modal.appendChild(title); modal.appendChild(description); modal.appendChild(inputContainer); modal.appendChild(checkboxContainer); modal.appendChild(errorContainer); modal.appendChild(progressContainer); modal.appendChild(buttonContainer); overlay.appendChild(modal); document.body.appendChild(overlay); // Schließe Modal bei Klick auf Overlay overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); } }); // Focus auf erstes Input fromInput.focus(); } function showError(message) { const errorContainer = document.getElementById('error-container'); if (errorContainer) { errorContainer.textContent = message; errorContainer.style.display = 'block'; setTimeout(() => { errorContainer.style.display = 'none'; }, 5000); } } function updateProgress(percent, statusMsg) { const progressContainer = document.getElementById('download-progress'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); const statusText = document.getElementById('status-text'); if (progressContainer) { progressContainer.style.display = 'block'; } if (progressFill) { progressFill.style.width = percent + '%'; } if (progressText) { progressText.textContent = Math.round(percent) + '%'; } if (statusText && statusMsg) { statusText.textContent = statusMsg; } } async function startDownload(fromVolume, toVolume, includeDecimals = false) { // Disable download button const downloadBtn = document.getElementById('start-download-btn'); if (downloadBtn) { downloadBtn.disabled = true; downloadBtn.style.opacity = '0.5'; downloadBtn.style.cursor = 'not-allowed'; } try { // Extrahiere Manga-ID aus URL const mangaId = window.location.pathname.split('/')[2]; updateProgress(10, 'Lade Cover-Informationen...'); // Hole Cover-Daten von API const response = await fetch(`${CONFIG.apiBase}/cover?manga[]=${mangaId}&limit=100`); if (!response.ok) { throw new Error('Fehler beim Laden der Cover-Daten von der API'); } const data = await response.json(); const allCovers = data.data; updateProgress(20, 'Filtere Cover nach Volume...'); // Filtere Cover nach Volume-Bereich const selectedCovers = allCovers.filter(cover => { const volumeStr = cover.attributes.volume; const volumeNum = parseFloat(volumeStr); // Prüfe ob Volume im Bereich liegt if (volumeNum < fromVolume || volumeNum > toVolume) { return false; } // Wenn Dezimal-Volumes nicht eingeschlossen werden sollen, // filtere alle Volumes mit Dezimalpunkt heraus if (!includeDecimals && volumeStr.includes('.')) { return false; } return true; }); if (selectedCovers.length === 0) { const msg = includeDecimals ? `Keine Cover für Volume ${fromVolume}-${toVolume} gefunden.` : `Keine Cover für Volume ${fromVolume}-${toVolume} gefunden (Dezimal-Volumes ausgeschlossen).`; throw new Error(msg); } // Sortiere nach Volume selectedCovers.sort((a, b) => { const volA = parseFloat(a.attributes.volume); const volB = parseFloat(b.attributes.volume); return volA - volB; }); updateProgress(30, `Lade ${selectedCovers.length} Cover herunter...`); // Erstelle ZIP const zip = new JSZip(); let downloadedCount = 0; // Download Cover for (const cover of selectedCovers) { const fileName = cover.attributes.fileName; const volumeStr = cover.attributes.volume; // Format Volume-Nummer: Pad den ganzzahligen Teil mit Nullen let formattedVolume; if (volumeStr.includes('.')) { // Dezimal-Volume: "1.2" -> "01.2" const parts = volumeStr.split('.'); formattedVolume = parts[0].padStart(2, '0') + '.' + parts.slice(1).join('.'); } else { // Ganze Zahl: "1" -> "01" formattedVolume = volumeStr.padStart(2, '0'); } const extension = fileName.split('.').pop(); const coverUrl = `${CONFIG.coverBase}/${mangaId}/${fileName}`; try { const coverResponse = await fetch(coverUrl); if (!coverResponse.ok) { console.warn(`Fehler beim Laden von Cover ${fileName}`); continue; } const blob = await coverResponse.blob(); const newFileName = `00-${formattedVolume}.${extension}`; zip.file(newFileName, blob); downloadedCount++; const progress = 30 + (downloadedCount / selectedCovers.length) * 60; updateProgress(progress, `Cover ${downloadedCount}/${selectedCovers.length} heruntergeladen...`); } catch (err) { console.error(`Fehler beim Download von ${fileName}:`, err); } } if (downloadedCount === 0) { throw new Error('Keine Cover konnten heruntergeladen werden.'); } updateProgress(90, 'Erstelle ZIP-Datei...'); // Generiere ZIP const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); updateProgress(95, 'Starte Download...'); // Erstelle Download-Link const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; // Hole Manga-Titel für Dateinamen const titleElement = document.querySelector('[style*="grid-area: title"] p'); const mangaTitle = titleElement ? titleElement.textContent.trim().replace(/[^a-z0-9]/gi, '_') : 'manga'; a.download = `${mangaTitle}_covers_vol${fromVolume}-${toVolume}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); updateProgress(100, `Download abgeschlossen! ${downloadedCount} Cover heruntergeladen.`); // Schließe Modal nach 2 Sekunden setTimeout(() => { const modal = document.getElementById('cover-download-modal'); if (modal) { modal.remove(); } }, 2000); } catch (error) { console.error('Download-Fehler:', error); showError(`Fehler: ${error.message}`); // Re-enable download button if (downloadBtn) { downloadBtn.disabled = false; downloadBtn.style.opacity = '1'; downloadBtn.style.cursor = 'pointer'; } // Hide progress const progressContainer = document.getElementById('download-progress'); if (progressContainer) { progressContainer.style.display = 'none'; } } } })();