Created
November 6, 2025 09:07
-
-
Save maluramichael/20dee51127c3fe4eeaed1f438bb366a4 to your computer and use it in GitHub Desktop.
ING Postbox PDF Downloader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name ING Postbox PDF Downloader | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Safely download all PDFs from ING Postbox Archive with rate limiting | |
| // @author Michael Malura <[email protected]> | |
| // @match https://banking.ing.de/app/postbox/postbox_archiv* | |
| // @grant none | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| const CONFIG = { | |
| delayBetweenDownloads: 2000, | |
| buttonColor: '#FF6200', | |
| checkmarkColor: '#00A651', | |
| buttonText: 'Download All PDFs' | |
| }; | |
| let isDownloading = false; | |
| let shouldAbort = false; | |
| let downloadQueue = []; | |
| let processedCount = 0; | |
| function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| function createCheckmark() { | |
| const checkmark = document.createElement('span'); | |
| checkmark.innerHTML = '✓'; | |
| checkmark.style.cssText = ` | |
| display: inline-block; | |
| margin-left: 8px; | |
| color: ${CONFIG.checkmarkColor}; | |
| font-weight: bold; | |
| font-size: 18px; | |
| animation: fadeIn 0.3s ease-in; | |
| `; | |
| return checkmark; | |
| } | |
| function markRowAsProcessed(row) { | |
| row.style.backgroundColor = '#f0f8f0'; | |
| row.style.opacity = '0.7'; | |
| const docTypeCell = row.querySelector('.postbox-grid-left span:last-child'); | |
| if (docTypeCell && !docTypeCell.querySelector('span[data-downloaded]')) { | |
| const checkmark = createCheckmark(); | |
| checkmark.setAttribute('data-downloaded', 'true'); | |
| docTypeCell.appendChild(checkmark); | |
| } | |
| } | |
| function extractDownloadLinks() { | |
| const links = []; | |
| const rows = document.querySelectorAll('.ibbr-table-row[role="row"]'); | |
| rows.forEach(row => { | |
| if (row.querySelector('span[data-downloaded]')) { | |
| return; | |
| } | |
| const downloadLink = row.querySelector('a.button[role="button"][href*="postbox_archiv"]'); | |
| if (downloadLink && downloadLink.textContent.trim() === 'Download') { | |
| const docType = row.querySelector('.postbox-grid-left span:last-child')?.textContent.trim() || 'Unknown'; | |
| const description = row.querySelector('.postbox-grid-description')?.textContent.trim() || ''; | |
| const date = row.querySelector('.postbox-grid-right')?.textContent.trim() || ''; | |
| links.push({ | |
| url: downloadLink.href, | |
| row: row, | |
| docType: docType, | |
| description: description.replace(/\s+/g, ' '), | |
| date: date | |
| }); | |
| } | |
| }); | |
| return links; | |
| } | |
| async function downloadPDF(linkData, index, total) { | |
| try { | |
| console.log(`[${index + 1}/${total}] Downloading: ${linkData.docType} - ${linkData.date}`); | |
| updateStatus(`Downloading ${index + 1}/${total}: ${linkData.docType}...`); | |
| const tempLink = document.createElement('a'); | |
| tempLink.href = linkData.url; | |
| tempLink.style.display = 'none'; | |
| document.body.appendChild(tempLink); | |
| tempLink.click(); | |
| document.body.removeChild(tempLink); | |
| markRowAsProcessed(linkData.row); | |
| processedCount++; | |
| console.log(`Successfully queued download ${index + 1}/${total}`); | |
| } catch (error) { | |
| console.error(`Error downloading ${linkData.docType}:`, error); | |
| updateStatus(`Error on ${index + 1}/${total}: ${error.message}`, true); | |
| } | |
| } | |
| function abortDownload() { | |
| shouldAbort = true; | |
| console.log('Download abort requested by user'); | |
| updateStatus(`Aborting after current download...`, false); | |
| } | |
| async function processDownloadQueue() { | |
| if (isDownloading) { | |
| alert('Download already in progress!'); | |
| return; | |
| } | |
| downloadQueue = extractDownloadLinks(); | |
| if (downloadQueue.length === 0) { | |
| alert('No PDFs found to download, or all have already been downloaded.'); | |
| return; | |
| } | |
| if (!confirm(`Found ${downloadQueue.length} PDF(s) to download.\n\nThis will download them with a ${CONFIG.delayBetweenDownloads/1000}s delay between each.\n\nContinue?`)) { | |
| return; | |
| } | |
| isDownloading = true; | |
| shouldAbort = false; | |
| processedCount = 0; | |
| const startButton = document.getElementById('ing-pdf-downloader-btn'); | |
| const abortButton = document.getElementById('ing-pdf-abort-btn'); | |
| if (startButton) { | |
| startButton.style.display = 'none'; | |
| } | |
| if (abortButton) { | |
| abortButton.style.display = 'block'; | |
| } | |
| console.log(`Starting download of ${downloadQueue.length} PDFs...`); | |
| updateStatus(`Starting download of ${downloadQueue.length} PDFs...`); | |
| for (let i = 0; i < downloadQueue.length; i++) { | |
| if (shouldAbort) { | |
| console.log(`Download aborted by user after ${processedCount} files`); | |
| updateStatus(`Aborted! ${processedCount}/${downloadQueue.length} PDFs downloaded.`, false, 5000); | |
| break; | |
| } | |
| await downloadPDF(downloadQueue[i], i, downloadQueue.length); | |
| if (i < downloadQueue.length - 1 && !shouldAbort) { | |
| const remainingDelay = CONFIG.delayBetweenDownloads; | |
| updateStatus(`Waiting ${remainingDelay/1000}s before next download...`); | |
| await sleep(remainingDelay); | |
| } | |
| } | |
| isDownloading = false; | |
| shouldAbort = false; | |
| if (processedCount === downloadQueue.length) { | |
| console.log(`All downloads completed! (${processedCount}/${downloadQueue.length} successful)`); | |
| updateStatus(`Completed! ${processedCount}/${downloadQueue.length} PDFs downloaded.`, false, 5000); | |
| } | |
| if (startButton) { | |
| startButton.style.display = 'block'; | |
| } | |
| if (abortButton) { | |
| abortButton.style.display = 'none'; | |
| } | |
| } | |
| function updateStatus(message, isError = false, autoClear = 0) { | |
| const statusDiv = document.getElementById('ing-pdf-status'); | |
| if (statusDiv) { | |
| statusDiv.textContent = message; | |
| statusDiv.style.color = isError ? '#d32f2f' : '#333'; | |
| if (autoClear > 0) { | |
| setTimeout(() => { | |
| statusDiv.textContent = ''; | |
| }, autoClear); | |
| } | |
| } | |
| } | |
| function createDownloadButton() { | |
| if (document.getElementById('ing-pdf-downloader-btn')) { | |
| return; | |
| } | |
| const container = document.createElement('div'); | |
| container.id = 'ing-pdf-downloader-container'; | |
| container.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 10000; | |
| background: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| font-family: Arial, sans-serif; | |
| min-width: 250px; | |
| `; | |
| const button = document.createElement('button'); | |
| button.id = 'ing-pdf-downloader-btn'; | |
| button.textContent = CONFIG.buttonText; | |
| button.style.cssText = ` | |
| background: ${CONFIG.buttonColor}; | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| font-size: 14px; | |
| font-weight: bold; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| width: 100%; | |
| transition: background 0.3s; | |
| display: block; | |
| `; | |
| button.onmouseover = () => button.style.background = '#E55A00'; | |
| button.onmouseout = () => button.style.background = CONFIG.buttonColor; | |
| button.onclick = processDownloadQueue; | |
| const abortButton = document.createElement('button'); | |
| abortButton.id = 'ing-pdf-abort-btn'; | |
| abortButton.textContent = '🛑 Abort Download'; | |
| abortButton.style.cssText = ` | |
| background: #d32f2f; | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| font-size: 14px; | |
| font-weight: bold; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| width: 100%; | |
| transition: background 0.3s; | |
| display: none; | |
| `; | |
| abortButton.onmouseover = () => abortButton.style.background = '#b71c1c'; | |
| abortButton.onmouseout = () => abortButton.style.background = '#d32f2f'; | |
| abortButton.onclick = abortDownload; | |
| const statusDiv = document.createElement('div'); | |
| statusDiv.id = 'ing-pdf-status'; | |
| statusDiv.style.cssText = ` | |
| margin-top: 10px; | |
| font-size: 12px; | |
| color: #666; | |
| min-height: 20px; | |
| word-wrap: break-word; | |
| `; | |
| const infoDiv = document.createElement('div'); | |
| infoDiv.style.cssText = ` | |
| margin-top: 8px; | |
| font-size: 11px; | |
| color: #999; | |
| border-top: 1px solid #eee; | |
| padding-top: 8px; | |
| `; | |
| infoDiv.innerHTML = ` | |
| Delay: ${CONFIG.delayBetweenDownloads/1000}s between downloads<br> | |
| = Downloaded | |
| `; | |
| container.appendChild(button); | |
| container.appendChild(abortButton); | |
| container.appendChild(statusDiv); | |
| container.appendChild(infoDiv); | |
| document.body.appendChild(container); | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: scale(0); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| console.log('ING PDF Downloader: Button injected successfully'); | |
| } | |
| function init() { | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| return; | |
| } | |
| if (!window.location.href.includes('postbox_archiv')) { | |
| return; | |
| } | |
| setTimeout(() => { | |
| const table = document.querySelector('.ibbr-table-wrapper-postbox'); | |
| if (table) { | |
| createDownloadButton(); | |
| console.log('ING PDF Downloader: Ready'); | |
| } else { | |
| console.log('ING PDF Downloader: Table not found, retrying...'); | |
| setTimeout(init, 1000); | |
| } | |
| }, 500); | |
| } | |
| init(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment