1
0

Neu: MangaDex Cover Mass-Downloader

This commit is contained in:
Akamaru
2025-11-10 23:31:03 +01:00
parent 66764451b7
commit ec69578e2e
2 changed files with 391 additions and 0 deletions

View File

@@ -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 = `
<span class="flex relative items-center justify-center font-medium select-none w-full pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="icon">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m4-5 5 5 5-5m-5 5V3"/>
</svg>
</span>
`;
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';
}
}
}
})();