ClipboardUrler Pro (with M3U8 Quality Parsing)

Detect multiple video URLs, including M3U8 resolutions, and copy to clipboard

// ==UserScript==
// @name         ClipboardUrler Pro (with M3U8 Quality Parsing)
// @namespace    http://your.namespace
// @version      3.5
// @description  Detect multiple video URLs, including M3U8 resolutions, and copy to clipboard
// @license      MIT
// @match        *://*/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const videoExtensions = /\.(mp4|webm|mkv|avi|mov|m3u8|ts|flv|f4v|3gp|3g2|ogv|rm|rmvb|asf|wmv|m4v|vob|mpeg|mpg|m2ts|mts)(\?|#|$)/i;

    const fetchedManifests = new Set();
    const foundURLs = new Set();

    function isValidVideoURL(url) {
        return url && !url.startsWith('blob:') && /^https?:\/\//.test(url) && videoExtensions.test(url);
    }

    const wrapper = document.createElement('div');
    Object.assign(wrapper.style, {
        position: 'fixed',
        bottom: '10px',
        left: '10px',
        zIndex: '99999',
        display: 'flex',
        gap: '6px',
        alignItems: 'center',
        background: 'rgba(255,255,255,0.95)',
        padding: '6px',
        borderRadius: '8px',
        boxShadow: '0 0 6px rgba(0,0,0,0.4)',
        fontSize: '12px',
        fontFamily: 'sans-serif'
    });

    const select = document.createElement('select');
    Object.assign(select.style, {
        fontSize: '12px',
        padding: '4px',
        maxWidth: '280px'
    });

    const btn = document.createElement('button');
    btn.textContent = '📋 Copy';
    Object.assign(btn.style, {
        fontSize: '13px',
        padding: '4px 10px',
        cursor: 'pointer',
        borderRadius: '4px',
        border: 'none',
        background: '#333',
        color: '#fff'
    });

    const tooltip = document.createElement('div');
    tooltip.textContent = 'Copied!';
    Object.assign(tooltip.style, {
        position: 'fixed',
        bottom: '40px',
        left: '10px',
        padding: '4px 8px',
        background: '#333',
        color: '#fff',
        borderRadius: '4px',
        fontSize: '12px',
        opacity: '0',
        transition: 'opacity 0.3s',
        zIndex: '99999',
        pointerEvents: 'none'
    });

    wrapper.appendChild(select);
    wrapper.appendChild(btn);
    document.body.appendChild(wrapper);
    document.body.appendChild(tooltip);
    wrapper.style.display = 'none';

    btn.onclick = () => {
        const url = select.value;
        if (url) {
            navigator.clipboard.writeText(url).then(() => {
                tooltip.style.opacity = '1';
                setTimeout(() => (tooltip.style.opacity = '0'), 1000);
            });
        }
    };

    function addURL(url, label = '') {
        if (foundURLs.has(url)) return;
        foundURLs.add(url);
        const option = document.createElement('option');
        option.value = url;
        option.textContent = label || (url.length > 60 ? url.slice(0, 60) + '...' : url);
        select.appendChild(option);
    }

    async function parseM3U8(masterUrl) {
        if (fetchedManifests.has(masterUrl)) return;
        fetchedManifests.add(masterUrl);

        try {
            const res = await fetch(masterUrl);
            const text = await res.text();
            const baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);

            const lines = text.split('\n');
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                if (line.startsWith('#EXT-X-STREAM-INF')) {
                    const nextLine = lines[i + 1];
                    const matchRes = line.match(/RESOLUTION=(\d+x\d+)/);
                    const matchBw = line.match(/BANDWIDTH=(\d+)/);
                    let label = 'M3U8 Stream';
                    if (matchRes) label = matchRes[1];
                    else if (matchBw) label = `${(parseInt(matchBw[1]) / 1000).toFixed(0)}kbps`;

                    const finalURL = nextLine.startsWith('http') ? nextLine : baseUrl + nextLine;
                    addURL(finalURL, label);
                }
            }
        } catch (e) {
            console.warn('Failed to parse M3U8:', e);
        }
    }

    function detectAllVideoURLs() {
        const candidates = new Set();

        document.querySelectorAll('video, a[href], iframe').forEach(el => {
            let src = el.currentSrc || el.src || el.href || '';
            if (!src || typeof src !== 'string') return;

            if (el.tagName === 'IFRAME') {
                if (src.includes('youtube.com/embed/')) {
                    const id = src.split('/').pop().split('?')[0];
                    candidates.add(`https://www.youtube.com/watch?v=${id}`);
                } else if (src.includes('player.vimeo.com/video/')) {
                    const id = src.split('/').pop().split('?')[0];
                    candidates.add(`https://vimeo.com/${id}`);
                } else if (isValidVideoURL(src)) {
                    candidates.add(src);
                }
            } else if (isValidVideoURL(src)) {
                candidates.add(src);
            }
        });

        if (candidates.size > 0) {
            wrapper.style.display = 'flex';
            candidates.forEach(url => {
                addURL(url);

                if (url.endsWith('.m3u8')) {
                    parseM3U8(url);
                }
            });
        } else {
            wrapper.style.display = 'none';
        }
    }

    setInterval(detectAllVideoURLs, 4000); // check every few seconds
})();