Skip to content

Instantly share code, notes, and snippets.

@maluramichael
Last active May 4, 2026 16:06
Show Gist options
  • Select an option

  • Save maluramichael/b30fcd6a595e6409891666e4e5d86d33 to your computer and use it in GitHub Desktop.

Select an option

Save maluramichael/b30fcd6a595e6409891666e4e5d86d33 to your computer and use it in GitHub Desktop.
Media Markt Gutscheine Parser
// ==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