Universal Video Share Button with M3U8 Support

Adds a floating share button that appears when videos are detected. Hold down to copy instead of share. Auto-detects and downloads M3U8 playlists.

当前为 2025-11-06 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Universal Video Share Button with M3U8 Support
// @namespace    http://tampermonkey.net/
// @version      4.5
// @description  Adds a floating share button that appears when videos are detected. Hold down to copy instead of share. Auto-detects and downloads M3U8 playlists.
// @author       Minoa
// @license MIT
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/m3u8-parser.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    var floatingButton = null;
    var pressTimer = null;
    var isLongPress = false;
    var checkInterval = null;
    var detectedM3U8s = [];
    var detectedM3U8Urls = [];
    var allDetectedVideos = new Map();
    var processedVideos = new Map(); // Track what was done with each video
    var downloadedBlobs = new Map(); // Store downloaded M3U8 blobs

    // Color scheme
    const COLORS = {
        button: '#55423d',
        buttonHover: '#6b5651',
        icon: '#ffc0ad',
        text: '#fff3ec'
    };

    // Common video selectors across different sites
    var VIDEO_SELECTORS = [
        'video',
        '.video-player video',
        '.player video',
        '#player video',
        '.video-container video',
        '[class*="video"] video',
        '[class*="player"] video',
        'iframe[src*="youtube.com"]',
        'iframe[src*="vimeo.com"]',
        'iframe[src*="dailymotion.com"]',
        'iframe[src*="twitch.tv"]'
    ];

    // Video extensions to detect
    var VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv', '.m4v', '.3gp'];

    // M3U8 Detection - Hook into network requests
    (function setupNetworkDetection() {
        // Hook fetch
        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            const promise = originalFetch.apply(this, args);
            const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;

            promise.then(response => {
                if (url) {
                    // Don't track .ts segments
                    if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts')) {
                        checkUrlForVideo(url);
                    }
                    if (url.includes('.m3u8') || url.includes('.m3u')) {
                        detectM3U8(url);
                    }
                }
                return response;
            }).catch(e => {
                throw e;
            });
            return promise;
        };

        // Hook XMLHttpRequest
        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(...args) {
            this.addEventListener("load", function() {
                try {
                    const url = args[1];
                    if (url) {
                        // Don't track .ts segments
                        if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts')) {
                            checkUrlForVideo(url);
                        }
                        if (url.includes('.m3u8') || url.includes('.m3u')) {
                            detectM3U8(url);
                        }
                        if (this.responseText && this.responseText.trim().startsWith("#EXTM3U")) {
                            detectM3U8(url);
                        }
                    }
                } catch(e) {}
            });
            return originalOpen.apply(this, args);
        };

        // Hook Response.text() to catch m3u8 content
        const originalText = Response.prototype.text;
        Response.prototype.text = function() {
            return originalText.call(this).then(text => {
                if (text.trim().startsWith("#EXTM3U")) {
                    detectM3U8(this.url);
                }
                return text;
            });
        };

        // Hook video element src changes
        const originalSetAttribute = Element.prototype.setAttribute;
        Element.prototype.setAttribute = function(name, value) {
            if (this.tagName === 'VIDEO' || this.tagName === 'SOURCE') {
                if (name === 'src' && value) {
                    checkUrlForVideo(value);
                }
            }
            return originalSetAttribute.call(this, name, value);
        };
    })();

    function checkUrlForVideo(url) {
        try {
            // Skip blob URLs and .ts segments
            if (url.startsWith('blob:')) return;
            if (url.match(/seg-\d+-.*\.ts/i)) return;
            if (url.endsWith('.ts')) return;

            // Skip if it's already an M3U8 (will be handled separately)
            if (url.includes('.m3u8') || url.includes('.m3u')) return;

            // Check if it's a video URL
            const lowerUrl = url.toLowerCase();
            const isVideo = VIDEO_EXTENSIONS.some(ext => lowerUrl.includes(ext));

            if (isVideo) {
                const fullUrl = new URL(url, location.href).href;
                if (!allDetectedVideos.has(fullUrl)) {
                    allDetectedVideos.set(fullUrl, {
                        url: fullUrl,
                        type: 'video',
                        timestamp: Date.now(),
                        title: 'Video - ' + getFilenameFromUrl(fullUrl)
                    });
                    checkForVideos();
                }
            }
        } catch(e) {}
    }

    function getFilenameFromUrl(url) {
        try {
            const pathname = new URL(url).pathname;
            const filename = pathname.split('/').pop();
            return filename || 'Unknown';
        } catch(e) {
            return 'Unknown';
        }
    }

    function getTimeAgo(timestamp) {
        const seconds = Math.floor((Date.now() - timestamp) / 1000);
        if (seconds < 60) return 'just now';
        const minutes = Math.floor(seconds / 60);
        if (minutes < 60) return minutes + 'm ago';
        const hours = Math.floor(minutes / 60);
        if (hours < 24) return hours + 'h ago';
        const days = Math.floor(hours / 24);
        return days + 'd ago';
    }

    function getBaseUrlFromSegment(segmentUrl) {
        try {
            const url = new URL(segmentUrl);
            const path = url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);
            return url.origin + path;
        } catch(e) {
            return null;
        }
    }

    function isSegmentOfPlaylist(videoUrl) {
        // Check if this video URL is a segment of a detected playlist
        if (!videoUrl.endsWith('.ts')) return false;
        if (!videoUrl.match(/seg-\d+-/i)) return false;

        const baseUrl = getBaseUrlFromSegment(videoUrl);
        if (!baseUrl) return false;

        // Check if any M3U8 shares the same base path
        for (const [m3u8Url, data] of allDetectedVideos.entries()) {
            if (data.type === 'm3u8') {
                const m3u8Base = getBaseUrlFromSegment(m3u8Url);
                if (m3u8Base && baseUrl.startsWith(m3u8Base)) {
                    return true;
                }
            }
        }

        return false;
    }

    function hasM3U8Playlist() {
        return Array.from(allDetectedVideos.values()).some(v => v.type === 'm3u8');
    }

    function isMasterPlaylist(url, manifest) {
        // Master playlists have playlists, not segments
        if (url.includes('master.m3u8')) return true;
        if (manifest.playlists && manifest.playlists.length > 0 && (!manifest.segments || manifest.segments.length === 0)) {
            return true;
        }
        return false;
    }

    function shouldFilterM3U8(url, manifest) {
        // If this is a master playlist and we have index playlists from the same base, filter it out
        if (!isMasterPlaylist(url, manifest)) return false;

        const baseUrl = getBaseUrlFromSegment(url);
        if (!baseUrl) return false;

        // Check if we have any index-*.m3u8 from the same base
        for (const [otherUrl, data] of allDetectedVideos.entries()) {
            if (data.type === 'm3u8' && otherUrl !== url) {
                const otherBase = getBaseUrlFromSegment(otherUrl);
                if (otherBase === baseUrl && otherUrl.includes('index-')) {
                    return true; // Filter out this master playlist
                }
            }
        }

        return false;
    }

    async function detectM3U8(url) {
        try {
            if (url.startsWith('blob:')) return;

            url = new URL(url, location.href).href;

            if (detectedM3U8Urls.includes(url)) return;
            detectedM3U8Urls.push(url);

            const response = await fetch(url);
            const content = await response.text();

            const parser = new m3u8Parser.Parser();
            parser.push(content);
            parser.end();
            const manifest = parser.manifest;

            let duration = 0;
            if (manifest.segments) {
                for (var s = 0; s < manifest.segments.length; s++) {
                    duration += manifest.segments[s].duration;
                }
            }

            const m3u8Data = {
                url: url,
                manifest: manifest,
                content: content,
                duration: duration,
                title: 'M3U8 - ' + (duration ? Math.ceil(duration / 60) + 'min' : 'Unknown'),
                timestamp: Date.now()
            };

            detectedM3U8s.push(m3u8Data);

            allDetectedVideos.set(url, {
                url: url,
                type: 'm3u8',
                timestamp: Date.now(),
                title: m3u8Data.title,
                m3u8Data: m3u8Data
            });

            checkForVideos();
        } catch(e) {
            console.error("M3U8 parse error:", e);
        }
    }

    function getVideoUrl(videoElement) {
        if (videoElement.tagName === 'VIDEO') {
            if (videoElement.currentSrc && !videoElement.currentSrc.startsWith('blob:'))
                return videoElement.currentSrc;
            if (videoElement.src && !videoElement.src.startsWith('blob:'))
                return videoElement.src;

            var sources = videoElement.querySelectorAll('source');
            for (var i = 0; i < sources.length; i++) {
                if (sources[i].src && !sources[i].src.startsWith('blob:'))
                    return sources[i].src;
            }
        }

        if (videoElement.tagName === 'IFRAME') {
            return videoElement.src;
        }

        return null;
    }

    function getUniqueVideos() {
        var videos = [];
        var seenUrls = new Set();

        // Add all detected videos from network
        allDetectedVideos.forEach(function(videoData) {
            // Skip .ts segments if we have playlists
            if (hasM3U8Playlist() && isSegmentOfPlaylist(videoData.url)) {
                return;
            }

            // Skip master playlists if we have index playlists
            if (videoData.type === 'm3u8' && shouldFilterM3U8(videoData.url, videoData.m3u8Data.manifest)) {
                return;
            }

            if (!seenUrls.has(videoData.url)) {
                seenUrls.add(videoData.url);
                videos.push(videoData);
            }
        });

        // Add regular videos from page
        for (var s = 0; s < VIDEO_SELECTORS.length; s++) {
            var elements = document.querySelectorAll(VIDEO_SELECTORS[s]);

            for (var i = 0; i < elements.length; i++) {
                var element = elements[i];
                var rect = element.getBoundingClientRect();

                if (rect.width > 100 && rect.height > 100) {
                    var url = getVideoUrl(element);
                    if (url && !seenUrls.has(url)) {
                        seenUrls.add(url);
                        const videoData = {
                            type: 'video',
                            element: element,
                            url: url,
                            title: element.title || element.alt || ('Video ' + (videos.length + 1)),
                            timestamp: Date.now()
                        };
                        videos.push(videoData);
                        allDetectedVideos.set(url, videoData);
                    }
                }
            }
        }

        // Check iframes for videos
        var iframes = document.querySelectorAll('iframe');
        for (var i = 0; i < iframes.length; i++) {
            try {
                var iframeDoc = iframes[i].contentDocument || iframes[i].contentWindow.document;
                if (iframeDoc) {
                    var iframeVideos = iframeDoc.querySelectorAll('video');
                    for (var v = 0; v < iframeVideos.length; v++) {
                        var url = getVideoUrl(iframeVideos[v]);
                        if (url && !seenUrls.has(url)) {
                            seenUrls.add(url);
                            const videoData = {
                                type: 'video',
                                element: iframeVideos[v],
                                url: url,
                                title: 'Iframe Video ' + (videos.length + 1),
                                timestamp: Date.now()
                            };
                            videos.push(videoData);
                            allDetectedVideos.set(url, videoData);
                        }
                    }
                }
            } catch(e) {
                // Cross-origin iframe, skip
            }
        }

        return videos;
    }

    function getButtonIcon(videos) {
        if (videos.length > 1) return '⇓';
        if (videos.length === 1) {
            return videos[0].type === 'm3u8' ? '⇣' : '↯';
        }
        return '▶';
    }

    function createFloatingButton() {
        if (floatingButton) return floatingButton;

        var container = document.createElement('div');
        container.id = 'universal-video-share-container';
        container.style.cssText = 'position: fixed; top: 15px; left: 15px; width: 40px; height: 40px; z-index: 999999;';

        var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '40');
        svg.setAttribute('height', '40');
        svg.style.cssText = 'position: absolute; top: 0; left: 0; transform: rotate(-90deg);';

        var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        circle.setAttribute('cx', '20');
        circle.setAttribute('cy', '20');
        circle.setAttribute('r', '18');
        circle.setAttribute('fill', 'none');
        circle.setAttribute('stroke', '#4ade80');
        circle.setAttribute('stroke-width', '3');
        circle.setAttribute('stroke-dasharray', '113');
        circle.setAttribute('stroke-dashoffset', '113');
        circle.setAttribute('stroke-linecap', 'round');
        circle.style.cssText = 'transition: stroke-dashoffset 0.3s ease;';
        circle.id = 'progress-circle';

        svg.appendChild(circle);
        container.appendChild(svg);

        floatingButton = document.createElement('div');
        floatingButton.innerHTML = '▶';
        floatingButton.id = 'universal-video-share-float';

        floatingButton.style.cssText = `position: absolute; top: 2px; left: 2px; width: 36px; height: 36px; background: ${COLORS.button}; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: ${COLORS.icon}; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 50%; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; user-select: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);`;

        container.appendChild(floatingButton);
        floatingButton.progressCircle = circle;

        floatingButton.addEventListener('mouseenter', function() {
            this.style.background = COLORS.buttonHover;
            this.style.transform = 'scale(1.05)';
        });

        floatingButton.addEventListener('mouseleave', function() {
            this.style.background = COLORS.button;
            this.style.transform = 'scale(1)';
        });

        floatingButton.addEventListener('mousedown', function(e) {
            e.preventDefault();
            isLongPress = false;
            pressTimer = setTimeout(function() {
                isLongPress = true;
                floatingButton.style.background = 'rgba(74, 222, 128, 0.8)';
                floatingButton.innerHTML = '⎘';
            }, 500);
        });

        floatingButton.addEventListener('mouseup', function(e) {
            e.preventDefault();
            clearTimeout(pressTimer);

            var videos = getUniqueVideos();
            floatingButton.style.background = COLORS.button;
            floatingButton.innerHTML = getButtonIcon(videos);

            if (isLongPress) {
                handleCopy();
            } else {
                handleShare();
            }
        });

        floatingButton.addEventListener('mouseleave', function() {
            clearTimeout(pressTimer);
            var videos = getUniqueVideos();
            floatingButton.style.background = COLORS.button;
            floatingButton.innerHTML = getButtonIcon(videos);
        });

        floatingButton.addEventListener('touchstart', function(e) {
            e.preventDefault();
            isLongPress = false;
            pressTimer = setTimeout(function() {
                isLongPress = true;
                floatingButton.style.background = 'rgba(74, 222, 128, 0.8)';
                floatingButton.innerHTML = '⎘';
            }, 500);
        });

        floatingButton.addEventListener('touchend', function(e) {
            e.preventDefault();
            clearTimeout(pressTimer);

            var videos = getUniqueVideos();
            floatingButton.style.background = COLORS.button;
            floatingButton.innerHTML = getButtonIcon(videos);

            if (isLongPress) {
                handleCopy();
            } else {
                handleShare();
            }
        });

        document.body.appendChild(container);
        return floatingButton;
    }

    function updateProgress(percent) {
        if (!floatingButton || !floatingButton.progressCircle) return;
        var offset = 113 - (113 * percent / 100);
        floatingButton.progressCircle.setAttribute('stroke-dashoffset', offset);
    }

    function resetProgress() {
        if (!floatingButton || !floatingButton.progressCircle) return;
        floatingButton.progressCircle.setAttribute('stroke-dashoffset', '113');
    }

    function handleShare() {
        var videos = getUniqueVideos();

        if (videos.length === 0) {
            showNotification('No videos found', 'error');
            return;
        }

        if (videos.length === 1) {
            shareVideo(videos[0]);
        } else {
            showVideoSelector(videos, 'share');
        }
    }

    function handleCopy() {
        var videos = getUniqueVideos();

        if (videos.length === 0) {
            showNotification('No videos found', 'error');
            return;
        }

        if (videos.length === 1) {
            // For M3U8, still download but skip share
            if (videos[0].type === 'm3u8') {
                downloadM3U8(videos[0], true); // Force download
            } else {
                copyVideoUrl(videos[0]);
            }
        } else {
            showVideoSelector(videos, 'copy');
        }
    }

    function showVideoSelector(videos, action) {
        var existingSelector = document.querySelector('#video-selector-popup');
        if (existingSelector) {
            existingSelector.remove();
        }

        var popup = document.createElement('div');
        popup.id = 'video-selector-popup';
        popup.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); z-index: 9999999; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box;';

        var container = document.createElement('div');
        container.style.cssText = 'background: rgba(20, 20, 20, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 24px; max-width: 600px; max-height: 70%; overflow-y: auto; position: relative; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);';

        var closeButton = document.createElement('button');
        closeButton.innerHTML = '✕';
        closeButton.style.cssText = `position: absolute; top: 12px; right: 12px; background: rgba(255, 255, 255, 0.1); border: none; font-size: 16px; cursor: pointer; color: ${COLORS.text}; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s;`;

        closeButton.addEventListener('click', function() {
            popup.remove();
        });

        closeButton.addEventListener('mouseenter', function() {
            this.style.background = 'rgba(255, 255, 255, 0.2)';
        });

        closeButton.addEventListener('mouseleave', function() {
            this.style.background = 'rgba(255, 255, 255, 0.1)';
        });

        var title = document.createElement('h3');
        title.textContent = 'Select Video to ' + (action.charAt(0).toUpperCase() + action.slice(1));
        title.style.cssText = `margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 16px; font-weight: 600; text-align: center;`;

        container.appendChild(closeButton);
        container.appendChild(title);

        // Sort videos by timestamp (newest first)
        videos.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));

        for (var i = 0; i < videos.length; i++) {
            var videoData = videos[i];
            var videoItem = document.createElement('div');
            videoItem.style.cssText = 'margin-bottom: 12px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; background: rgba(255, 255, 255, 0.05);';

            (function(currentVideoData) {
                videoItem.addEventListener('mouseenter', function() {
                    this.style.borderColor = 'rgba(255, 255, 255, 0.3)';
                    this.style.background = 'rgba(255, 255, 255, 0.1)';
                });

                videoItem.addEventListener('mouseleave', function() {
                    this.style.borderColor = 'rgba(255, 255, 255, 0.1)';
                    this.style.background = 'rgba(255, 255, 255, 0.05)';
                });

                var headerDiv = document.createElement('div');
                headerDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;';

                var typeBadge = document.createElement('span');
                typeBadge.textContent = currentVideoData.type === 'm3u8' ? 'M3U8' : 'VIDEO';
                typeBadge.style.cssText = 'display: inline-block; background: ' + (currentVideoData.type === 'm3u8' ? 'rgba(239, 68, 68, 0.8)' : 'rgba(59, 130, 246, 0.8)') + `; color: ${COLORS.text}; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;`;

                var timeAgo = document.createElement('span');
                timeAgo.textContent = getTimeAgo(currentVideoData.timestamp || Date.now());
                timeAgo.style.cssText = `color: ${COLORS.text}; opacity: 0.6; font-size: 10px;`;

                headerDiv.appendChild(typeBadge);
                headerDiv.appendChild(timeAgo);
                videoItem.appendChild(headerDiv);

                var videoInfo = document.createElement('div');
                videoInfo.innerHTML = `<div style="color: ${COLORS.text}; font-size: 13px; font-weight: 500; margin-bottom: 4px;">` + currentVideoData.title + `</div><div style="color: ${COLORS.text}; opacity: 0.5; font-size: 11px; word-break: break-all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">` + currentVideoData.url + '</div>';
                videoItem.appendChild(videoInfo);

                videoItem.addEventListener('click', function() {
                    popup.remove();
                    if (action === 'share') {
                        shareVideo(currentVideoData);
                    } else {
                        if (currentVideoData.type === 'm3u8') {
                            downloadM3U8(currentVideoData, true); // Force download for copy
                        } else {
                            copyVideoUrl(currentVideoData);
                        }
                    }
                });
            })(videoData);

            container.appendChild(videoItem);
        }

        popup.appendChild(container);

        popup.addEventListener('click', function(e) {
            if (e.target === popup) {
                popup.remove();
            }
        });

        document.body.appendChild(popup);
    }

    async function downloadM3U8(videoData, forceDownload = false) {
        // Check if we already downloaded this
        const cachedBlob = downloadedBlobs.get(videoData.url);
        if (cachedBlob && !forceDownload) {
            showNotification('Using cached video...', 'info');
            const filename = cachedBlob.filename;
            await shareOrDownloadBlob(cachedBlob.blob, filename, videoData, forceDownload);
            return;
        }

        showNotification('Downloading M3U8...', 'info');
        resetProgress();

        try {
            const manifest = videoData.m3u8Data.manifest;
            const baseUrl = videoData.url.substring(0, videoData.url.lastIndexOf('/') + 1);
            const segments = manifest.segments;

            if (!segments || segments.length === 0) {
                throw new Error("No segments found in this playlist");
            }

            var segmentData = [];
            for (var i = 0; i < segments.length; i++) {
                var segUrl = segments[i].uri.startsWith('http')
                    ? segments[i].uri
                    : baseUrl + segments[i].uri;

                var response = await fetch(segUrl);
                var data = await response.arrayBuffer();
                segmentData.push(data);

                var percent = Math.floor((i + 1) / segments.length * 100);
                updateProgress(percent);

                if (percent % 20 === 0 || i === segments.length - 1) {
                    showNotification('Downloading: ' + percent + '%', 'info');
                }
            }

            showNotification('Merging...', 'info');
            var merged = new Blob(segmentData, { type: 'video/mp2t' });

            var urlPath = new URL(videoData.url).pathname;
            var baseName = urlPath.split('/').pop().replace(/\.(m3u8?|ts)$/i, '') || 'video';
            var filename = baseName + '-noenc.ts';

            // Cache the blob
            downloadedBlobs.set(videoData.url, { blob: merged, filename: filename });

            await shareOrDownloadBlob(merged, filename, videoData, forceDownload);

        } catch(e) {
            console.error("M3U8 download failed:", e);
            showNotification('Download failed: ' + e.message, 'error');
            resetProgress();
        }
    }

    async function shareOrDownloadBlob(blob, filename, videoData, forceDownload = false) {
        const processed = processedVideos.get(videoData.url);

        // If already processed and user clicks again, force download
        if (processed && !forceDownload) {
            forceDownload = true;
            showNotification('Downloading...', 'info');
        }

        if (!forceDownload && navigator.share && navigator.canShare) {
            try {
                var file = new File([blob], filename, { type: 'video/mp2t' });

                if (navigator.canShare({ files: [file] })) {
                    var shareStartTime = Date.now();
                    var shareCompleted = false;

                    // Set a timeout to detect if share dialog stayed open
                    var shareTimeout = setTimeout(function() {
                        if (!shareCompleted) {
                            console.log("Share dialog appears to be active (2s elapsed)");
                        }
                    }, 2000);

                    try {
                        await navigator.share({
                            files: [file],
                            title: document.title,
                            text: 'M3U8 Video'
                        });

                        shareCompleted = true;
                        clearTimeout(shareTimeout);

                        processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                        showNotification('Shared!', 'success');
                        resetProgress();
                        return;

                    } catch(e) {
                        shareCompleted = true;
                        clearTimeout(shareTimeout);

                        var timeElapsed = Date.now() - shareStartTime;

                        // If share was open for >2 seconds, assume it worked
                        if (timeElapsed > 2000) {
                            processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                            showNotification('Share completed', 'success');
                            resetProgress();
                            return;
                        }

                        console.log("Share failed/cancelled after " + timeElapsed + "ms, downloading instead:", e);
                    }
                }
            } catch(e) {
                console.log("Share API error:", e);
            }
        }

        // Download the file
        var blobUrl = URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.href = blobUrl;
        a.download = filename;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();

        setTimeout(function() {
            document.body.removeChild(a);
            URL.revokeObjectURL(blobUrl);
        }, 100);

        processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() });
        showNotification('Downloaded: ' + filename, 'success');
        resetProgress();
    }

    function shareVideo(videoData) {
        if (videoData.type === 'm3u8') {
            const processed = processedVideos.get(videoData.url);
            const forceDownload = processed && (processed.action === 'shared' || processed.action === 'downloaded');
            downloadM3U8(videoData, forceDownload);
            return;
        }

        const processed = processedVideos.get(videoData.url);
        if (processed && (processed.action === 'shared' || processed.action === 'copied')) {
            downloadVideo(videoData);
            return;
        }

        if (navigator.share) {
            var shareStartTime = Date.now();
            navigator.share({
                title: document.title,
                url: videoData.url
            }).then(function() {
                processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                showNotification('Shared!', 'success');
            }).catch(function(error) {
                if (Date.now() - shareStartTime > 2000) {
                    processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() });
                    showNotification('Share completed', 'success');
                } else {
                    console.log('Share cancelled:', error);
                    copyVideoUrl(videoData);
                }
            });
        } else {
            copyVideoUrl(videoData);
        }
    }

    function downloadVideo(videoData) {
        showNotification('Opening video...', 'info');
        window.open(videoData.url, '_blank');
        processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() });
    }

    function copyVideoUrl(videoData) {
        const processed = processedVideos.get(videoData.url);
        if (processed && processed.action === 'copied') {
            downloadVideo(videoData);
            return;
        }

        if (navigator.clipboard && navigator.clipboard.writeText) {
            var copyStartTime = Date.now();
            navigator.clipboard.writeText(videoData.url).then(function() {
                processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() });
                showNotification('URL copied', 'success');
            }).catch(function(error) {
                if (Date.now() - copyStartTime > 2000) {
                    processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() });
                    showNotification('URL copied', 'success');
                } else {
                    fallbackCopy(videoData.url, videoData);
                }
            });
        } else {
            fallbackCopy(videoData.url, videoData);
        }
    }

    function fallbackCopy(url, videoData) {
        try {
            var textArea = document.createElement('textarea');
            textArea.value = url;
            textArea.style.position = 'fixed';
            textArea.style.left = '-999999px';
            document.body.appendChild(textArea);
            textArea.select();

            if (document.execCommand('copy')) {
                processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() });
                showNotification('URL copied', 'success');
            } else {
                showNotification('Copy failed', 'error');
            }

            document.body.removeChild(textArea);
        } catch (err) {
            showNotification('Copy failed', 'error');
        }
    }

    function showNotification(message, type) {
        if (type === undefined) type = 'success';

        var existingNotifications = document.querySelectorAll('.universal-video-notification');
        for (var i = 0; i < existingNotifications.length; i++) {
            existingNotifications[i].remove();
        }

        var notification = document.createElement('div');
        notification.textContent = message;
        notification.className = 'universal-video-notification';

        var bgColor = type === 'success' ? 'rgba(74, 222, 128, 0.9)' :
                      type === 'error' ? 'rgba(239, 68, 68, 0.9)' :
                      'rgba(59, 130, 246, 0.9)';

        notification.style.cssText = `position: fixed; top: 65px; left: 15px; background: ${bgColor}; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: ${COLORS.text}; padding: 8px 12px; border-radius: 6px; z-index: 999998; font-weight: 500; font-size: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); max-width: 250px; word-wrap: break-word; transform: translateX(-300px); transition: transform 0.3s ease;`;

        document.body.appendChild(notification);

        setTimeout(function() {
            notification.style.transform = 'translateX(0)';
        }, 100);

        setTimeout(function() {
            if (notification.parentNode) {
                notification.style.transform = 'translateX(-300px)';
                setTimeout(function() {
                    notification.remove();
                }, 300);
            }
        }, 3000);
    }

    function checkForVideos() {
        var videos = getUniqueVideos();

        if (videos.length > 0) {
            if (!floatingButton) {
                createFloatingButton();
            }
            if (floatingButton) {
                floatingButton.innerHTML = getButtonIcon(videos);
            }
        } else {
            if (floatingButton && floatingButton.parentNode) {
                floatingButton.parentNode.remove();
                floatingButton = null;
            }
        }
    }

    function init() {
        console.log('Universal Video Share with M3U8 initialized');
        setTimeout(checkForVideos, 1000);
        checkInterval = setInterval(checkForVideos, 5000);
    }

    init();

})();