MangaDex Cover Mass-Downloader: Button immer sichtbar // Checkbox für Dezimal-Volumes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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++;
|
||||
|
||||
Reference in New Issue
Block a user