Torn Bookie Game Extractor

Copies the currently selected bookie game (match + betting options) to clipboard

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==UserScript==
// @name         Torn Bookie Game Extractor
// @namespace    fishsoup.torn.bookie
// @version      1.1.1
// @description  Copies the currently selected bookie game (match + betting options) to clipboard
// @match        https://www.torn.com/page.php*
// @match        https://www.torn.com/bookie.php*
// @grant        GM_setClipboard
// @run-at       document-idle
// @license       MIT
// ==/UserScript==

(function () {
    'use strict';

    const BTN_FULL_ID = 'tbe-copy-full';
    const BTN_COMPACT_ID = 'tbe-copy-compact';
    const TOAST_ID = 'tbe-toast';
    const SEP = '-'.repeat(60);
    const COMPACT_RANGE = 3; // keep central +/- N lines for O/U and Asian Handicap

    const clean = s => (s || '').replace(/\s+/g, ' ').trim();
    const parseMult = s => {
        const m = (s || '').match(/x([\d.]+)/);
        return m ? parseFloat(m[1]) : null;
    };

    function onBookiePage() {
        return document.body && document.body.dataset.page === 'bookie';
    }

    function makeButton(id, label, bg, onClick) {
        const btn = document.createElement('button');
        btn.id = id;
        btn.textContent = label;
        Object.assign(btn.style, {
            padding: '10px 16px',
            background: bg,
            color: '#fff',
            border: '1px solid #1a242e',
            borderRadius: '6px',
            cursor: 'pointer',
            fontSize: '13px',
            fontFamily: 'Arial, sans-serif',
            boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
            minWidth: '130px',
        });
        btn.addEventListener('mouseenter', () => (btn.style.filter = 'brightness(1.2)'));
        btn.addEventListener('mouseleave', () => (btn.style.filter = 'none'));
        btn.addEventListener('click', onClick);
        return btn;
    }

    function ensureButtons() {
        const existing = document.getElementById('tbe-btn-container');
        if (!onBookiePage()) {
            existing?.remove();
            return;
        }
        if (existing) return;

        const container = document.createElement('div');
        container.id = 'tbe-btn-container';
        Object.assign(container.style, {
            position: 'fixed',
            bottom: '60px',
            right: '20px',
            zIndex: '99999',
            display: 'flex',
            flexDirection: 'column',
            gap: '8px',
        });

        container.appendChild(makeButton(BTN_FULL_ID, 'Copy Game', '#344556', () => handleClick(false)));
        container.appendChild(makeButton(BTN_COMPACT_ID, 'Copy Compact', '#4a6b3a', () => handleClick(true)));
        document.body.appendChild(container);
    }

    function toast(msg, isError) {
        document.getElementById(TOAST_ID)?.remove();
        const t = document.createElement('div');
        t.id = TOAST_ID;
        t.textContent = msg;
        Object.assign(t.style, {
            position: 'fixed',
            bottom: '150px',
            right: '20px',
            zIndex: '99999',
            padding: '8px 14px',
            background: isError ? '#a33' : '#2a6b3a',
            color: '#fff',
            borderRadius: '4px',
            fontSize: '12px',
            fontFamily: 'Arial, sans-serif',
            boxShadow: '0 2px 6px rgba(0,0,0,0.4)',
        });
        document.body.appendChild(t);
        setTimeout(() => t.remove(), 2500);
    }

    async function expandExtraOdds(active) {
        const links = active.querySelectorAll('a');
        for (const a of links) {
            if (/Show\s+\d+\s+additional/i.test(a.textContent)) {
                a.click();
                await new Promise(r => setTimeout(r, 250));
                break;
            }
        }
    }

    function extractActiveGame() {
        const active = document.querySelector('li.c-pointer.active');
        if (!active) return null;

        const sport = clean(active.querySelector('li.game')?.title);
        const matchP =
            active.querySelector('.matchName p') ||
            active.querySelector('.pop-game .name p');
        const matchTitle = clean(matchP?.title || matchP?.textContent);

        let matchName = matchTitle;
        let competition = '';
        const idx = matchTitle.indexOf(' - ');
        if (idx > -1) {
            matchName = matchTitle.slice(0, idx).trim();
            competition = matchTitle.slice(idx + 3).trim();
        }

        const startTitle = clean(active.querySelector('.state-wrap .state')?.title);
        const startTime = startTitle.replace(/^Due to start at\s*/i, '');

        const infoWrap = active.querySelector('.info-wrap');
        const markets = [];

        if (infoWrap) {
            const wraps = infoWrap.querySelectorAll('ul.bets-wrap');
            for (const wrap of wraps) {
                const firstLi = wrap.querySelector(':scope > li.bets');
                if (!firstLi) continue;
                const marketCell = firstLi.querySelector('.market-name-cell');
                if (!marketCell) continue;

                const marketName = clean(marketCell.querySelector('.bold')?.textContent);

                const betLis = wrap.querySelectorAll(':scope > li.bets');
                const bets = [];
                for (const betLi of betLis) {
                    if (betLi.querySelector('.market-name-cell')) continue;

                    const oddsCell = betLi.querySelector('.bet-cell.odds.fractional');
                    const multCell = betLi.querySelector('.bet-cell.odds.decimal');
                    const descCell = betLi.querySelector('.bet-cell.result');
                    if (!oddsCell || !descCell) continue;

                    const odds = clean(oddsCell.textContent).replace(/^Odds:\s*/i, '');
                    const mult = clean(multCell?.textContent).replace(/^Multiplier:\s*/i, '');
                    const desc = clean(
                        descCell.querySelector('span')?.textContent || descCell.textContent
                    );

                    const moneyGroup = betLi.querySelector('.input-money-group');
                    const suspended =
                        moneyGroup?.classList.contains('disabled') ||
                        betLi.querySelector('input.amount')?.value === 'Suspended';

                    bets.push({ desc, odds, mult, suspended });
                }
                if (bets.length) markets.push({ name: marketName, bets });
            }
        }

        return { sport, matchName, competition, startTime, markets };
    }

    function compactMarkets(markets) {
        const ouGroups = new Map();
        const ahEntries = [];
        const keep = new Set();

        markets.forEach((m, i) => {
            let mt = m.name.match(/^(.+?) Score Over\/Under ([\d.]+) (.+?) Full event$/i);
            if (mt) {
                const key = `team:${mt[1]}|${mt[3]}`;
                if (!ouGroups.has(key)) ouGroups.set(key, []);
                ouGroups.get(key).push({ market: m, line: parseFloat(mt[2]), index: i });
                return;
            }
            mt = m.name.match(/^Over\/Under ([\d.]+) (.+?) Full event$/i);
            if (mt) {
                const key = `total:${mt[2]}`;
                if (!ouGroups.has(key)) ouGroups.set(key, []);
                ouGroups.get(key).push({ market: m, line: parseFloat(mt[1]), index: i });
                return;
            }
            mt = m.name.match(/^Asian Handicap ([\d.]+) Full event$/i);
            if (mt) {
                const handicap = parseFloat(mt[1]);
                const firstDesc = m.bets[0]?.desc || '';
                const isFavGives = firstDesc.includes(`(-${handicap})`);
                ahEntries.push({ market: m, handicap, index: i, isFavGives });
                return;
            }
            keep.add(i);
        });

        ouGroups.forEach(entries => {
            if (entries.length <= 2 * COMPACT_RANGE + 1) {
                entries.forEach(e => keep.add(e.index));
                return;
            }
            entries.sort((a, b) => a.line - b.line);
            let centralIdx = 0;
            let minDiff = Infinity;
            entries.forEach((e, i) => {
                const o = parseMult(e.market.bets[0]?.mult);
                const u = parseMult(e.market.bets[1]?.mult);
                if (o != null && u != null) {
                    const d = Math.abs(o - u);
                    if (d < minDiff) { minDiff = d; centralIdx = i; }
                }
            });
            const start = Math.max(0, centralIdx - COMPACT_RANGE);
            const end = Math.min(entries.length - 1, centralIdx + COMPACT_RANGE);
            for (let i = start; i <= end; i++) keep.add(entries[i].index);
        });

        const ahByH = new Map();
        ahEntries.forEach(e => {
            const cur = ahByH.get(e.handicap);
            if (!cur) ahByH.set(e.handicap, e);
            else if (!cur.isFavGives && e.isFavGives) ahByH.set(e.handicap, e);
        });
        const deduped = Array.from(ahByH.values()).sort((a, b) => a.handicap - b.handicap);

        if (deduped.length <= 2 * COMPACT_RANGE + 1) {
            deduped.forEach(e => keep.add(e.index));
        } else {
            let centralIdx = 0;
            let minDiff = Infinity;
            deduped.forEach((e, i) => {
                const m1 = parseMult(e.market.bets[0]?.mult);
                const m2 = parseMult(e.market.bets[1]?.mult);
                if (m1 != null && m2 != null) {
                    const d = Math.abs(m1 - m2);
                    if (d < minDiff) { minDiff = d; centralIdx = i; }
                }
            });
            const start = Math.max(0, centralIdx - COMPACT_RANGE);
            const end = Math.min(deduped.length - 1, centralIdx + COMPACT_RANGE);
            for (let i = start; i <= end; i++) keep.add(deduped[i].index);
        }

        return markets.filter((_, i) => keep.has(i));
    }

    function formatGame(g, compact) {
        const lines = [];
        lines.push(SEP);
        lines.push(`Sport:       ${g.sport}`);
        lines.push(`Match:       ${g.matchName}`);
        if (g.competition) lines.push(`Competition: ${g.competition}`);
        if (g.startTime) lines.push(`Start:       ${g.startTime}`);
        lines.push('');

        const markets = compact ? compactMarkets(g.markets) : g.markets;

        for (const m of markets) {
            lines.push(`Market: ${m.name}`);
            const maxDesc = Math.max(...m.bets.map(b => b.desc.length));
            const maxOdds = Math.max(...m.bets.map(b => b.odds.length));
            for (const b of m.bets) {
                const desc = b.desc.padEnd(maxDesc + 2, ' ');
                const odds = b.odds.padEnd(maxOdds + 2, ' ');
                const mult = b.mult ? `(${b.mult})` : '';
                const sus = b.suspended ? ' [SUSPENDED]' : '';
                lines.push(`  ${desc}${odds}${mult}${sus}`);
            }
            lines.push('');
        }

        if (compact) {
            lines.push('Compact bookie options shown - advise if the full output is needed.');
            lines.push('');
        }
        lines.push(SEP);
        return lines.join('\n');
    }

    async function copyToClipboard(text) {
        try {
            if (typeof GM_setClipboard === 'function') {
                GM_setClipboard(text, 'text');
                return true;
            }
            await navigator.clipboard.writeText(text);
            return true;
        } catch (e) {
            const ta = document.createElement('textarea');
            ta.value = text;
            ta.style.position = 'fixed';
            ta.style.left = '-9999px';
            document.body.appendChild(ta);
            ta.select();
            const ok = document.execCommand('copy');
            ta.remove();
            return ok;
        }
    }

    async function handleClick(compact) {
        const active = document.querySelector('li.c-pointer.active');
        if (!active) {
            toast('No game expanded. Click a game first.', true);
            return;
        }
        await expandExtraOdds(active);

        const game = extractActiveGame();
        if (!game || !game.matchName) {
            toast('Could not parse game details.', true);
            return;
        }
        const text = formatGame(game, compact);
        const ok = await copyToClipboard(text);
        if (ok) toast(`Copied${compact ? ' (compact)' : ''}: ${game.matchName}`);
        else {
            toast('Copy failed. Output in console.', true);
            console.log(text);
        }
    }

    ensureButtons();
    new MutationObserver(() => ensureButtons()).observe(document.body, { childList: true, subtree: true });
})();