1
0

MangaDex Cover Mass-Downloader: Button immer sichtbar // Checkbox für Dezimal-Volumes

This commit is contained in:
Akamaru
2025-11-11 00:42:35 +01:00
parent ec69578e2e
commit be776d418f
2 changed files with 83 additions and 29 deletions

View File

@@ -226,10 +226,12 @@ Ersetzt Client-Namen in den "Top Clients" Tabellen des Pi-hole Admin-Dashboards
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`)
- Download-Button in der Buttonleiste (immer sichtbar, funktioniert auf allen Tabs)
- Modal zum Eingeben des Volume-Bereichs (z.B. "1-10")
- Checkbox für Dezimal-Volumes (z.B. 1.2, 2.5) - optional ein/ausschaltbar
- Automatisches Laden aller Cover-Informationen via MangaDex API
- Cover werden mit Volume-Nummer benannt: `00-01.jpg`, `00-02.jpg`, etc.
- Cover werden mit Volume-Nummer benannt: `00-01.jpg`, `00-02.jpg`, `00-01.2.jpg`, etc.
- Intelligente Filterung: nur ganze Zahlen oder inklusive Dezimal-Volumes
- 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

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @name MangaDex Cover Mass-Downloader
// @namespace https://git.ponywave.de/Akamaru/Userscripts
// @version 1.0
// @version 1.1
// @description Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter
// @author Akamaru
// @match https://mangadex.org/title/*
@@ -28,15 +28,10 @@
}
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) {
const buttonContainer = document.querySelector('.flex.gap-2.sm\\:mb-0.mb-2.flex-wrap');
if (buttonContainer) {
addDownloadButton();
} else {
setTimeout(checkAndAddButton, 500);
@@ -44,6 +39,16 @@
};
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() {
@@ -52,21 +57,21 @@
return;
}
// Finde die Button-Container-Section
const buttonSection = document.querySelector('[style*="grid-area: buttons"]');
if (!buttonSection) {
// Finde den Button-Container
const buttonContainer = document.querySelector('.flex.gap-2.sm\\:mb-0.mb-2.flex-wrap');
if (!buttonContainer) {
return;
}
// Erstelle Download-Button
// Erstelle Download-Button im Stil von "Start Reading"
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.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">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="icon">
<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>
@@ -74,8 +79,8 @@
downloadBtn.addEventListener('click', showDownloadModal);
// Füge Button zur Button-Section hinzu
buttonSection.appendChild(downloadBtn);
// Füge Button am Ende des Containers hinzu (ganz rechts)
buttonContainer.appendChild(downloadBtn);
}
function showDownloadModal() {
@@ -116,6 +121,23 @@
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';
@@ -169,13 +191,14 @@
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);
startDownload(from, to, includeDecimals);
};
buttonContainer.appendChild(cancelBtn);
@@ -184,6 +207,7 @@
modal.appendChild(title);
modal.appendChild(description);
modal.appendChild(inputContainer);
modal.appendChild(checkboxContainer);
modal.appendChild(errorContainer);
modal.appendChild(progressContainer);
modal.appendChild(buttonContainer);
@@ -236,7 +260,7 @@
}
}
async function startDownload(fromVolume, toVolume) {
async function startDownload(fromVolume, toVolume, includeDecimals = false) {
// Disable download button
const downloadBtn = document.getElementById('start-download-btn');
if (downloadBtn) {
@@ -264,18 +288,34 @@
// Filtere Cover nach Volume-Bereich
const selectedCovers = allCovers.filter(cover => {
const volume = parseInt(cover.attributes.volume);
return volume >= fromVolume && volume <= toVolume;
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) {
throw new Error(`Keine Cover für Volume ${fromVolume}-${toVolume} gefunden.`);
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 = parseInt(a.attributes.volume);
const volB = parseInt(b.attributes.volume);
const volA = parseFloat(a.attributes.volume);
const volB = parseFloat(b.attributes.volume);
return volA - volB;
});
@@ -288,7 +328,19 @@
// 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 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}`;
@@ -300,7 +352,7 @@
}
const blob = await coverResponse.blob();
const newFileName = `00-${volume}.${extension}`;
const newFileName = `00-${formattedVolume}.${extension}`;
zip.file(newFileName, blob);
downloadedCount++;