1
0
Files
Userscripts/mangadex-cover-downloader.user.js
2025-11-11 00:58:22 +01:00

426 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==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 = `
<span class="flex relative items-center justify-center font-medium select-none w-full pointer-events-none" style="justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="icon" style="color: currentcolor;">
<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 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';
}
}
}
})();