skript nemá GUI, přehrávané titulky se stáhnou po skončení videa automaticky do nastavené dwl složky prohlížeče
// ==UserScript==
// @name Arte.tv Auto Subtitle Scraper
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Automatically scrape and save subtitles on tab close/navigation
// @match https://www.arte.tv/*
// @match https://*.arte.tv/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ============ CONFIGURATION ============
const CONFIG = {
// Console logging
debug: true,
// Start scraping automatically
autoStart: true,
// Auto-save when leaving page
saveOnExit: true,
// Auto-save on URL/navigation change
saveOnUrlChange: true,
// Minimum subtitles before auto-saving
minSubtitles: 10,
// Manual save shortcut
keyboardShortcut: 'ctrl+shift+s',
// Filename prefix
filenamePrefix: 'arte_subtitles_',
};
// Note: Download folder is controlled by browser settings, not this script.
// Files will save to your browser's default download location.
// ============ STATE ============
const log = (...args) => CONFIG.debug && console.log('[SubScraper]', ...args);
let capturedSubs = [];
let isScanning = false;
let hasAutoSaved = false;
// ============ FILTERS ============
const UI_GARBAGE = [
'Pause', 'Play', 'Mute', 'Unmute',
'Forward 10 seconds', 'Back 10 seconds',
'Rewind 10 seconds', 'Skip 10 seconds',
'Settings', 'Fullscreen', 'Exit fullscreen',
'Subtitles', 'Audio', 'Quality', 'Captions',
'Volume', 'Speed', 'Next', 'Previous',
'Share', 'Download', 'More', 'Less'
];
function isUIGarbage(text) {
if (!text || text.length < 2) return true;
// Percentages like "1.36%"
if (/^\d+\.?\d*%$/.test(text)) return true;
// Timestamps like "12:34" or "1:23:45"
if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(text)) return true;
// Known UI text
if (UI_GARBAGE.some(ui => text.includes(ui))) return true;
// Very short or pure numbers
if (text.length < 3 || /^\d+$/.test(text)) return true;
return false;
}
// ============ SCRAPING ============
function startScraping() {
if (isScanning) {
log('Already scanning');
return;
}
isScanning = true;
log('Auto-scraping started');
const observer = new MutationObserver((mutations) => {
if (!isScanning) return;
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
let text = '';
if (node.nodeType === Node.TEXT_NODE) {
text = node.textContent.trim();
} else if (node.nodeType === Node.ELEMENT_NODE) {
text = node.textContent.trim();
}
if (text && !isUIGarbage(text)) {
captureSubtitle(text);
}
});
});
});
// Monitor subtitle areas
const targets = document.querySelectorAll('.avp, video, [class*="player"], [class*="subtitle"], [class*="caption"]');
targets.forEach(target => {
observer.observe(target, {
childList: true,
subtree: true,
characterData: true
});
});
log(`Monitoring ${targets.length} elements`);
}
function captureSubtitle(text) {
// Avoid duplicates
if (capturedSubs.some(s => s.text === text)) return;
const video = document.querySelector('video');
const timestamp = video ? video.currentTime : capturedSubs.length * 2;
capturedSubs.push({
start: timestamp,
end: timestamp + 2, // Default 2 second duration
text: text
});
log(`[${capturedSubs.length}] ${text.substring(0, 50)}`);
}
// ============ OVERLAP CORRECTION ============
function fixOverlaps(subs) {
// Sort by start time
subs.sort((a, b) => a.start - b.start);
// Fix overlaps: if current subtitle ends after next starts,
// adjust current end to match next start
for (let i = 0; i < subs.length - 1; i++) {
if (subs[i].end > subs[i + 1].start) {
log(`Overlap fixed: [${i}] ${formatTime(subs[i].end)} -> ${formatTime(subs[i + 1].start)}`);
subs[i].end = subs[i + 1].start;
}
}
return subs;
}
// ============ DEDUPLICATION ============
function removeDuplicates(subs) {
const unique = [];
const seen = new Set();
subs.forEach(sub => {
const key = `${sub.start.toFixed(1)}_${sub.text}`;
if (!seen.has(key)) {
seen.add(key);
unique.push(sub);
}
});
return unique;
}
// ============ SRT GENERATION ============
function generateSRT() {
if (capturedSubs.length === 0) {
log('No subtitles to save');
return null;
}
// Process subtitles
let processed = removeDuplicates(capturedSubs);
processed = fixOverlaps(processed);
log(`Generating SRT: ${capturedSubs.length} raw -> ${processed.length} final`);
// Convert to SRT format
let srt = '';
processed.forEach((sub, idx) => {
srt += `${idx + 1}\n`;
srt += `${formatTime(sub.start)} --> ${formatTime(sub.end)}\n`;
srt += `${sub.text}\n\n`;
});
return srt;
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 1000);
return `${pad(h)}:${pad(m)}:${pad(s)},${pad(ms, 3)}`;
}
function pad(num, size = 2) {
let s = num + '';
while (s.length < size) s = '0' + s;
return s;
}
// ============ SAVE ============
function saveSubtitles(reason = 'manual') {
if (capturedSubs.length < CONFIG.minSubtitles) {
log(`Not saving: only ${capturedSubs.length} subtitles (min: ${CONFIG.minSubtitles})`);
return false;
}
const srt = generateSRT();
if (!srt) return false;
const filename = `${CONFIG.filenamePrefix}${Date.now()}.srt`;
try {
const blob = new Blob([srt], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
// For auto-save on exit, we need to click synchronously
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Clean up after a delay
setTimeout(() => URL.revokeObjectURL(url), 100);
log(`✓ Saved: ${filename} (${capturedSubs.length} lines, reason: ${reason})`);
return true;
} catch (e) {
console.error('[SubScraper] Save failed:', e);
return false;
}
}
// ============ AUTO-SAVE TRIGGERS ============
// Tab close / page refresh / browser close
window.addEventListener('beforeunload', (e) => {
if (CONFIG.saveOnExit && !hasAutoSaved) {
hasAutoSaved = true;
saveSubtitles('page-exit');
}
});
// URL change (SPA navigation)
if (CONFIG.saveOnUrlChange) {
let currentUrl = window.location.href;
setInterval(() => {
if (window.location.href !== currentUrl) {
log('URL changed detected');
currentUrl = window.location.href;
if (!hasAutoSaved) {
hasAutoSaved = true;
saveSubtitles('url-change');
}
}
}, 1000);
}
// ============ KEYBOARD SHORTCUT ============
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+S (or Cmd+Shift+S on Mac)
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 's') {
e.preventDefault();
log('Manual save triggered');
saveSubtitles('keyboard');
}
});
// ============ INITIALIZATION ============
function init() {
if (!document.body) {
setTimeout(init, 100);
return;
}
log('Arte.tv Auto Subtitle Scraper v3.0');
log(`Config: autoStart=${CONFIG.autoStart}, saveOnExit=${CONFIG.saveOnExit}`);
log(`Keyboard shortcut: ${CONFIG.keyboardShortcut.toUpperCase()} for manual save`);
if (CONFIG.autoStart) {
// Wait for video player to load
setTimeout(() => {
const video = document.querySelector('video');
if (video) {
log('Video detected, starting scraper');
startScraping();
} else {
log('No video found, waiting...');
setTimeout(init, 2000);
}
}, 2000);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ============ DEBUG INFO ============
// Expose stats for console inspection
window.subScraperStats = () => {
return {
captured: capturedSubs.length,
autoSaved: hasAutoSaved,
config: CONFIG
};
};
log('Tip: Run window.subScraperStats() in console to see current status');
})();