Mobile Redirect Video to External App

Detects video sources by injecting code to bypass player sandboxing and uses robust redirect methods.

// ==UserScript==
// @name         Mobile Redirect Video to External App
// @namespace    http://tampermonkey.net/
// @version      4.7
// @description  Detects video sources by injecting code to bypass player sandboxing and uses robust redirect methods.
// @author       MasuRii
// @match        *://*/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===================================================================================
    // === CONFIGURATION =================================================================
    // ===================================================================================
    const CONFIG = {
        INTENT_TYPE: 'CHOOSER', // 'CHOOSER' or 'SPECIFIC_APP'
        SPECIFIC_APP_PACKAGE: 'com.mxtech.videoplayer.pro',
        BUTTON_TEXT: 'Open Externally',
        DEBUG_MODE: true,
        DEBUG_INSPECTOR: true
    };
    // ===================================================================================
    // === END OF CONFIGURATION ==========================================================
    // ===================================================================================

    let redirectButton = null;
    let linkChooserModal = null;
    let inspectorButton = null;
    const videoSources = new Set();

    function log(message) {
        if (CONFIG.DEBUG_MODE) {
            console.log('[Video Redirector] ' + message);
        }
    }

    function buildIntentUrl(videoUrl) {
        if (!videoUrl) return null;
        let intentUrl = `intent:${videoUrl}#Intent;action=android.intent.action.VIEW;type=video/*;launchFlags=0x10000000;`;
        if (CONFIG.INTENT_TYPE === 'SPECIFIC_APP' && CONFIG.SPECIFIC_APP_PACKAGE) {
            intentUrl += `package=${CONFIG.SPECIFIC_APP_PACKAGE};`;
        }
        intentUrl += 'end';
        return intentUrl;
    }

    /**
     * ===================================================================================
     * === THE FIX IS HERE (v4.7) ========================================================
     * ===================================================================================
     * Issue: `window.location.href` is blocked by strict Content Security Policies (CSP).
     * Solution: For the single-video case, we now use the more reliable method of
     *           creating a temporary link and programmatically clicking it. This is
     *           less likely to be blocked by CSP than a direct location change.
     */
    function redirectToExternalApp(videoUrl) {
        const intentUrl = buildIntentUrl(videoUrl);
        if (intentUrl) {
            log(`Attempting single-video redirect via programmatic click: ${intentUrl}`);

            const link = document.createElement('a');
            link.href = intentUrl;
            link.style.display = 'none';

            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
    // ===================================================================================
    // === END OF FIX ====================================================================
    // ===================================================================================


    function createLinkChooserModal() {
        if (document.getElementById('video-redirect-modal')) return;
        const modalContainer = document.createElement('div');
        modalContainer.id = 'video-redirect-modal';
        modalContainer.innerHTML = `
            <div class="modal-overlay"></div>
            <div class="modal-content">
                <div class="modal-header">
                    <h3>Select a Video Link</h3>
                    <button class="modal-close">&times;</button>
                </div>
                <div class="modal-body">
                    <p>Click a link to open in your external player:</p>
                    <ul id="video-link-list"></ul>
                </div>
            </div>
        `;
        document.body.appendChild(modalContainer);
        linkChooserModal = modalContainer;
        const closeModal = () => linkChooserModal.style.display = 'none';
        modalContainer.querySelector('.modal-overlay').addEventListener('click', closeModal);
        modalContainer.querySelector('.modal-close').addEventListener('click', closeModal);
    }

    function showLinkChooser() {
        if (videoSources.size === 0) {
            alert('No valid video links found on this page.');
            return;
        }
        if (videoSources.size === 1) {
            const singleUrl = videoSources.values().next().value;
            redirectToExternalApp(singleUrl);
            return;
        }
        const listElement = linkChooserModal.querySelector('#video-link-list');
        listElement.innerHTML = '';
        videoSources.forEach(url => {
            const intentUrl = buildIntentUrl(url);
            if (!intentUrl) return;
            const listItem = document.createElement('li');
            const link = document.createElement('a');
            link.href = intentUrl;
            link.textContent = url.length > 100 ? url.substring(0, 97) + '...' : url;
            link.title = url;
            link.addEventListener('click', () => {
                linkChooserModal.style.display = 'none';
            });
            listItem.appendChild(link);
            listElement.appendChild(listItem);
        });
        linkChooserModal.style.display = 'flex';
    }

    function createRedirectButton() {
        if (document.getElementById('video-redirect-button')) return;
        redirectButton = document.createElement('button');
        redirectButton.id = 'video-redirect-button';
        redirectButton.textContent = CONFIG.BUTTON_TEXT;
        redirectButton.addEventListener('click', showLinkChooser);
        document.body.appendChild(redirectButton);
    }

    function showRedirectButton() {
        if (redirectButton && videoSources.size > 0) {
            redirectButton.style.display = 'block';
        }
    }

    function findAdvancedPlayerSources() {
        document.addEventListener('VD_PlayerSourceFound', (e) => {
            const url = e.detail.url;
            if (url && !videoSources.has(url)) {
                videoSources.add(url);
                log(`Received player source via injection: ${url}`);
                showRedirectButton();
            }
        });
        const codeToInject = `
            (function() {
                if (typeof jwplayer !== 'function') return;
                const playerDivs = document.querySelectorAll('div.jwplayer[id]');
                playerDivs.forEach(div => {
                    try {
                        const player = jwplayer(div.id);
                        if (player && typeof player.getPlaylist === 'function') {
                            const playlist = player.getPlaylist();
                            playlist.forEach(item => {
                                const sources = (item.sources && Array.isArray(item.sources)) ? item.sources : [];
                                if (item.file) sources.push({ file: item.file });
                                sources.forEach(source => {
                                    if (source.file && typeof source.file === 'string' && !source.file.startsWith('blob:')) {
                                        document.dispatchEvent(new CustomEvent('VD_PlayerSourceFound', { detail: { url: source.file } }));
                                    }
                                });
                            });
                        }
                    } catch (err) {}
                });
            })();
        `;
        try {
            const script = document.createElement('script');
            script.textContent = codeToInject;
            (document.head || document.documentElement).appendChild(script);
            script.remove();
            log('Injected script to find player sources.');
        } catch (e) {
            log(`Error injecting script: ${e.message}`);
        }
    }

    function handleVideoElement(video) {
        const checkSrcAndStoreIt = () => {
            const videoSrc = video.currentSrc;
            if (videoSrc && !videoSrc.startsWith('blob:')) {
                if (!videoSources.has(videoSrc)) {
                    videoSources.add(videoSrc);
                    log(`Found standard <video> source: ${videoSrc}`);
                    showRedirectButton();
                }
            }
        };
        if (video.readyState >= 1) {
            checkSrcAndStoreIt();
        } else {
            video.addEventListener('loadedmetadata', checkSrcAndStoreIt, { once: true });
        }
        video.addEventListener('loadstart', () => setTimeout(checkSrcAndStoreIt, 100));
    }

    // --- Main Execution ---
    // The rest of the script (inspector, styles, observer) remains the same.
    // ... (omitted for brevity, it's identical to v4.6)
    function createDebugInspector(){if(!CONFIG.DEBUG_INSPECTOR||document.getElementById("video-redirect-inspector")){return}inspectorButton=document.createElement("button");inspectorButton.id="video-redirect-inspector";inspectorButton.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>';inspectorButton.title="Inspect Element for Debugging";inspectorButton.addEventListener("click",()=>{log("INSPECTOR MODE ACTIVATED. Click on or near the video player.");document.body.style.cursor="crosshair";document.body.addEventListener("click",inspectElement,{once:true,capture:true})});document.body.appendChild(inspectorButton)}function inspectElement(event){event.preventDefault();event.stopPropagation();document.body.style.cursor="default";let target=event.target;console.log("--- [Video Redirector] Inspector Report ---");console.log("User clicked on the element below. Analyzing its structure (up to 8 parents):");console.log("Please copy this report when asking for support for a new site.");for(let i=0;i<8&&target&&target.tagName!=="BODY";i++){const id=target.id?`#${target.id}`:"";const classes=target.className&&typeof target.className==="string"?`.${target.className.split(" ").join(".")}`:"";console.log(`[${i}] ${target.tagName}${id}${classes}`,target);target=target.parentElement}console.log("--- End of Report ---")}function addGlobalStyles(){GM_addStyle(`
            #video-redirect-button {
                position: fixed; bottom: 15px; left: 50%;
                transform: translateX(-50%);
                z-index: 2147483646;
                padding: 8px 16px; font-size: 14px; color: white;
                background-color: rgba(0, 0, 0, 0.6);
                border: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 20px; cursor: pointer;
                box-shadow: 0 2px 8px rgba(0,0,0,0.5);
                display: none; /* Initially hidden */
                backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
                transition: opacity 0.3s ease, transform 0.3s ease;
            }
            #video-redirect-button:hover {
                background-color: rgba(0, 0, 0, 0.8);
                transform: translateX(-50%) scale(1.05);
            }
            #video-redirect-inspector {
                position: fixed; bottom: 15px; right: 15px;
                z-index: 2147483646;
                width: 40px; height: 40px;
                background-color: rgba(100, 100, 100, 0.5);
                border: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 50%;
                cursor: pointer;
                display: flex; align-items: center; justify-content: center;
                backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
                transition: background-color 0.3s ease;
            }
            #video-redirect-inspector:hover {
                background-color: rgba(120, 0, 0, 0.7);
            }
            #video-redirect-modal {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                z-index: 2147483647;
                display: none; align-items: center; justify-content: center;
            }
            #video-redirect-modal .modal-overlay {
                position: absolute; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0,0,0,0.7);
            }
            #video-redirect-modal .modal-content {
                position: relative; z-index: 1;
                background-color: #2c2c2c; color: #f1f1f1;
                border-radius: 8px;
                width: 90%; max-width: 600px;
                max-height: 80vh;
                display: flex; flex-direction: column;
                box-shadow: 0 5px 15px rgba(0,0,0,0.5);
            }
            #video-redirect-modal .modal-header {
                padding: 15px; border-bottom: 1px solid #444;
                display: flex; justify-content: space-between; align-items: center;
            }
            #video-redirect-modal .modal-header h3 { margin: 0; font-size: 1.2em; }
            #video-redirect-modal .modal-close {
                background: none; border: none; font-size: 1.8em;
                color: #aaa; cursor: pointer; line-height: 1; padding: 0;
            }
            #video-redirect-modal .modal-close:hover { color: white; }
            #video-redirect-modal .modal-body { padding: 15px; overflow-y: auto; }
            #video-redirect-modal #video-link-list {
                list-style: none; padding: 0; margin: 0;
            }
            #video-redirect-modal #video-link-list li {
                margin-bottom: 10px;
            }
            #video-redirect-modal #video-link-list a {
                display: block; padding: 10px;
                background-color: #3a3a3a;
                border-radius: 4px;
                color: #aaddff; text-decoration: none;
                word-break: break-all;
                transition: background-color 0.2s ease;
            }
            #video-redirect-modal #video-link-list a:hover {
                background-color: #4f4f4f;
            }
        `)}

    addGlobalStyles();
    createRedirectButton();
    createLinkChooserModal();
    createDebugInspector();

    document.querySelectorAll('video').forEach(handleVideoElement);

    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === "childList") {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) {
                        if (node.tagName === "VIDEO") {
                            handleVideoElement(node);
                        } else if (node.querySelectorAll) {
                            node.querySelectorAll("video").forEach(handleVideoElement);
                        }
                    }
                });
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(findAdvancedPlayerSources, 2500);

})();