Export Stream Link to .m3u for Stremio Web

Auto-clicks "Copy stream link", grabs clipboard, saves .m3u — with toggle, shortcut, persistence, and proper title detection!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Export Stream Link to .m3u for Stremio Web
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Auto-clicks "Copy stream link", grabs clipboard, saves .m3u — with toggle, shortcut, persistence, and proper title detection!
// @author       heapsofjoy
// @match        https://web.stremio.com/*
// @grant        clipboardRead
// ==/UserScript==

(function () {
    'use strict';

    let enabled = localStorage.getItem('m3uExportEnabled') !== 'false';

    const toggle = document.createElement('div');
    toggle.textContent = `📄 .m3u: ${enabled ? 'ON' : 'OFF'}`;
    Object.assign(toggle.style, {
        position: 'fixed',
        bottom: '10px',
        left: '10px',
        zIndex: 9999,
        padding: '4px 10px',
        backgroundColor: '#1f1f1fbb',
        color: '#ffffffcc',
        fontSize: '12px',
        fontFamily: 'sans-serif',
        borderRadius: '6px',
        cursor: 'pointer',
        userSelect: 'none',
        opacity: enabled ? '1' : '0.6',
        transition: 'all 0.3s ease',
    });

    toggle.addEventListener('mouseenter', () => {
        toggle.style.backgroundColor = '#2ecc71';
        toggle.style.color = '#fff';
    });

    toggle.addEventListener('mouseleave', () => {
        toggle.style.backgroundColor = '#1f1f1fbb';
        toggle.style.color = '#ffffffcc';
    });

    toggle.addEventListener('click', () => {
        enabled = !enabled;
        toggle.textContent = `📄 .m3u: ${enabled ? 'ON' : 'OFF'}`;
        toggle.style.opacity = enabled ? '1' : '0.6';
        localStorage.setItem('m3uExportEnabled', enabled);
        console.log(`📄 .m3u export is now ${enabled ? 'enabled ✅' : 'disabled ❌'}`);
    });

    document.body.appendChild(toggle);

    // Hide toggle on player page
    function updateToggleVisibility() {
        toggle.style.display = location.hash.startsWith('#/player') ? 'none' : 'block';
    }
    updateToggleVisibility();
    const observer = new MutationObserver(updateToggleVisibility);
    observer.observe(document.body, { childList: true, subtree: true });
    window.addEventListener('hashchange', updateToggleVisibility);

    document.addEventListener('keydown', (e) => {
        if (e.shiftKey && e.key === 'M') {
            toggle.click();
        }
    });


    document.addEventListener('contextmenu', () => {
        if (!enabled) return;

        setTimeout(() => {
            const copyButton = [...document.querySelectorAll('div[title="Copy stream link"]')]
                .find(el => el?.innerText?.toLowerCase().includes('copy stream link'));

            if (!copyButton) {
                console.warn('No "Copy stream link" button found');
                return;
            }

            copyButton.click();

            setTimeout(async () => {
                try {
                    const text = await navigator.clipboard.readText();
                    if (!text.startsWith('http')) {
                        alert('❌ Invalid stream URL in clipboard');
                        return;
                    }

                    // 1. Try logo image title
                    let titleImg = document.querySelector('img.logo-X3hTV[title]');
                    let title = titleImg?.getAttribute('title')?.trim();

                    // 2. Fallback: logo placeholder div inside meta-info container
                    if (!title) {
                        const placeholder = document.querySelector('.meta-info-container-ub8AH > div[class^="logo-placeholder-"]');
                        title = placeholder?.textContent?.trim();
                    }

                    // 3. Fallback: document title
                    if (!title) {
                        title = document.title;
                    }

                    // Sanitize
                    title = title.replace(/[^\w\d]+/g, '_').slice(0, 40) || 'stream';


                    const m3u = `#EXTM3U\n${text}`;
                    const blob = new Blob([m3u], { type: 'audio/x-mpegurl' });
                    const a = document.createElement('a');
                    a.href = URL.createObjectURL(blob);
                    a.download = `${title}.m3u`;
                    a.click();
                    URL.revokeObjectURL(a.href);
                    console.log(`✅ Saved .m3u as "${title}.m3u" with link:`, text);
                } catch (err) {
                    alert('❌ Failed to access clipboard. Permissions may be blocked.\n' + err);
                }
            }, 500);
        }, 150);
    });
})();