357 lines
14 KiB
JavaScript
357 lines
14 KiB
JavaScript
// Topic-Übersicht erkennen (keine Buttons dort)
|
||
function isInTopicList() {
|
||
return document.querySelector('.topics-list, .topic-list, .forums-list, .chat-list .topic') !== null ||
|
||
document.body.classList.contains('topics-mode');
|
||
}
|
||
|
||
// Kleine Helfer-Funktion: freundlich sanitisieren (erhalte Leerzeichen, Klammern, Punkte)
|
||
function sanitizeFriendly(name) {
|
||
if (!name) return 'Telegram';
|
||
return name.toString().replace(/[\/\\\?\%\*\:\|\"\<\>]/g, "_").trim();
|
||
}
|
||
|
||
// Extrahiere den Original-Dateinamen aus Telegram-Metadaten
|
||
function extractTelegramFilename(video) {
|
||
try {
|
||
const videoUrl = video.currentSrc || video.src;
|
||
|
||
console.log('=== FILENAME EXTRACTION ===');
|
||
console.log('Video URL:', videoUrl);
|
||
|
||
// 1. Suche die Message, die das Video enthält
|
||
const messageContainer = video.closest('.message, .Message, .bubble, .media-container');
|
||
if (!messageContainer) {
|
||
console.warn('No message container found');
|
||
return 'telegram_video_' + Date.now();
|
||
}
|
||
|
||
console.log('Message container found');
|
||
|
||
// === PRIORITÄT 1: VERBESSERTES ALBUM/SERIEN-HANDLING ===
|
||
const albumContainer = messageContainer.querySelector('.Album');
|
||
if (albumContainer) {
|
||
console.log('Album erkannt → verbessertes Serien-Naming');
|
||
|
||
const videoContainer = video.closest('.album-item-select-wrapper');
|
||
if (videoContainer) {
|
||
const allVideoContainers = albumContainer.querySelectorAll('.album-item-select-wrapper');
|
||
let videoIndex = -1;
|
||
for (let i = 0; i < allVideoContainers.length; i++) {
|
||
if (allVideoContainers[i] === videoContainer) {
|
||
videoIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
console.log('Video index in album:', videoIndex);
|
||
|
||
if (videoIndex >= 0) {
|
||
const textContent = messageContainer.querySelector('.text-content.clearfix.with-meta, .text-content');
|
||
if (textContent) {
|
||
const lines = textContent.innerText.split('\n')
|
||
.map(l => l.trim())
|
||
.filter(l => l.length > 0);
|
||
|
||
console.log('Album text lines:', lines);
|
||
|
||
const seriesTitle = lines[0] || 'Unknown Series';
|
||
|
||
// Direkter Match: SxxExx - Titel
|
||
const episodeLines = lines.filter(line => /^S\d{2}E\d{2}\s*-/.test(line));
|
||
if (episodeLines.length > videoIndex && episodeLines[videoIndex]) {
|
||
const line = episodeLines[videoIndex];
|
||
const match = line.match(/^(S\d{2}E\d{2})\s*-\s*(.+)$/);
|
||
if (match) {
|
||
const code = match[1];
|
||
const title = match[2].trim();
|
||
const finalName = `${sanitizeFriendly(seriesTitle)} - ${code} - ${sanitizeFriendly(title)}`;
|
||
console.log('Perfekter Serien-Dateiname:', finalName);
|
||
return finalName;
|
||
}
|
||
}
|
||
|
||
// Fallback: Zeile nach Serientitel als Episodentitel
|
||
if (lines.length > videoIndex + 1) {
|
||
const episodeTitle = lines[videoIndex + 1];
|
||
if (episodeTitle.length > 5) {
|
||
const finalName = `${sanitizeFriendly(seriesTitle)} - ${sanitizeFriendly(episodeTitle)}`;
|
||
console.log('Serien-Fallback-Dateiname:', finalName);
|
||
return finalName;
|
||
}
|
||
}
|
||
|
||
return sanitizeFriendly(seriesTitle + ' - Episode ' + (videoIndex + 1));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Einzelvideo: Erste Zeile aus .text-content
|
||
const textContent = messageContainer.querySelector('.text-content, .text-content.clearfix');
|
||
if (textContent) {
|
||
const fullText = textContent.innerText || textContent.textContent;
|
||
const firstLine = fullText.split('\n')[0].trim();
|
||
|
||
if (firstLine.length > 5 &&
|
||
!firstLine.startsWith('@') &&
|
||
!firstLine.startsWith('#') &&
|
||
!firstLine.includes('⬇') &&
|
||
!firstLine.match(/^\d{2}:\d{2}:\d{2}$/)) {
|
||
console.log('Found filename from .text-content:', firstLine);
|
||
return sanitizeFriendly(firstLine);
|
||
}
|
||
}
|
||
|
||
// 3. Weitere Titel-Selektoren
|
||
const titleSelectors = [
|
||
'.media-caption-text',
|
||
'.media-caption',
|
||
'.message-title',
|
||
'.video-title',
|
||
'.document-name',
|
||
'.file-name',
|
||
'.name',
|
||
'.title:not(.peer-title):not(.top-bar)',
|
||
'[class*="caption"]',
|
||
'[class*="title"]:not([class*="peer"]):not([class*="top"])'
|
||
];
|
||
|
||
for (const selector of titleSelectors) {
|
||
const element = messageContainer.querySelector(selector);
|
||
if (element) {
|
||
const text = element.innerText?.trim() || element.textContent?.trim();
|
||
if (text && text.length > 0 && text.length < 300) {
|
||
if (!text.includes('⬇') &&
|
||
!text.toLowerCase().includes('download') &&
|
||
!text.match(/^\d{2}:\d{2}:\d{2}$/)) {
|
||
console.log('Found filename from selector', selector, ':', text);
|
||
return sanitizeFriendly(text);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. Textblöcke, Text-Nodes, React Props usw. (wie in deinem Original)
|
||
const allTextElements = messageContainer.querySelectorAll('div, span, p');
|
||
for (const el of allTextElements) {
|
||
const text = Array.from(el.childNodes)
|
||
.filter(node => node.nodeType === Node.TEXT_NODE)
|
||
.map(node => node.textContent.trim())
|
||
.join(' ')
|
||
.trim();
|
||
|
||
if (text.length > 5 && text.length < 200) {
|
||
if (!text.startsWith('@') &&
|
||
!text.startsWith('#') &&
|
||
!text.includes('⬇') &&
|
||
!text.match(/^\d{2}:\d{2}:\d{2}$/) &&
|
||
/[A-Z]/.test(text)) {
|
||
console.log('Found potential filename from text element:', text);
|
||
return sanitizeFriendly(text);
|
||
}
|
||
}
|
||
}
|
||
|
||
const walker = document.createTreeWalker(
|
||
messageContainer,
|
||
NodeFilter.SHOW_TEXT,
|
||
{
|
||
acceptNode: function(node) {
|
||
const text = node.textContent.trim();
|
||
if (text.length < 5 ||
|
||
text.match(/^\d{2}:\d{2}:\d{2}$/) ||
|
||
text.startsWith('@') ||
|
||
text.startsWith('#') ||
|
||
text.includes('⬇')) {
|
||
return NodeFilter.FILTER_REJECT;
|
||
}
|
||
return NodeFilter.FILTER_ACCEPT;
|
||
}
|
||
},
|
||
false
|
||
);
|
||
|
||
let foundTexts = [];
|
||
let node;
|
||
while (node = walker.nextNode()) {
|
||
const text = node.textContent.trim();
|
||
if (text.length > 0) foundTexts.push(text);
|
||
}
|
||
|
||
for (const text of foundTexts) {
|
||
if (text.length > 5 && text.length < 200 && /[A-Z]/.test(text)) {
|
||
console.log('Using text node as filename:', text);
|
||
return sanitizeFriendly(text);
|
||
}
|
||
}
|
||
|
||
const reactKeys = Object.keys(messageContainer).filter(k => k.startsWith('__react'));
|
||
for (const key of reactKeys) {
|
||
try {
|
||
const fiber = messageContainer[key];
|
||
const filename = searchReactTree(fiber, ['fileName', 'file_name', 'name', 'title', 'caption']);
|
||
if (filename && typeof filename === 'string' && filename.length > 3) {
|
||
console.log('Found filename in React props:', filename);
|
||
return sanitizeFriendly(filename);
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
const match = videoUrl.match(/document(\d+)/);
|
||
if (match) {
|
||
console.warn('Using document ID as fallback');
|
||
return 'telegram_doc_' + match[1];
|
||
}
|
||
|
||
console.warn('Could not extract filename, using timestamp fallback');
|
||
return 'telegram_video_' + Date.now();
|
||
|
||
} catch (err) {
|
||
console.error('Error extracting filename:', err);
|
||
return 'telegram_video_' + Date.now();
|
||
}
|
||
}
|
||
|
||
// Durchsuche React Fiber Tree
|
||
function searchReactTree(node, keys, depth = 0, maxDepth = 15) {
|
||
if (depth > maxDepth || !node || typeof node !== 'object') return null;
|
||
|
||
try {
|
||
for (const key of keys) {
|
||
if (node[key]) {
|
||
const val = node[key];
|
||
if (typeof val === 'string' && val.length > 0) return val;
|
||
if (typeof val === 'object' && val.name) return val.name;
|
||
}
|
||
}
|
||
|
||
if (node.memoizedProps) {
|
||
for (const key of keys) {
|
||
if (node.memoizedProps[key]) {
|
||
const val = node.memoizedProps[key];
|
||
if (typeof val === 'string' && val.length > 0) return val;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (node.child) {
|
||
const result = searchReactTree(node.child, keys, depth + 1, maxDepth);
|
||
if (result) return result;
|
||
}
|
||
} catch (e) {}
|
||
|
||
return null;
|
||
}
|
||
|
||
// Download auslösen – OHNE .mp4 anhängen (für maximale Geschwindigkeit!)
|
||
function fallbackToInjectDownload(videoUrl, autoName) {
|
||
const event = new CustomEvent('downloadRequested', {
|
||
detail: {
|
||
url: videoUrl,
|
||
name: autoName, // Kein .mp4 hier → schneller Chunk-Download!
|
||
version: location.pathname,
|
||
}
|
||
});
|
||
document.dispatchEvent(event);
|
||
}
|
||
|
||
// Download-Button hinzufügen
|
||
function addVideoDownloadButton(video) {
|
||
if (isInTopicList()) return;
|
||
if (!video || video.tagName !== "VIDEO" ||
|
||
video.classList.contains("sticker-media") ||
|
||
video.classList.contains("media-sticker")) return;
|
||
if (!video.currentSrc && !video.src) return;
|
||
|
||
let container = video.closest('.media-inner') || video.parentElement || video.closest('div') || video;
|
||
if (!container || container.querySelector('.tg-video-btn')) return;
|
||
|
||
const computed = window.getComputedStyle(container);
|
||
if (computed.position === 'static') container.style.position = 'relative';
|
||
|
||
const btn = document.createElement('button');
|
||
btn.className = 'tg-video-btn';
|
||
btn.innerText = '⬇ DOWNLOAD';
|
||
btn.title = 'Video herunterladen';
|
||
btn.style.cssText = `
|
||
position: absolute !important;
|
||
top: 10px !important;
|
||
right: 10px !important;
|
||
z-index: 100000 !important;
|
||
background: #e63946 !important;
|
||
color: white !important;
|
||
border: none !important;
|
||
border-radius: 8px !important;
|
||
padding: 8px 12px !important;
|
||
font-size: 14px !important;
|
||
font-weight: bold !important;
|
||
cursor: pointer !important;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.6) !important;
|
||
`;
|
||
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
|
||
const videoUrl = video.currentSrc || video.src;
|
||
if (!videoUrl) {
|
||
alert('Video-URL nicht verfügbar. Bitte das Video einmal kurz abspielen.');
|
||
return;
|
||
}
|
||
|
||
const autoName = extractTelegramFilename(video);
|
||
|
||
console.log('=== DOWNLOAD INFO ===');
|
||
console.log('Final filename (ohne .mp4):', autoName);
|
||
|
||
fallbackToInjectDownload(videoUrl, autoName);
|
||
});
|
||
|
||
container.appendChild(btn);
|
||
}
|
||
|
||
// Alle Videos verarbeiten
|
||
function processVideos() {
|
||
if (isInTopicList()) return;
|
||
document.querySelectorAll('video:not([data-tg-video])').forEach(video => {
|
||
try {
|
||
video.dataset.tgVideo = 'true';
|
||
addVideoDownloadButton(video);
|
||
} catch (err) {
|
||
console.error('Error processing video element', err);
|
||
}
|
||
});
|
||
}
|
||
|
||
const observer = new MutationObserver(processVideos);
|
||
observer.observe(document.body, { childList: true, subtree: true });
|
||
setInterval(processVideos, 1200);
|
||
processVideos();
|
||
|
||
// Completed-Event weiterleiten
|
||
document.addEventListener('telDownloaderCompleted', (e) => {
|
||
try {
|
||
const detail = e.detail || {};
|
||
const extId = detail.extensionId || chrome.runtime.id;
|
||
chrome.runtime.sendMessage(extId, {
|
||
action: detail.action || 'videoProgressCompleted',
|
||
videoId: detail.videoId,
|
||
clientId: detail.clientId,
|
||
name: detail.name,
|
||
version: detail.version,
|
||
locale: detail.locale
|
||
}, (res) => {
|
||
if (chrome.runtime.lastError) {
|
||
console.error('Error sending message to background:', chrome.runtime.lastError.message);
|
||
}
|
||
});
|
||
} catch (err) {
|
||
console.error('Failed to forward telDownloaderCompleted to extension:', err);
|
||
}
|
||
});
|
||
|
||
// inject.js laden
|
||
const script = document.createElement('script');
|
||
script.src = chrome.runtime.getURL('inject.js');
|
||
script.setAttribute('data-extension-id', chrome.runtime.id);
|
||
(document.head || document.documentElement).appendChild(script); |