Last active
May 4, 2026 16:06
-
-
Save maluramichael/b30fcd6a595e6409891666e4e5d86d33 to your computer and use it in GitHub Desktop.
Media Markt Gutscheine Parser
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 MediaMarkt Geschenkkarten Manager | |
| // @namespace https://lulububu.de | |
| // @version 1.0 | |
| // @description PDF-Gutscheine droppen, Kartennummer + PIN parsen, auf Checkout-Seite einlösen | |
| // @match https://www.mediamarkt.de/* | |
| // @require https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; | |
| const STORAGE_KEY = 'mm_gift_cards'; | |
| function getCards() { | |
| try { | |
| return JSON.parse(GM_getValue(STORAGE_KEY, '[]')); | |
| } catch { | |
| return []; | |
| } | |
| } | |
| function saveCards(cards) { | |
| GM_setValue(STORAGE_KEY, JSON.stringify(cards)); | |
| } | |
| function parseGiftCard(text) { | |
| const match = text.match(/(\d{13})\s+PIN:\s*(\d{4})/); | |
| if (!match) return null; | |
| const betragMatch = text.match(/Betrag:\s*([\d,.]+)/i); | |
| return { | |
| number: match[1], | |
| pin: match[2], | |
| amount: betragMatch ? betragMatch[1] : '?', | |
| addedAt: new Date().toISOString(), | |
| used: false | |
| }; | |
| } | |
| async function extractTextFromPdf(arrayBuffer) { | |
| const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; | |
| let fullText = ''; | |
| for (let i = 1; i <= pdf.numPages; i++) { | |
| const page = await pdf.getPage(i); | |
| const content = await page.getTextContent(); | |
| fullText += content.items.map(item => item.str).join(' ') + '\n'; | |
| } | |
| return fullText; | |
| } | |
| function setNativeValue(el, value) { | |
| const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; | |
| setter.call(el, value); | |
| el.dispatchEvent(new Event('input', { bubbles: true })); | |
| el.dispatchEvent(new Event('change', { bubbles: true })); | |
| } | |
| function fillGiftCard(card) { | |
| const cardNumberInput = document.getElementById('mms-gift-card__cardNumber'); | |
| const pinInput = document.getElementById('mms-gift-card__pin'); | |
| if (!cardNumberInput || !pinInput) { | |
| showToast('Geschenkkarten-Formular nicht gefunden. Bitte zuerst öffnen.', 'error'); | |
| return false; | |
| } | |
| setNativeValue(cardNumberInput, card.number); | |
| setNativeValue(pinInput, card.pin); | |
| showToast(`Karte ${card.number} (${card.amount}€) eingetragen. Submit-Button manuell klicken.`, 'success'); | |
| return true; | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| const colors = { success: '#2e7d32', error: '#c62828', info: '#1565c0' }; | |
| Object.assign(toast.style, { | |
| position: 'fixed', bottom: '20px', right: '20px', zIndex: '999999', | |
| padding: '14px 20px', borderRadius: '8px', color: '#fff', fontSize: '14px', | |
| backgroundColor: colors[type] || colors.info, boxShadow: '0 4px 12px rgba(0,0,0,0.3)', | |
| transition: 'opacity 0.3s', maxWidth: '400px' | |
| }); | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 4000); | |
| } | |
| function createUI() { | |
| const container = document.createElement('div'); | |
| container.id = 'mm-gc-manager'; | |
| Object.assign(container.style, { | |
| position: 'fixed', top: '80px', left: '20px', zIndex: '99999', | |
| width: '360px', backgroundColor: '#fff', borderRadius: '12px', | |
| boxShadow: '0 8px 32px rgba(0,0,0,0.2)', fontFamily: 'Arial, sans-serif', | |
| overflow: 'hidden', transition: 'all 0.3s' | |
| }); | |
| const header = document.createElement('div'); | |
| Object.assign(header.style, { | |
| backgroundColor: '#df0000', color: '#fff', padding: '12px 16px', | |
| display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' | |
| }); | |
| header.innerHTML = '<strong>🎁 Geschenkkarten Manager</strong><span id="mm-gc-toggle">▼</span>'; | |
| const body = document.createElement('div'); | |
| body.id = 'mm-gc-body'; | |
| Object.assign(body.style, { padding: '16px', maxHeight: '500px', overflowY: 'auto' }); | |
| const dropZone = document.createElement('div'); | |
| dropZone.id = 'mm-gc-dropzone'; | |
| Object.assign(dropZone.style, { | |
| border: '2px dashed #ccc', borderRadius: '8px', padding: '24px', textAlign: 'center', | |
| color: '#888', marginBottom: '12px', transition: 'all 0.2s', cursor: 'pointer' | |
| }); | |
| dropZone.textContent = 'PDF-Gutscheine hier ablegen oder klicken'; | |
| const fileInput = document.createElement('input'); | |
| fileInput.type = 'file'; | |
| fileInput.accept = '.pdf'; | |
| fileInput.multiple = true; | |
| fileInput.style.display = 'none'; | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', () => { | |
| if (fileInput.files.length) handleFiles(fileInput.files); | |
| fileInput.value = ''; | |
| }); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = '#df0000'; | |
| dropZone.style.color = '#df0000'; | |
| }); | |
| dropZone.addEventListener('dragleave', () => { | |
| dropZone.style.borderColor = '#ccc'; | |
| dropZone.style.color = '#888'; | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = '#ccc'; | |
| dropZone.style.color = '#888'; | |
| const files = [...e.dataTransfer.files].filter(f => f.type === 'application/pdf'); | |
| if (files.length) handleFiles(files); | |
| }); | |
| const cardList = document.createElement('div'); | |
| cardList.id = 'mm-gc-list'; | |
| body.appendChild(dropZone); | |
| body.appendChild(fileInput); | |
| body.appendChild(cardList); | |
| container.appendChild(header); | |
| container.appendChild(body); | |
| document.body.appendChild(container); | |
| let collapsed = false; | |
| header.addEventListener('click', () => { | |
| collapsed = !collapsed; | |
| body.style.display = collapsed ? 'none' : 'block'; | |
| document.getElementById('mm-gc-toggle').textContent = collapsed ? '▶' : '▼'; | |
| }); | |
| renderCardList(); | |
| } | |
| function renderCardList() { | |
| const list = document.getElementById('mm-gc-list'); | |
| if (!list) return; | |
| const cards = getCards(); | |
| const unused = cards.filter(c => !c.used); | |
| const used = cards.filter(c => c.used); | |
| if (cards.length === 0) { | |
| list.innerHTML = '<p style="color:#888;text-align:center;font-size:13px;">Keine Gutscheine vorhanden</p>'; | |
| return; | |
| } | |
| let html = ''; | |
| if (unused.length) { | |
| html += '<div style="font-size:12px;color:#888;margin-bottom:6px;font-weight:bold;">Verfügbar</div>'; | |
| unused.forEach((card) => { | |
| const globalIdx = cards.indexOf(card); | |
| html += cardRowHtml(card, globalIdx, false); | |
| }); | |
| } | |
| if (used.length) { | |
| html += '<div style="font-size:12px;color:#888;margin:10px 0 6px;font-weight:bold;">Verwendet</div>'; | |
| used.forEach((card) => { | |
| const globalIdx = cards.indexOf(card); | |
| html += cardRowHtml(card, globalIdx, true); | |
| }); | |
| } | |
| list.innerHTML = html; | |
| list.querySelectorAll('[data-action="use"]').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const idx = parseInt(btn.dataset.idx); | |
| const allCards = getCards(); | |
| if (fillGiftCard(allCards[idx])) { | |
| allCards[idx].used = true; | |
| saveCards(allCards); | |
| renderCardList(); | |
| } | |
| }); | |
| }); | |
| list.querySelectorAll('[data-action="delete"]').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const idx = parseInt(btn.dataset.idx); | |
| const allCards = getCards(); | |
| allCards.splice(idx, 1); | |
| saveCards(allCards); | |
| renderCardList(); | |
| }); | |
| }); | |
| list.querySelectorAll('[data-action="unuse"]').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const idx = parseInt(btn.dataset.idx); | |
| const allCards = getCards(); | |
| allCards[idx].used = false; | |
| saveCards(allCards); | |
| renderCardList(); | |
| }); | |
| }); | |
| } | |
| function cardRowHtml(card, idx, isUsed) { | |
| const bg = isUsed ? '#f5f5f5' : '#fff'; | |
| const textColor = isUsed ? '#aaa' : '#333'; | |
| const btnStyle = 'border:none;border-radius:4px;padding:4px 8px;font-size:11px;cursor:pointer;'; | |
| let actions = ''; | |
| if (!isUsed) { | |
| actions = `<button data-action="use" data-idx="${idx}" style="${btnStyle}background:#df0000;color:#fff;">Einlösen</button>`; | |
| } else { | |
| actions = `<button data-action="unuse" data-idx="${idx}" style="${btnStyle}background:#888;color:#fff;" title="Zurücksetzen">↩</button>`; | |
| } | |
| actions += ` <button data-action="delete" data-idx="${idx}" style="${btnStyle}background:#eee;color:#666;" title="Löschen">✕</button>`; | |
| return `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;margin-bottom:4px;border-radius:6px;background:${bg};border:1px solid #eee;"> | |
| <div style="color:${textColor};font-size:13px;"> | |
| <div style="font-weight:bold;">${card.number}</div> | |
| </div> | |
| <div>${actions}</div> | |
| </div>`; | |
| } | |
| async function handleFiles(files) { | |
| const cards = getCards(); | |
| let added = 0; | |
| for (const file of files) { | |
| try { | |
| const buffer = await file.arrayBuffer(); | |
| const text = await extractTextFromPdf(buffer); | |
| const card = parseGiftCard(text); | |
| if (!card) { | |
| showToast(`${file.name}: Keine Kartendaten gefunden`, 'error'); | |
| continue; | |
| } | |
| if (cards.some(c => c.number === card.number)) { | |
| showToast(`${file.name}: Karte ${card.number} bereits vorhanden`, 'info'); | |
| continue; | |
| } | |
| cards.push(card); | |
| added++; | |
| } catch (err) { | |
| showToast(`${file.name}: Fehler beim Lesen - ${err.message}`, 'error'); | |
| } | |
| } | |
| if (added > 0) { | |
| saveCards(cards); | |
| renderCardList(); | |
| showToast(`${added} Gutschein(e) hinzugefügt`, 'success'); | |
| } | |
| } | |
| createUI(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment