1
0
Files
Userscripts/mangadex-cover-downloader.user.js
2025-11-10 23:31:03 +01:00

373 lines
15 KiB
JavaScript
Raw 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.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';
}
}
}
})();