diff --git a/README.md b/README.md index 6d7c3da..664ece5 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,25 @@ Ersetzt Client-Namen in den "Top Clients" Tabellen des Pi-hole Admin-Dashboards --- +### 12. `mangadex-cover-downloader.user.js` +**[Installieren](https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/mangadex-cover-downloader.user.js)** + +**Beschreibung:** +Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter. Perfekt um Manga-Cover für mehrere Volumes auf einmal zu sammeln. + +**Funktionen:** +- Download-Button auf Cover-Seiten (`?tab=art`) +- Modal zum Eingeben des Volume-Bereichs (z.B. "1-10") +- Automatisches Laden aller Cover-Informationen via MangaDex API +- Cover werden mit Volume-Nummer benannt: `00-01.jpg`, `00-02.jpg`, etc. +- Fortschrittsanzeige während des Downloads (API-Aufruf, Download, ZIP-Erstellung) +- Lädt nur Full-Cover herunter (keine komprimierten `.512.jpg` Versionen) +- Automatischer ZIP-Download mit Manga-Titel im Dateinamen +- Fehlerbehandlung für fehlende Volumes oder API-Probleme +- JSZip Integration für effiziente ZIP-Kompression + +--- + ## Übersicht der enthaltenen UserStyles ### 1. `myanimelist-tweaks.user.css` diff --git a/mangadex-cover-downloader.user.js b/mangadex-cover-downloader.user.js new file mode 100644 index 0000000..f56d12d --- /dev/null +++ b/mangadex-cover-downloader.user.js @@ -0,0 +1,372 @@ +// ==UserScript== +// @name MangaDex Cover Mass-Downloader +// @namespace https://git.ponywave.de/Akamaru/Userscripts +// @version 1.0 +// @description Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter +// @author Akamaru +// @match https://mangadex.org/title/* +// @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() { + // Prüfe ob wir auf einer Art-Seite sind + if (!window.location.search.includes('tab=art')) { + return; + } + + // Warte bis die Seite vollständig geladen ist + const checkAndAddButton = () => { + const artSection = document.querySelector('[style*="grid-area: art"]'); + if (artSection) { + addDownloadButton(); + } else { + setTimeout(checkAndAddButton, 500); + } + }; + + checkAndAddButton(); + } + + function addDownloadButton() { + // Prüfe ob Button bereits existiert + if (document.getElementById('cover-download-btn')) { + return; + } + + // Finde die Button-Container-Section + const buttonSection = document.querySelector('[style*="grid-area: buttons"]'); + if (!buttonSection) { + return; + } + + // Erstelle Download-Button + const downloadBtn = document.createElement('button'); + downloadBtn.id = 'cover-download-btn'; + downloadBtn.className = 'rounded custom-opacity relative md-btn flex items-center px-3 overflow-hidden accent'; + downloadBtn.style.cssText = 'min-height: 3rem; min-width: 3rem; margin-top: 0.5rem;'; + downloadBtn.title = 'Cover herunterladen'; + downloadBtn.innerHTML = ` + + + + + + `; + + downloadBtn.addEventListener('click', showDownloadModal); + + // Füge Button zur Button-Section hinzu + buttonSection.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); + + // 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); + + if (!from || !to || from > to) { + showError('Bitte gültige Volume-Nummern eingeben (Von ≤ Bis).'); + return; + } + + startDownload(from, to); + }; + + buttonContainer.appendChild(cancelBtn); + buttonContainer.appendChild(downloadBtn); + + modal.appendChild(title); + modal.appendChild(description); + modal.appendChild(inputContainer); + 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) { + // 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 volume = parseInt(cover.attributes.volume); + return volume >= fromVolume && volume <= toVolume; + }); + + if (selectedCovers.length === 0) { + throw new Error(`Keine Cover für Volume ${fromVolume}-${toVolume} gefunden.`); + } + + // Sortiere nach Volume + selectedCovers.sort((a, b) => { + const volA = parseInt(a.attributes.volume); + const volB = parseInt(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 volume = cover.attributes.volume.padStart(2, '0'); // Pad with leading zeros + 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-${volume}.${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'; + } + } + } +})();