Last active
July 24, 2026 11:13
-
-
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.2 | |
| // @description Safely download all PDFs from ING Postbox Archive in the background (no new tabs) | |
| // @author Michael Malura <michael@malura.de> | |
| // @match https://banking.ing.de/app/postbox/postbox* | |
| // @grant GM_download | |
| // @connect banking.ing.de | |
| // @run-at document-idle | |
| // @updateURL https://gist.githubusercontent.com/maluramichael/20dee51127c3fe4eeaed1f438bb366a4/raw/ing-postbox-downloader.user.js | |
| // @downloadURL https://gist.githubusercontent.com/maluramichael/20dee51127c3fe4eeaed1f438bb366a4/raw/ing-postbox-downloader.user.js | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const CONFIG = { | |
| delayBetweenDownloads: 400, | |
| 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"]'); | |
| 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; | |
| } | |
| function buildFilename(linkData) { | |
| const parts = [linkData.date, linkData.docType, linkData.description] | |
| .filter(Boolean) | |
| .join('_'); | |
| const safe = parts.replace(/[\/\\:*?"<>|]+/g, '-').replace(/\s+/g, ' ').trim(); | |
| return (safe || 'document').slice(0, 150) + '.pdf'; | |
| } | |
| function gmDownload(url, name) { | |
| return new Promise((resolve, reject) => { | |
| GM_download({ | |
| url: url, | |
| name: name, | |
| onload: () => resolve(), | |
| onerror: err => reject(new Error(err && err.error ? err.error : 'download failed')), | |
| ontimeout: () => reject(new Error('timeout')) | |
| }); | |
| }); | |
| } | |
| async function blobDownload(url, name) { | |
| const response = await fetch(url, { credentials: 'include' }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const blob = await response.blob(); | |
| const objectUrl = URL.createObjectURL(blob); | |
| const tempLink = document.createElement('a'); | |
| tempLink.href = objectUrl; | |
| tempLink.download = name; | |
| tempLink.style.display = 'none'; | |
| document.body.appendChild(tempLink); | |
| tempLink.click(); | |
| document.body.removeChild(tempLink); | |
| URL.revokeObjectURL(objectUrl); | |
| } | |
| 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 filename = buildFilename(linkData); | |
| if (typeof GM_download === 'function') { | |
| await gmDownload(linkData.url, filename); | |
| } else { | |
| await blobDownload(linkData.url, filename); | |
| } | |
| markRowAsProcessed(linkData.row); | |
| processedCount++; | |
| console.log(`Successfully downloaded ${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; | |
| bottom: 20px; | |
| left: 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/postbox')) { | |
| 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