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.
|
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
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
|||||||
Reference in New Issue
Block a user