Neu: MangaDex Cover Mass-Downloader
This commit is contained in:
372
mangadex-cover-downloader.user.js
Normal file
372
mangadex-cover-downloader.user.js
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user