Greasy Fork

Greasy Fork is available in English.

Disney+ Arabic Subtitle Auto Downloader

Automatically download Arabic subtitles from Disney+ once they're detected

当前为 2025-03-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Disney+ Arabic Subtitle Auto Downloader
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Automatically download Arabic subtitles from Disney+ once they're detected
// @author       @Daghriry 
// @match        https://www.apps.disneyplus.com/*
// @match        https://apps.disneyplus.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const Config = {
        DEBUG: false,           // Set to false to reduce console logs
        CHECK_INTERVAL: 1500,   // Check interval for subtitle detection
        AUTO_DOWNLOAD: true     // Always auto-download when subtitle is found
    };

    // State tracking
    const State = {
        subtitleFound: false,         
        lastUrl: window.location.href,
        vttUrls: [],                   
        uiAdded: false,
        currentShowInfo: {}
    };

    // Simple logging utility
    const log = (message, data) => {
        if (Config.DEBUG) {
            console.log(`[Disney+ Subtitle Downloader] ${message}`, data || '');
        }
    };
    
    const error = (message, err) => {
        console.error(`[Disney+ Subtitle Downloader] ${message}`, err || '');
    };

    // Extract episode info from URL and page content
    const extractEpisodeInfo = () => {
        const url = window.location.href;
        const urlObj = new URL(url);
        const pathParts = urlObj.pathname.split('/');
        const queryParams = new URLSearchParams(urlObj.search);

        // Extract episode info
        let info = {
            showName: '',
            episodeNumber: parseInt(queryParams.get('episodeNumber') || '0'),
            seasonId: queryParams.get('seasonId') || '',
            episodeTitle: ''
        };

        // Extract from path
        for (let i = 0; i < pathParts.length; i++) {
            if (pathParts[i] === 'shows' && i+1 < pathParts.length) {
                info.showName = pathParts[i+1];
            }
        }

        // Try to get title information from the page elements
        try {
            const titleElement = document.querySelector('.title-field');
            if (titleElement) {
                info.showTitle = titleElement.innerText || info.showName;
            }

            const subtitleElement = document.querySelector('.subtitle-field, .subtitle-text');
            if (subtitleElement) {
                info.episodeTitle = subtitleElement.innerText;
            }
        } catch (e) {
            error('Error getting title info from DOM', e);
        }

        State.currentShowInfo = info;
        return info;
    };

    // Resolve relative URLs to absolute URLs
    const resolveUrl = (base, relative) => {
        if (relative.startsWith('http')) {
            return relative;
        }

        if (relative.startsWith('/')) {
            const urlObj = new URL(base);
            return `${urlObj.protocol}//${urlObj.host}${relative}`;
        }

        // Handle relative paths
        const baseParts = base.split('/');
        baseParts.pop();
        return `${baseParts.join('/')}/${relative}`;
    };

    // Convert VTT format to SRT format
    const convertVttToSrt = (vtt) => {
        // Remove the VTT header
        let content = vtt.replace(/WEBVTT\r?\n/, '');
        content = content.replace(/NOTE.*\r?\n/g, '');

        const lines = content.split(/\r\n|\r|\n/);
        let srtLines = [];
        let subtitleCount = 0;
        let inSubtitle = false;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];

            // Check if this is a timestamp line
            if (line.includes('-->')) {
                subtitleCount++;
                srtLines.push(subtitleCount.toString()); // Add subtitle number

                // Convert timestamp format from VTT to SRT
                const timestamps = line.match(/(\d{2}:\d{2}:\d{2}.\d{3}) --> (\d{2}:\d{2}:\d{2}.\d{3})/);
                if (timestamps) {
                    const startTime = timestamps[1].replace('.', ',');
                    const endTime = timestamps[2].replace('.', ',');
                    srtLines.push(`${startTime} --> ${endTime}`);
                } else {
                    srtLines.push(line.replace(/\./g, ','));
                }

                inSubtitle = true;
            }
            // If we're in a subtitle block and it's not a blank line
            else if (inSubtitle && line.trim() !== '') {
                // Clean up formatting tags
                let cleanLine = line.replace(/<\/?[^>]+(>|$)/g, '');
                cleanLine = cleanLine.replace(/&amp;/g, '&');
                srtLines.push(cleanLine);
            }
            // If we're in a subtitle block and hit a blank line, add a blank line to separate subtitles
            else if (inSubtitle && line.trim() === '') {
                srtLines.push('');
                inSubtitle = false;
            }
        }

        return srtLines.join('\r\n');
    };

    // Save subtitle to a file
    const saveSubtitle = (content) => {
        const info = State.currentShowInfo;
        let filename = info.showName || info.showTitle || 'Disney_Show';

        if (info.episodeTitle) {
            filename += ` - ${info.episodeTitle.replace(/[:|/\\?*"<>]/g, '')}`;
        } else if (info.episodeNumber > 0) {
            filename += ` - Episode ${info.episodeNumber}`;
        }

        filename += '.ar.srt';
        log('Saving subtitle as', filename);

        // Create a Blob with the content
        const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        saveAs(blob, filename);
        showNotification(`✓ Arabic subtitle downloaded: ${filename}`);
        updateStatus('Subtitle downloaded successfully');
    };

    // Process and download a subtitle URL
    const processSubtitleUrl = (url) => {
        if (State.subtitleFound || State.vttUrls.includes(url)) {
            return;
        }

        State.vttUrls.push(url);
        log('Processing subtitle URL', url);
        updateStatus('Found subtitle, downloading...');

        // Fetch the VTT content
        fetch(url)
            .then(response => response.text())
            .then(vttContent => {
                if (vttContent && vttContent.includes('-->')) {
                    State.subtitleFound = true;
                    log('VTT content retrieved, length:', vttContent.length);

                    // Convert VTT to SRT and save
                    const srtContent = convertVttToSrt(vttContent);
                    saveSubtitle(srtContent);
                } else {
                    log('Invalid VTT content');
                    updateStatus('Invalid subtitle data');
                }
            })
            .catch(err => {
                error('Error fetching subtitle', err);
                updateStatus('Error downloading subtitle');
            });
    };

    // Parse M3U8 manifest for subtitle URLs
    const parseM3U8ForSubtitles = (content, baseUrl) => {
        const lines = content.split('\n');
        let subtitleSection = false;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];

            // Look for Arabic subtitle tracks
            if (line.includes('TYPE=SUBTITLES') && 
                (line.includes('LANGUAGE="ar"') || line.includes('LANGUAGE="ara"') || line.includes('LANGUAGE="arb"'))) {
                subtitleSection = true;
                log('Arabic subtitle track found in M3U8');
            }

            // If we're in a subtitle section and find a URI
            if (subtitleSection && line.includes('URI="')) {
                // Extract the URI
                const uriMatch = line.match(/URI="([^"]+)"/);
                if (uriMatch && uriMatch[1]) {
                    const subtitleManifestUrl = resolveUrl(baseUrl, uriMatch[1]);
                    log('Subtitle manifest URL', subtitleManifestUrl);

                    // Fetch the subtitle manifest
                    fetchSubtitleManifest(subtitleManifestUrl);
                }

                subtitleSection = false;
            }
        }
    };

    // Fetch subtitle manifest file to find VTT URLs
    const fetchSubtitleManifest = (url) => {
        fetch(url)
            .then(response => response.text())
            .then(content => {
                // Parse the manifest for VTT URLs
                const lines = content.split('\n');
                let baseUrl = url.substring(0, url.lastIndexOf('/') + 1);

                for (let i = 0; i < lines.length; i++) {
                    const line = lines[i];
                    if (line.endsWith('.vtt')) {
                        const vttUrl = resolveUrl(baseUrl, line);
                        log('VTT URL found', vttUrl);
                        processSubtitleUrl(vttUrl);
                    }
                }
            })
            .catch(err => error('Error fetching subtitle manifest', err));
    };

    // Check for subtitle URLs in network resources
    const checkForSubtitles = () => {
        if (State.subtitleFound) return; // Already found for this episode
        
        updateStatus('Scanning for Arabic subtitles...');
        
        // Get current show info for proper filename
        extractEpisodeInfo();
        
        // Check performance entries for subtitle URLs
        const entries = performance.getEntriesByType('resource');
        let found = false;

        // Look through all network resources
        for (const entry of entries) {
            if (entry.name &&
                typeof entry.name === 'string' &&
                entry.name.includes('subtitle.vtt')) {
                log('Found subtitle in performance entries', entry.name);

                if (entry.name.includes('/ara/') ||
                    entry.name.includes('/arb/') ||
                    entry.name.includes('arabic') ||
                    entry.name.includes('am_normal')) {
                    log('Arabic subtitle found in performance entries', entry.name);
                    processSubtitleUrl(entry.name);
                    found = true;
                    break;
                }
            }
        }

        if (!found) {
            // Try alternative method - look for subtitle URLs in the HTML
            const pageContent = document.documentElement.outerHTML;
            const subtitleUrlRegex = /https:\/\/[^"'\s]+subtitle\.vtt[^"'\s]*/g;
            const matches = pageContent.match(subtitleUrlRegex);

            if (matches && matches.length > 0) {
                for (const url of matches) {
                    if (url.includes('/ara/') ||
                        url.includes('/arb/') ||
                        url.includes('arabic') ||
                        url.includes('am_normal')) {
                        log('Arabic subtitle found in HTML', url);
                        processSubtitleUrl(url);
                        found = true;
                        break;
                    }
                }
            }

            // If still not found, look for any VTT file that might be Arabic
            if (!found) {
                for (const entry of entries) {
                    if (entry.name &&
                        typeof entry.name === 'string' &&
                        entry.name.includes('.vtt')) {
                        log('Trying potential subtitle', entry.name);

                        // Try to fetch and see if it contains Arabic
                        fetch(entry.name)
                            .then(response => response.text())
                            .then(content => {
                                // Look for Arabic script in the content
                                if (content.match(/[\u0600-\u06FF]/)) {
                                    log('Found Arabic content in', entry.name);
                                    processSubtitleUrl(entry.name);
                                }
                            })
                            .catch(err => error('Error checking VTT file', err));
                    }
                }
            }
        }

        if (!found) {
            updateStatus('Waiting for Arabic subtitles...');
        }

        return found;
    };

    // Show a notification message
    const showNotification = (message) => {
        log('Showing notification', message);

        const notification = document.createElement('div');
        notification.textContent = message;
        notification.style.position = 'fixed';
        notification.style.top = '20px';
        notification.style.left = '50%';
        notification.style.transform = 'translateX(-50%)';
        notification.style.backgroundColor = '#0063e5';
        notification.style.color = 'white';
        notification.style.padding = '10px 20px';
        notification.style.borderRadius = '5px';
        notification.style.zIndex = '10000';
        notification.style.fontFamily = 'Arial, sans-serif';
        notification.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';

        document.body.appendChild(notification);

        setTimeout(() => {
            notification.style.opacity = '0';
            notification.style.transition = 'opacity 1s';
            setTimeout(() => {
                if (notification.parentNode) {
                    document.body.removeChild(notification);
                }
            }, 1000);
        }, 5000);
    };

    // Update status message
    const updateStatus = (message) => {
        const statusDiv = document.getElementById('subtitle-downloader-status');
        if (statusDiv) {
            statusDiv.textContent = 'Status: ' + message;
        }
    };

    // XHR Interceptor to detect subtitle requests
    const initNetworkInterceptor = () => {
        log("Initializing network interceptor");

        // Store original XHR open and send methods
        const originalOpen = XMLHttpRequest.prototype.open;
        const originalSend = XMLHttpRequest.prototype.send;

        // Override open method to store URL
        XMLHttpRequest.prototype.open = function(method, url, ...args) {
            this._url = url;
            return originalOpen.apply(this, [method, url, ...args]);
        };

        // Override send method to intercept responses
        XMLHttpRequest.prototype.send = function(...args) {
            const xhr = this;

            this.addEventListener('load', function() {
                try {
                    if (!xhr._url || typeof xhr._url !== 'string') return;

                    // Look for subtitle VTT files
                    if (xhr._url.includes('subtitle.vtt')) {
                        log('Subtitle URL detected', xhr._url);

                        // Check if Arabic subtitle
                        if (xhr._url.includes('/ara/') ||
                            xhr._url.includes('/arb/') ||
                            xhr._url.includes('arabic') ||
                            xhr._url.includes('am_normal')) {

                            log('Arabic subtitle found', xhr._url);
                            processSubtitleUrl(xhr._url);
                        }
                    }
                    // Check for M3U8 manifests that may contain subtitle info
                    else if (xhr._url.includes('.m3u8') && xhr.responseText) {
                        if (xhr.responseText.includes('TYPE=SUBTITLES')) {
                            log('Subtitle tracks found in M3U8');
                            parseM3U8ForSubtitles(xhr.responseText, xhr._url);
                        }
                    }
                } catch (e) {
                    error('Error in XHR intercept', e);
                }
            });

            return originalSend.apply(this, args);
        };
    };

    // Add minimal floating UI control
    const addUserInterface = () => {
        if (State.uiAdded) return;

        log('Adding user interface');

        // Create main container for the UI
        const container = document.createElement('div');
        container.id = 'disney-subtitle-downloader-ui';
        container.style.position = 'fixed';
        container.style.bottom = '20px';
        container.style.right = '20px';
        container.style.zIndex = '10000';

        // Create floating button
        const floatingButton = document.createElement('div');
        floatingButton.id = 'disney-subtitle-toggle-button';
        floatingButton.style.width = '44px';
        floatingButton.style.height = '44px';
        floatingButton.style.borderRadius = '50%';
        floatingButton.style.backgroundColor = '#0063e5';
        floatingButton.style.color = 'white';
        floatingButton.style.textAlign = 'center';
        floatingButton.style.lineHeight = '44px';
        floatingButton.style.fontSize = '20px';
        floatingButton.style.cursor = 'pointer';
        floatingButton.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
        floatingButton.innerHTML = 'ع'; // Arabic letter for Arabic subtitles
        floatingButton.title = 'Disney+ Arabic Subtitle Downloader';

        // Create status panel
        const panel = document.createElement('div');
        panel.id = 'disney-subtitle-control-panel';
        panel.style.position = 'absolute';
        panel.style.bottom = '54px';
        panel.style.right = '0';
        panel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        panel.style.padding = '12px';
        panel.style.borderRadius = '8px';
        panel.style.color = 'white';
        panel.style.fontFamily = 'Arial, sans-serif';
        panel.style.display = 'none';
        panel.style.flexDirection = 'column';
        panel.style.width = '180px';

        // Title for the panel
        const title = document.createElement('div');
        title.textContent = 'Arabic Subtitle Downloader';
        title.style.fontWeight = 'bold';
        title.style.textAlign = 'center';
        title.style.marginBottom = '8px';
        panel.appendChild(title);

        // Status display
        const statusDiv = document.createElement('div');
        statusDiv.id = 'subtitle-downloader-status';
        statusDiv.textContent = 'Status: Looking for subtitles...';
        statusDiv.style.fontSize = '12px';
        statusDiv.style.marginBottom = '8px';
        panel.appendChild(statusDiv);
        
        // Manual scan button
        const scanButton = document.createElement('button');
        scanButton.textContent = 'Scan for Subtitles';
        scanButton.style.backgroundColor = '#0063e5';
        scanButton.style.color = 'white';
        scanButton.style.border = 'none';
        scanButton.style.padding = '8px';
        scanButton.style.borderRadius = '4px';
        scanButton.style.cursor = 'pointer';
        scanButton.style.width = '100%';

        scanButton.addEventListener('click', function() {
            checkForSubtitles();
        });

        panel.appendChild(scanButton);

        // Toggle panel visibility when clicking the floating button
        floatingButton.addEventListener('click', function(e) {
            e.stopPropagation();

            if (panel.style.display === 'none') {
                panel.style.display = 'flex';
                floatingButton.innerHTML = '×'; // Close icon
            } else {
                panel.style.display = 'none';
                floatingButton.innerHTML = 'ع'; // Arabic letter
            }
        });

        // Close panel when clicking outside
        document.addEventListener('click', function(e) {
            if (panel.style.display === 'flex' &&
                !panel.contains(e.target) &&
                e.target !== floatingButton) {
                panel.style.display = 'none';
                floatingButton.innerHTML = 'ع';
            }
        });

        // Add elements to the container
        container.appendChild(panel);
        container.appendChild(floatingButton);

        // Add container to the body
        document.body.appendChild(container);

        State.uiAdded = true;
    };

    // Monitor URL changes to detect new episodes
    const monitorUrlChanges = () => {
        if (State.lastUrl !== window.location.href) {
            log('URL changed, resetting subtitle state');
            State.lastUrl = window.location.href;
            State.subtitleFound = false;
            State.vttUrls = [];

            // Update status
            updateStatus('New episode detected, looking for subtitles...');
            
            // Scan for subtitles after episode change
            setTimeout(checkForSubtitles, 2000);
        }

        // Make sure UI is added
        if (!State.uiAdded && document.body) {
            addUserInterface();
        }
        
        // Regularly check for subtitles if not found yet
        if (!State.subtitleFound && Config.AUTO_DOWNLOAD) {
            checkForSubtitles();
        }
    };

    // Initialize the application
    const initialize = () => {
        log('Initializing Disney+ Arabic Subtitle Auto Downloader');

        // Initialize network interceptor
        initNetworkInterceptor();

        // Set up periodic URL monitoring and subtitle checking
        setInterval(monitorUrlChanges, Config.CHECK_INTERVAL);

        // Add UI as soon as document is ready
        if (document.body) {
            addUserInterface();
        }

        // Show notification that script is running
        setTimeout(() => {
            if (document.body) {
                showNotification('Disney+ Arabic Subtitle Downloader Active');
                // Initial scan
                checkForSubtitles();
            }
        }, 2000);
    };

    // Start the application
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        initialize();
    } else {
        // Wait for document to load
        window.addEventListener('DOMContentLoaded', initialize);
    }

    // Ensure we run on document load
    window.addEventListener('load', function() {
        if (!State.uiAdded && document.body) {
            addUserInterface();
            checkForSubtitles();
        }
    });
})();