// ==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(/&/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();
}
});
})();