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. Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter. Perfekt um Manga-Cover für mehrere Volumes auf einmal zu sammeln.
**Funktionen:** **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") - 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 - 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) - Fortschrittsanzeige während des Downloads (API-Aufruf, Download, ZIP-Erstellung)
- Lädt nur Full-Cover herunter (keine komprimierten `.512.jpg` Versionen) - Lädt nur Full-Cover herunter (keine komprimierten `.512.jpg` Versionen)
- Automatischer ZIP-Download mit Manga-Titel im Dateinamen - Automatischer ZIP-Download mit Manga-Titel im Dateinamen

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name MangaDex Cover Mass-Downloader // @name MangaDex Cover Mass-Downloader
// @namespace https://git.ponywave.de/Akamaru/Userscripts // @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 // @description Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter
// @author Akamaru // @author Akamaru
// @match https://mangadex.org/title/* // @match https://mangadex.org/title/*
@@ -28,15 +28,10 @@
} }
function 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 // Warte bis die Seite vollständig geladen ist
const checkAndAddButton = () => { const checkAndAddButton = () => {
const artSection = document.querySelector('[style*="grid-area: art"]'); const buttonContainer = document.querySelector('.flex.gap-2.sm\\:mb-0.mb-2.flex-wrap');
if (artSection) { if (buttonContainer) {
addDownloadButton(); addDownloadButton();
} else { } else {
setTimeout(checkAndAddButton, 500); setTimeout(checkAndAddButton, 500);
@@ -44,6 +39,16 @@
}; };
checkAndAddButton(); 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() { function addDownloadButton() {
@@ -52,21 +57,21 @@
return; return;
} }
// Finde die Button-Container-Section // Finde den Button-Container
const buttonSection = document.querySelector('[style*="grid-area: buttons"]'); const buttonContainer = document.querySelector('.flex.gap-2.sm\\:mb-0.mb-2.flex-wrap');
if (!buttonSection) { if (!buttonContainer) {
return; return;
} }
// Erstelle Download-Button // Erstelle Download-Button im Stil von "Start Reading"
const downloadBtn = document.createElement('button'); const downloadBtn = document.createElement('button');
downloadBtn.id = 'cover-download-btn'; downloadBtn.id = 'cover-download-btn';
downloadBtn.className = 'rounded custom-opacity relative md-btn flex items-center px-3 overflow-hidden accent'; 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; margin-top: 0.5rem;'; downloadBtn.style.cssText = 'min-height: 3rem; min-width: 3rem;';
downloadBtn.title = 'Cover herunterladen'; downloadBtn.title = 'Cover herunterladen';
downloadBtn.innerHTML = ` downloadBtn.innerHTML = `
<span class="flex relative items-center justify-center font-medium select-none w-full pointer-events-none"> <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"> <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"/> <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> </svg>
</span> </span>
@@ -74,8 +79,8 @@
downloadBtn.addEventListener('click', showDownloadModal); downloadBtn.addEventListener('click', showDownloadModal);
// Füge Button zur Button-Section hinzu // Füge Button am Ende des Containers hinzu (ganz rechts)
buttonSection.appendChild(downloadBtn); buttonContainer.appendChild(downloadBtn);
} }
function showDownloadModal() { function showDownloadModal() {
@@ -116,6 +121,23 @@
inputContainer.appendChild(document.createTextNode('')); inputContainer.appendChild(document.createTextNode(''));
inputContainer.appendChild(toInput); 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) // Progress-Container (initially hidden)
const progressContainer = document.createElement('div'); const progressContainer = document.createElement('div');
progressContainer.id = 'download-progress'; progressContainer.id = 'download-progress';
@@ -169,13 +191,14 @@
downloadBtn.onclick = () => { downloadBtn.onclick = () => {
const from = parseInt(fromInput.value); const from = parseInt(fromInput.value);
const to = parseInt(toInput.value); const to = parseInt(toInput.value);
const includeDecimals = decimalCheckbox.checked;
if (!from || !to || from > to) { if (!from || !to || from > to) {
showError('Bitte gültige Volume-Nummern eingeben (Von ≤ Bis).'); showError('Bitte gültige Volume-Nummern eingeben (Von ≤ Bis).');
return; return;
} }
startDownload(from, to); startDownload(from, to, includeDecimals);
}; };
buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(cancelBtn);
@@ -184,6 +207,7 @@
modal.appendChild(title); modal.appendChild(title);
modal.appendChild(description); modal.appendChild(description);
modal.appendChild(inputContainer); modal.appendChild(inputContainer);
modal.appendChild(checkboxContainer);
modal.appendChild(errorContainer); modal.appendChild(errorContainer);
modal.appendChild(progressContainer); modal.appendChild(progressContainer);
modal.appendChild(buttonContainer); modal.appendChild(buttonContainer);
@@ -236,7 +260,7 @@
} }
} }
async function startDownload(fromVolume, toVolume) { async function startDownload(fromVolume, toVolume, includeDecimals = false) {
// Disable download button // Disable download button
const downloadBtn = document.getElementById('start-download-btn'); const downloadBtn = document.getElementById('start-download-btn');
if (downloadBtn) { if (downloadBtn) {
@@ -264,18 +288,34 @@
// Filtere Cover nach Volume-Bereich // Filtere Cover nach Volume-Bereich
const selectedCovers = allCovers.filter(cover => { const selectedCovers = allCovers.filter(cover => {
const volume = parseInt(cover.attributes.volume); const volumeStr = cover.attributes.volume;
return volume >= fromVolume && volume <= toVolume; 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) { 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 // Sortiere nach Volume
selectedCovers.sort((a, b) => { selectedCovers.sort((a, b) => {
const volA = parseInt(a.attributes.volume); const volA = parseFloat(a.attributes.volume);
const volB = parseInt(b.attributes.volume); const volB = parseFloat(b.attributes.volume);
return volA - volB; return volA - volB;
}); });
@@ -288,7 +328,19 @@
// Download Cover // Download Cover
for (const cover of selectedCovers) { for (const cover of selectedCovers) {
const fileName = cover.attributes.fileName; 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 extension = fileName.split('.').pop();
const coverUrl = `${CONFIG.coverBase}/${mangaId}/${fileName}`; const coverUrl = `${CONFIG.coverBase}/${mangaId}/${fileName}`;
@@ -300,7 +352,7 @@
} }
const blob = await coverResponse.blob(); const blob = await coverResponse.blob();
const newFileName = `00-${volume}.${extension}`; const newFileName = `00-${formattedVolume}.${extension}`;
zip.file(newFileName, blob); zip.file(newFileName, blob);
downloadedCount++; downloadedCount++;