4 Commits
1.4 ... 1.5

Author SHA1 Message Date
7f7a59f54f inject.js aktualisiert 2025-12-21 15:19:03 +00:00
a89785a5c8 content_tg.js aktualisiert 2025-12-21 15:18:49 +00:00
53dfbbddaa manifest.json aktualisiert 2025-12-21 15:18:27 +00:00
8e640efa37 background.js aktualisiert 2025-12-21 15:18:10 +00:00
4 changed files with 1214 additions and 875 deletions

View File

@@ -1,99 +1,96 @@
// background.js const downloadsMap = new Map(); // downloadId -> { tabId, filename }
// Service worker / background script: startet chrome.downloads.download und sendet Fortschritte an die Content-Tab.
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const downloadsMap = new Map(); // downloadId -> { tabId, filename } if (!msg || !msg.action) {
sendResponse({ status: "unknown" });
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { return;
if (!msg || !msg.action) { }
sendResponse({ status: "unknown" });
return; if (msg.action === "videoProgressCompleted") {
} console.log('Video completed:', msg.videoId, 'name:', msg.name);
sendResponse({ status: "success" });
if (msg.action === "videoProgressCompleted") { return;
console.log('Video completed:', msg.videoId, 'name:', msg.name); }
sendResponse({ status: "success" });
return; if (msg.action === "startBackgroundDownload") {
} const url = msg.url;
let filename = msg.filename || ('download_' + Date.now());
if (msg.action === "startBackgroundDownload") {
const url = msg.url; if (!url) {
let filename = msg.filename || ('download_' + Date.now()); sendResponse({ status: "error", message: "No URL provided" });
return;
if (!url) { }
sendResponse({ status: "error", message: "No URL provided" });
return; try {
} chrome.downloads.download(
{
try { url: url,
chrome.downloads.download( filename: filename,
{ conflictAction: "uniquify",
url: url, saveAs: false
filename: filename, },
conflictAction: "uniquify", downloadId => {
saveAs: false if (chrome.runtime.lastError) {
}, console.error('chrome.downloads.download error:', chrome.runtime.lastError.message);
downloadId => { sendResponse({ status: "error", message: chrome.runtime.lastError.message });
if (chrome.runtime.lastError) { } else {
console.error('chrome.downloads.download error:', chrome.runtime.lastError.message); const tabId = (sender && sender.tab && sender.tab.id) ? sender.tab.id : null;
sendResponse({ status: "error", message: chrome.runtime.lastError.message }); downloadsMap.set(downloadId, { tabId, filename });
} else { sendResponse({ status: "started", downloadId });
const tabId = (sender && sender.tab && sender.tab.id) ? sender.tab.id : null; }
downloadsMap.set(downloadId, { tabId, filename }); }
sendResponse({ status: "started", downloadId }); );
} return true;
} } catch (e) {
); console.error('Exception starting download', e);
return true; sendResponse({ status: "error", message: String(e) });
} catch (e) { return;
console.error('Exception starting download', e); }
sendResponse({ status: "error", message: String(e) }); }
return;
} sendResponse({ status: "ok" });
} });
sendResponse({ status: "ok" }); // Listen for download changes and forward progress/state to the tab
}); chrome.downloads.onChanged.addListener(delta => {
const info = downloadsMap.get(delta.id);
// Listen for download changes and forward progress/state to the tab if (!info || !info.tabId) return;
chrome.downloads.onChanged.addListener(delta => {
const info = downloadsMap.get(delta.id); if (delta.bytesReceived) {
if (!info || !info.tabId) return; chrome.downloads.search({ id: delta.id }, items => {
const item = items && items[0];
if (delta.bytesReceived) { if (!item) return;
chrome.downloads.search({ id: delta.id }, items => {
const item = items && items[0]; const received = item.bytesReceived || 0;
if (!item) return; const total = item.totalBytes || 0;
const percent = total > 0 ? Math.round(100 * received / total) : null;
const received = item.bytesReceived || 0;
const total = item.totalBytes || 0; chrome.tabs.sendMessage(info.tabId, {
const percent = total > 0 ? Math.round(100 * received / total) : null; action: 'downloadProgress',
downloadId: delta.id,
chrome.tabs.sendMessage(info.tabId, { bytesReceived: received,
action: 'downloadProgress', totalBytes: total,
downloadId: delta.id, percent
bytesReceived: received, }, () => { /* ignore response */ });
totalBytes: total, });
percent }
}, () => { /* ignore response */ });
}); if (delta.state && delta.state.current === 'complete') {
} chrome.tabs.sendMessage(info.tabId, {
action: 'downloadComplete',
if (delta.state && delta.state.current === 'complete') { downloadId: delta.id,
chrome.tabs.sendMessage(info.tabId, { filename: info.filename
action: 'downloadComplete', }, () => { /* ignore response */ });
downloadId: delta.id, downloadsMap.delete(delta.id);
filename: info.filename }
}, () => { /* ignore response */ });
downloadsMap.delete(delta.id); if (delta.state && delta.state.current === 'interrupted') {
} chrome.tabs.sendMessage(info.tabId, {
action: 'downloadFailed',
if (delta.state && delta.state.current === 'interrupted') { downloadId: delta.id,
chrome.tabs.sendMessage(info.tabId, { filename: info.filename,
action: 'downloadFailed', reason: delta.state && delta.state.current
downloadId: delta.id, }, () => { /* ignore response */ });
filename: info.filename, downloadsMap.delete(delta.id);
reason: delta.state && delta.state.current }
}, () => { /* ignore response */ });
downloadsMap.delete(delta.id);
}
}); });

View File

@@ -1,357 +1,537 @@
// Topic-Übersicht erkennen (keine Buttons dort) // Topic-Übersicht erkennen (keine Buttons dort)
function isInTopicList() { function isInTopicList() {
return document.querySelector('.topics-list, .topic-list, .forums-list, .chat-list .topic') !== null || return document.querySelector('.topics-list, .topic-list, .forums-list, .chat-list .topic') !== null ||
document.body.classList.contains('topics-mode'); document.body.classList.contains('topics-mode');
} }
// Kleine Helfer-Funktion: freundlich sanitisieren (erhalte Leerzeichen, Klammern, Punkte) // Kleine Helfer-Funktion: freundlich sanitisieren (erhalte Leerzeichen, Klammern, Punkte)
function sanitizeFriendly(name) { function sanitizeFriendly(name) {
if (!name) return 'Telegram'; if (!name) return 'Telegram';
return name.toString().replace(/[\/\\\?\%\*\:\|\"\<\>]/g, "_").trim(); return name.toString().replace(/[\/\\\?\%\*\:\|\"\<\>]/g, "_").trim();
} }
// Extrahiere den Original-Dateinamen aus Telegram-Metadaten // Extrahiere den Original-Dateinamen aus Telegram-Metadaten
function extractTelegramFilename(video) { function extractTelegramFilename(element) {
try { try {
const videoUrl = video.currentSrc || video.src; const elementUrl = element.currentSrc || element.src;
console.log('=== FILENAME EXTRACTION ==='); console.log('=== FILENAME EXTRACTION ===');
console.log('Video URL:', videoUrl); console.log('Element URL:', elementUrl);
// 1. Suche die Message, die das Video enthält // 1. Suche die Message, die das Element enthält
const messageContainer = video.closest('.message, .Message, .bubble, .media-container'); const messageContainer = element.closest('.message, .Message, .bubble, .media-container');
if (!messageContainer) { if (!messageContainer) {
console.warn('No message container found'); console.warn('No message container found');
return 'telegram_video_' + Date.now(); return 'telegram_media_' + Date.now();
} }
console.log('Message container found'); console.log('Message container found');
// === PRIORITÄT 1: VERBESSERTES ALBUM/SERIEN-HANDLING === // 2. Zuerst: Suche nach .text-content (funktioniert für einzelne Medien)
const albumContainer = messageContainer.querySelector('.Album'); const textContent = messageContainer.querySelector('.text-content, .text-content.clearfix');
if (albumContainer) { if (textContent) {
console.log('Album erkannt → verbessertes Serien-Naming'); const fullText = textContent.innerText || textContent.textContent;
const firstLine = fullText.split('\n')[0].trim();
const videoContainer = video.closest('.album-item-select-wrapper');
if (videoContainer) { if (firstLine.length > 5 &&
const allVideoContainers = albumContainer.querySelectorAll('.album-item-select-wrapper'); !firstLine.startsWith('@') &&
let videoIndex = -1; !firstLine.startsWith('#') &&
for (let i = 0; i < allVideoContainers.length; i++) { !firstLine.includes('⬇') &&
if (allVideoContainers[i] === videoContainer) { !firstLine.match(/^\d{2}:\d{2}:\d{2}$/)) {
videoIndex = i; console.log('Found filename from .text-content:', firstLine);
break; return sanitizeFriendly(firstLine);
} }
} }
console.log('Video index in album:', videoIndex); // 3. Bei Alben/Serien
const albumContainer = messageContainer.querySelector('.album, .Album, [class*="album"]');
if (videoIndex >= 0) { if (albumContainer) {
const textContent = messageContainer.querySelector('.text-content.clearfix.with-meta, .text-content'); console.log('Album/Series detected');
if (textContent) {
const lines = textContent.innerText.split('\n') let elementContainer = element.closest('.album-item-select-wrapper, .album-item, .media-item');
.map(l => l.trim()) if (elementContainer) {
.filter(l => l.length > 0); const allContainers = albumContainer.querySelectorAll('.album-item-select-wrapper, .album-item');
let elementIndex = -1;
console.log('Album text lines:', lines); for (let i = 0; i < allContainers.length; i++) {
if (allContainers[i] === elementContainer || allContainers[i].contains(element)) {
const seriesTitle = lines[0] || 'Unknown Series'; elementIndex = i;
break;
// 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]; console.log('Element index in album:', elementIndex);
const match = line.match(/^(S\d{2}E\d{2})\s*-\s*(.+)$/);
if (match) { const contentInner = messageContainer.querySelector('.content-inner, .text, .message-text');
const code = match[1]; if (contentInner && elementIndex >= 0) {
const title = match[2].trim(); const fullText = contentInner.innerText || contentInner.textContent;
const finalName = `${sanitizeFriendly(seriesTitle)} - ${code} - ${sanitizeFriendly(title)}`;
console.log('Perfekter Serien-Dateiname:', finalName); const episodePattern = /S\d{2}E\d{2}\s*-\s*[^\n]+/gi;
return finalName; const episodes = fullText.match(episodePattern);
}
} if (episodes && episodes[elementIndex]) {
const episodeName = episodes[elementIndex].trim();
// Fallback: Zeile nach Serientitel als Episodentitel console.log('Found episode name for index', elementIndex, ':', episodeName);
if (lines.length > videoIndex + 1) { return sanitizeFriendly(episodeName);
const episodeTitle = lines[videoIndex + 1]; }
if (episodeTitle.length > 5) {
const finalName = `${sanitizeFriendly(seriesTitle)} - ${sanitizeFriendly(episodeTitle)}`; const lines = fullText.split('\n').map(l => l.trim()).filter(l => l.length > 5);
console.log('Serien-Fallback-Dateiname:', finalName); const episodeLines = lines.filter(line =>
return finalName; /S\d{2}E\d{2}/.test(line) ||
} (line.includes('-') && line.length > 10 && line.length < 150)
} );
return sanitizeFriendly(seriesTitle + ' - Episode ' + (videoIndex + 1)); if (episodeLines[elementIndex]) {
} console.log('Found episode from lines:', episodeLines[elementIndex]);
} return sanitizeFriendly(episodeLines[elementIndex]);
} }
} }
}
// 2. Einzelvideo: Erste Zeile aus .text-content }
const textContent = messageContainer.querySelector('.text-content, .text-content.clearfix');
if (textContent) { // 4. Weitere Selektoren
const fullText = textContent.innerText || textContent.textContent; const titleSelectors = [
const firstLine = fullText.split('\n')[0].trim(); '.media-caption-text',
'.media-caption',
if (firstLine.length > 5 && '.message-title',
!firstLine.startsWith('@') && '.video-title',
!firstLine.startsWith('#') && '.document-name',
!firstLine.includes('⬇') && '.file-name',
!firstLine.match(/^\d{2}:\d{2}:\d{2}$/)) { '.name',
console.log('Found filename from .text-content:', firstLine); '.title:not(.peer-title):not(.top-bar)'
return sanitizeFriendly(firstLine); ];
}
} for (const selector of titleSelectors) {
const el = messageContainer.querySelector(selector);
// 3. Weitere Titel-Selektoren if (el) {
const titleSelectors = [ const text = el.innerText?.trim() || el.textContent?.trim();
'.media-caption-text', if (text && text.length > 0 && text.length < 300) {
'.media-caption', if (!text.includes('⬇') &&
'.message-title', !text.toLowerCase().includes('download') &&
'.video-title', !text.match(/^\d{2}:\d{2}:\d{2}$/)) {
'.document-name', console.log('Found filename from selector', selector, ':', text);
'.file-name', return sanitizeFriendly(text);
'.name', }
'.title:not(.peer-title):not(.top-bar)', }
'[class*="caption"]', }
'[class*="title"]:not([class*="peer"]):not([class*="top"])' }
];
// 5. Fallbacks
for (const selector of titleSelectors) { const match = elementUrl?.match(/document(\d+)/);
const element = messageContainer.querySelector(selector); if (match) {
if (element) { console.warn('Using document ID as fallback');
const text = element.innerText?.trim() || element.textContent?.trim(); return 'telegram_doc_' + match[1];
if (text && text.length > 0 && text.length < 300) { }
if (!text.includes('⬇') &&
!text.toLowerCase().includes('download') && console.warn('Could not extract filename, using timestamp fallback');
!text.match(/^\d{2}:\d{2}:\d{2}$/)) { return 'telegram_media_' + Date.now();
console.log('Found filename from selector', selector, ':', text);
return sanitizeFriendly(text); } catch (err) {
} console.error('Error extracting filename:', err);
} return 'telegram_media_' + Date.now();
} }
} }
// 4. Textblöcke, Text-Nodes, React Props usw. (wie in deinem Original) // Video-Button
const allTextElements = messageContainer.querySelectorAll('div, span, p'); function addVideoDownloadButton(video) {
for (const el of allTextElements) { if (isInTopicList()) return;
const text = Array.from(el.childNodes) if (!video || video.tagName !== "VIDEO" ||
.filter(node => node.nodeType === Node.TEXT_NODE) video.classList.contains("sticker-media") ||
.map(node => node.textContent.trim()) video.classList.contains("media-sticker")) return;
.join(' ') if (!video.currentSrc && !video.src) return;
.trim();
let container = video.parentElement || video.closest('div') || video;
if (text.length > 5 && text.length < 200) { if (!container || container.querySelector('.tg-video-btn')) return;
if (!text.startsWith('@') &&
!text.startsWith('#') && const computed = window.getComputedStyle(container);
!text.includes('⬇') && if (computed.position === 'static') container.style.position = 'relative';
!text.match(/^\d{2}:\d{2}:\d{2}$/) &&
/[A-Z]/.test(text)) { const btn = document.createElement('button');
console.log('Found potential filename from text element:', text); btn.className = 'tg-video-btn';
return sanitizeFriendly(text); btn.innerText = '⬇ Download';
} btn.title = 'Video herunterladen';
} btn.style.cssText = `
} position: absolute !important;
top: 10px !important;
const walker = document.createTreeWalker( right: 10px !important;
messageContainer, z-index: 100000 !important;
NodeFilter.SHOW_TEXT, background: #e63946 !important;
{ color: white !important;
acceptNode: function(node) { border: none !important;
const text = node.textContent.trim(); border-radius: 8px !important;
if (text.length < 5 || padding: 8px 12px !important;
text.match(/^\d{2}:\d{2}:\d{2}$/) || font-size: 14px !important;
text.startsWith('@') || font-weight: bold !important;
text.startsWith('#') || cursor: pointer !important;
text.includes('⬇')) { box-shadow: 0 4px 12px rgba(0,0,0,0.6) !important;
return NodeFilter.FILTER_REJECT; `;
}
return NodeFilter.FILTER_ACCEPT; btn.addEventListener('click', (e) => {
} e.stopPropagation();
}, e.preventDefault();
false
); const videoUrl = video.currentSrc || video.src;
if (!videoUrl) {
let foundTexts = []; alert('Video-URL nicht verfügbar.');
let node; return;
while (node = walker.nextNode()) { }
const text = node.textContent.trim();
if (text.length > 0) foundTexts.push(text); const autoName = extractTelegramFilename(video);
} console.log('=== VIDEO DOWNLOAD ===', autoName);
for (const text of foundTexts) { const event = new CustomEvent('downloadRequested', {
if (text.length > 5 && text.length < 200 && /[A-Z]/.test(text)) { detail: { url: videoUrl, name: autoName, version: location.pathname }
console.log('Using text node as filename:', text); });
return sanitizeFriendly(text); document.dispatchEvent(event);
} });
}
container.appendChild(btn);
const reactKeys = Object.keys(messageContainer).filter(k => k.startsWith('__react')); }
for (const key of reactKeys) {
try { // Bild-Button
const fiber = messageContainer[key]; function addImageDownloadButton(img) {
const filename = searchReactTree(fiber, ['fileName', 'file_name', 'name', 'title', 'caption']); if (isInTopicList()) return;
if (filename && typeof filename === 'string' && filename.length > 3) { if (!img || img.tagName !== "IMG") return;
console.log('Found filename in React props:', filename); if (img.classList.contains('emoji') || img.classList.contains('thumbnail') ||
return sanitizeFriendly(filename); img.width < 100 || img.height < 100) return;
}
} catch (e) {} let container = img.closest('.media-inner') || img.closest('.Message') || img.parentElement;
} if (!container || container.querySelector('.tg-img-btn')) return;
const match = videoUrl.match(/document(\d+)/); if (window.getComputedStyle(container).position === 'static') {
if (match) { container.style.position = 'relative';
console.warn('Using document ID as fallback'); }
return 'telegram_doc_' + match[1];
} const btn = document.createElement('button');
btn.className = 'tg-img-btn';
console.warn('Could not extract filename, using timestamp fallback'); btn.innerText = '⬇';
return 'telegram_video_' + Date.now(); btn.title = 'Bild herunterladen';
btn.style.cssText = `
} catch (err) { position: absolute !important;
console.error('Error extracting filename:', err); top: 5px !important;
return 'telegram_video_' + Date.now(); left: 5px !important;
} z-index: 999999 !important;
} background: rgba(42, 157, 143, 0.95) !important;
color: white !important;
// Durchsuche React Fiber Tree border: 2px solid white !important;
function searchReactTree(node, keys, depth = 0, maxDepth = 15) { border-radius: 50% !important;
if (depth > maxDepth || !node || typeof node !== 'object') return null; width: 44px !important;
height: 44px !important;
try { font-size: 22px !important;
for (const key of keys) { cursor: pointer !important;
if (node[key]) { box-shadow: 0 2px 8px rgba(0,0,0,0.8) !important;
const val = node[key]; display: flex !important;
if (typeof val === 'string' && val.length > 0) return val; align-items: center !important;
if (typeof val === 'object' && val.name) return val.name; justify-content: center !important;
} pointer-events: auto !important;
} `;
if (node.memoizedProps) { btn.onmouseenter = () => btn.style.transform = 'scale(1.15)';
for (const key of keys) { btn.onmouseleave = () => btn.style.transform = 'scale(1)';
if (node.memoizedProps[key]) {
const val = node.memoizedProps[key]; btn.addEventListener('click', (e) => {
if (typeof val === 'string' && val.length > 0) return val; e.stopPropagation();
} e.preventDefault();
}
} const imgUrl = img.src || img.currentSrc;
if (!imgUrl || imgUrl.startsWith('data:')) {
if (node.child) { alert('Bild-URL nicht verfügbar.');
const result = searchReactTree(node.child, keys, depth + 1, maxDepth); return;
if (result) return result; }
}
} catch (e) {} const autoName = extractTelegramFilename(img) || 'telegram_image';
console.log('=== IMAGE DOWNLOAD ===', autoName);
return null;
} const event = new CustomEvent('imageDownloadRequested', {
detail: { url: imgUrl, name: autoName }
// Download auslösen OHNE .mp4 anhängen (für maximale Geschwindigkeit!) });
function fallbackToInjectDownload(videoUrl, autoName) { document.dispatchEvent(event);
const event = new CustomEvent('downloadRequested', { });
detail: {
url: videoUrl, container.appendChild(btn);
name: autoName, // Kein .mp4 hier → schneller Chunk-Download! console.log('✅ Image button added');
version: location.pathname, }
}
}); // Audio-Button
document.dispatchEvent(event); function addAudioDownloadButton(audioContainer) {
} if (isInTopicList()) return;
if (!audioContainer || audioContainer.querySelector('.tg-audio-btn')) return;
// Download-Button hinzufügen
function addVideoDownloadButton(video) { const audio = audioContainer.querySelector('audio');
if (isInTopicList()) return; if (!audio) return;
if (!video || video.tagName !== "VIDEO" ||
video.classList.contains("sticker-media") || if (window.getComputedStyle(audioContainer).position === 'static') {
video.classList.contains("media-sticker")) return; audioContainer.style.position = 'relative';
if (!video.currentSrc && !video.src) return; }
let container = video.closest('.media-inner') || video.parentElement || video.closest('div') || video; const btn = document.createElement('button');
if (!container || container.querySelector('.tg-video-btn')) return; btn.className = 'tg-audio-btn';
btn.innerText = '⬇ Download';
const computed = window.getComputedStyle(container); btn.title = 'Audio herunterladen';
if (computed.position === 'static') container.style.position = 'relative'; btn.style.cssText = `
position: absolute !important;
const btn = document.createElement('button'); top: 10px !important;
btn.className = 'tg-video-btn'; right: 10px !important;
btn.innerText = '⬇ DOWNLOAD'; z-index: 999999 !important;
btn.title = 'Video herunterladen'; background: rgba(244, 162, 97, 0.95) !important;
btn.style.cssText = ` color: white !important;
position: absolute !important; border: 2px solid white !important;
top: 10px !important; border-radius: 50% !important;
right: 10px !important; width: 40px !important;
z-index: 100000 !important; height: 40px !important;
background: #e63946 !important; font-size: 20px !important;
color: white !important; cursor: pointer !important;
border: none !important; box-shadow: 0 2px 8px rgba(0,0,0,0.8) !important;
border-radius: 8px !important; display: flex !important;
padding: 8px 12px !important; align-items: center !important;
font-size: 14px !important; justify-content: center !important;
font-weight: bold !important; pointer-events: auto !important;
cursor: pointer !important; `;
box-shadow: 0 4px 12px rgba(0,0,0,0.6) !important;
`; btn.onmouseenter = () => btn.style.transform = 'scale(1.15)';
btn.onmouseleave = () => btn.style.transform = 'scale(1)';
btn.addEventListener('click', (e) => {
e.stopPropagation(); btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
e.preventDefault();
const videoUrl = video.currentSrc || video.src;
if (!videoUrl) { const audioUrl = audio.currentSrc || audio.src;
alert('Video-URL nicht verfügbar. Bitte das Video einmal kurz abspielen.'); if (!audioUrl) {
return; alert('Audio-URL nicht verfügbar.');
} return;
}
const autoName = extractTelegramFilename(video);
const autoName = extractTelegramFilename(audioContainer) || 'telegram_audio';
console.log('=== DOWNLOAD INFO ==='); console.log('=== AUDIO DOWNLOAD ===', autoName);
console.log('Final filename (ohne .mp4):', autoName);
const event = new CustomEvent('audioDownloadRequested', {
fallbackToInjectDownload(videoUrl, autoName); detail: { url: audioUrl, name: autoName }
}); });
document.dispatchEvent(event);
container.appendChild(btn); });
}
audioContainer.appendChild(btn);
// Alle Videos verarbeiten console.log('✅ Audio button added');
function processVideos() { }
if (isInTopicList()) return;
document.querySelectorAll('video:not([data-tg-video])').forEach(video => { // Document-Button (für .mp3, .zip, .pdf, etc.)
try { function addDocumentDownloadButton(docContainer) {
video.dataset.tgVideo = 'true'; if (isInTopicList()) return;
addVideoDownloadButton(video); if (!docContainer || docContainer.querySelector('.tg-doc-btn')) return;
} catch (err) {
console.error('Error processing video element', err); // Suche nach dem Dateinamen - verschiedene mögliche Selektoren
} const docNameEl = docContainer.querySelector('.file-title, .document-name, .file-name');
}); const docName = docNameEl?.innerText?.trim() || docNameEl?.title || '';
}
// Suche nach File-Extension
const observer = new MutationObserver(processVideos); const extEl = docContainer.querySelector('.file-ext');
observer.observe(document.body, { childList: true, subtree: true }); const fileExt = extEl?.innerText?.trim()?.toLowerCase() || '';
setInterval(processVideos, 1200);
processVideos(); // Wenn kein Name gefunden wurde, ignoriere
if (!docName && !fileExt) return;
// Completed-Event weiterleiten
document.addEventListener('telDownloaderCompleted', (e) => { // Prüfe Dateityp basierend auf Extension oder Name
try { const isAudio = /\.(mp3|ogg|wav|m4a|flac|aac|opus)$/i.test(docName) ||
const detail = e.detail || {}; ['mp3', 'ogg', 'wav', 'm4a', 'flac', 'aac', 'opus'].includes(fileExt);
const extId = detail.extensionId || chrome.runtime.id; const isArchive = /\.(zip|rar|7z|tar|gz)$/i.test(docName) ||
chrome.runtime.sendMessage(extId, { ['zip', 'rar', '7z', 'tar', 'gz'].includes(fileExt);
action: detail.action || 'videoProgressCompleted', const isDocument = /\.(pdf|doc|docx|txt|xlsx|xls|ppt)$/i.test(docName) ||
videoId: detail.videoId, ['pdf', 'doc', 'docx', 'txt', 'xlsx', 'xls', 'ppt'].includes(fileExt);
clientId: detail.clientId,
name: detail.name, if (window.getComputedStyle(docContainer).position === 'static') {
version: detail.version, docContainer.style.position = 'relative';
locale: detail.locale }
}, (res) => {
if (chrome.runtime.lastError) { // Icon und Farbe basierend auf Dateityp
console.error('Error sending message to background:', chrome.runtime.lastError.message); let icon = '⬇';
} let bgColor = 'rgba(168, 85, 247, 0.95)';
}); let title = 'Datei herunterladen';
} catch (err) {
console.error('Failed to forward telDownloaderCompleted to extension:', err); if (isAudio) {
} icon = '⬇';
}); bgColor = 'rgba(244, 162, 97, 0.95)';
title = 'Audio herunterladen';
// inject.js laden } else if (isArchive) {
const script = document.createElement('script'); icon = '⬇';
script.src = chrome.runtime.getURL('inject.js'); bgColor = 'rgba(59, 130, 246, 0.95)';
script.setAttribute('data-extension-id', chrome.runtime.id); title = 'Archiv herunterladen';
} else if (isDocument) {
icon = '⬇';
bgColor = 'rgba(168, 85, 247, 0.95)';
title = 'Dokument herunterladen';
}
const btn = document.createElement('button');
btn.className = 'tg-doc-btn';
btn.innerText = icon;
btn.title = title + (docName ? ': ' + docName : '');
btn.style.cssText = `
position: absolute !important;
top: 10px !important;
right: 10px !important;
z-index: 999999 !important;
background: ${bgColor} !important;
color: white !important;
border: 2px solid white !important;
border-radius: 50% !important;
width: 40px !important;
height: 40px !important;
font-size: 20px !important;
cursor: pointer !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.8) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
pointer-events: auto !important;
`;
btn.onmouseenter = () => btn.style.transform = 'scale(1.15)';
btn.onmouseleave = () => btn.style.transform = 'scale(1)';
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
// Versuche Download-URL zu finden
let downloadUrl = null;
// Methode 1: Suche nach data-doc-id im File-Element oder Container
const fileEl = docContainer.querySelector('.File');
const docId = fileEl?.dataset?.docId ||
docContainer.dataset?.docId ||
docContainer.closest('[data-doc-id]')?.dataset?.docId;
if (docId) {
// Konstruiere die Telegram progressive URL
downloadUrl = `${location.origin}/a/progressive/document${docId}`;
}
// Methode 2: Suche nach existierendem Download-Link
if (!downloadUrl) {
const downloadLink = docContainer.querySelector('a[href*="document"], a[download]');
if (downloadLink) {
downloadUrl = downloadLink.href;
}
}
// Methode 3: Suche im parent Message nach document-ID
if (!downloadUrl) {
const message = docContainer.closest('.Message');
const messageId = message?.id?.replace('message-', '');
if (messageId) {
// Versuche document-ID aus verschiedenen Quellen
const allDataIds = message.querySelectorAll('[data-doc-id]');
if (allDataIds.length > 0) {
const firstDocId = allDataIds[0].dataset.docId;
downloadUrl = `${location.origin}/a/progressive/document${firstDocId}`;
}
}
}
if (!downloadUrl) {
alert('Download-URL nicht gefunden. Bitte klicke zuerst auf die Datei um sie zu laden.');
return;
}
const finalName = docName || 'telegram_file';
console.log('=== DOCUMENT DOWNLOAD ===');
console.log('Name:', finalName);
console.log('URL:', downloadUrl);
const eventName = isAudio ? 'audioDownloadRequested' : 'documentDownloadRequested';
const event = new CustomEvent(eventName, {
detail: { url: downloadUrl, name: finalName }
});
document.dispatchEvent(event);
});
docContainer.appendChild(btn);
console.log('✅ Document button added:', docName || fileExt);
}
// Alle Medien verarbeiten
function processMedia() {
if (isInTopicList()) return;
// Videos
document.querySelectorAll('video:not([data-tg-video])').forEach(video => {
try {
video.dataset.tgVideo = 'true';
addVideoDownloadButton(video);
} catch (err) {
console.error('Error processing video', err);
}
});
// Bilder
document.querySelectorAll('img.full-media').forEach(img => {
try {
const container = img.closest('.media-inner, .Message') || img.parentElement;
if (container && !container.querySelector('.tg-img-btn')) {
addImageDownloadButton(img);
}
} catch (err) {
console.error('Error processing image', err);
}
});
// Audio mit <audio> Element
document.querySelectorAll('.Audio, .audio').forEach(container => {
try {
if (!container.querySelector('.tg-audio-btn')) {
const audio = container.querySelector('audio');
if (audio) addAudioDownloadButton(container);
}
} catch (err) {
console.error('Error processing audio', err);
}
});
// Documents (.mp3, .zip, .pdf, etc.) - erkennbar an .document Container MIT .File Element
document.querySelectorAll('.document, .Document').forEach(container => {
try {
// Prüfe ob es ein .File Element gibt (das ist der Indikator für Documents)
const fileEl = container.querySelector('.File');
const fileTitle = container.querySelector('.file-title');
if ((fileEl || fileTitle) && !container.querySelector('.tg-doc-btn')) {
addDocumentDownloadButton(container);
}
} catch (err) {
console.error('Error processing document', err);
}
});
}
const observer = new MutationObserver(processMedia);
observer.observe(document.body, { childList: true, subtree: true });
setInterval(processMedia, 1200);
processMedia();
console.log('✅ TG Downloader loaded - Video, Image & Audio support enabled');
// Event-Weiterleitung
document.addEventListener('telDownloaderCompleted', (e) => {
try {
const detail = e.detail || {};
chrome.runtime.sendMessage(detail.extensionId || chrome.runtime.id, {
action: detail.action || 'videoProgressCompleted',
videoId: detail.videoId,
clientId: detail.clientId,
name: detail.name,
version: detail.version,
locale: detail.locale
}, () => {
if (chrome.runtime.lastError) {
console.error('Error sending message:', chrome.runtime.lastError.message);
}
});
} catch (err) {
console.error('Failed to forward event:', 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); (document.head || document.documentElement).appendChild(script);

952
inject.js
View File

@@ -1,396 +1,558 @@
(function () { (function () {
const scriptElement = document.currentScript; const scriptElement = document.currentScript;
const extensionId = scriptElement ? scriptElement.getAttribute("data-extension-id") : null; const extensionId = scriptElement ? scriptElement.getAttribute("data-extension-id") : null;
console.log('inject.js running, extensionId=', extensionId); console.log('inject.js running, extensionId=', extensionId);
// Empfang von "downloadRequested" (kommt vom content script) // Empfang von "downloadRequested" (kommt vom content script)
document.addEventListener("downloadRequested", function(e) { document.addEventListener("downloadRequested", function(e) {
try { try {
let t = e.detail.url; let t = e.detail.url;
let l = e.detail.name; let l = e.detail.name;
let r = e.detail.version; let r = e.detail.version;
let o = e.detail.clientId; let o = e.detail.clientId;
let n = e.detail.locale; let n = e.detail.locale;
tel_download_video(t, l, r, o, n); tel_download_video(t, l, r, o, n);
} catch (err) { } catch (err) {
console.error('Error in downloadRequested handler', err); console.error('Error in downloadRequested handler', err);
} }
}); });
document.addEventListener("imageDownloadRequested", function(e) { document.addEventListener("imageDownloadRequested", function(e) {
try { try {
let t = e.detail.url; let t = e.detail.url;
let l = e.detail.name; let l = e.detail.name;
tel_download_image(t, l); tel_download_image(t, l);
} catch (err) { } catch (err) {
console.error('Error in imageDownloadRequested handler', err); console.error('Error in imageDownloadRequested handler', err);
} }
}); });
// Hilfsfunktion: progress-bar-container document.addEventListener("audioDownloadRequested", function(e) {
function ensureProgressContainer() { try {
let container = document.getElementById("tel-downloader-progress-bar-container"); let t = e.detail.url;
if (!container) { let l = e.detail.name;
container = document.createElement("div"); tel_download_audio(t, l);
container.id = "tel-downloader-progress-bar-container"; } catch (err) {
container.style.position = "fixed"; console.error('Error in audioDownloadRequested handler', err);
container.style.bottom = "0"; }
container.style.right = "0"; });
container.style.display = "flex";
container.style.flexDirection = "column"; document.addEventListener("documentDownloadRequested", function(e) {
container.style.gap = "0.4rem"; try {
container.style.padding = "0.4rem"; let t = e.detail.url;
container.style.zIndex = "1600"; let l = e.detail.name;
document.body.appendChild(container); tel_download_document(t, l);
} } catch (err) {
return container; console.error('Error in documentDownloadRequested handler', err);
} }
});
const createProgressBar = (id, filename) => {
const dark = document.querySelector("html")?.classList?.contains("night") || // Hilfsfunktion: progress-bar-container
document.querySelector("html")?.classList?.contains("theme-dark"); function ensureProgressContainer() {
const container = ensureProgressContainer(); let container = document.getElementById("tel-downloader-progress-bar-container");
container.style.zIndex = location.pathname.startsWith("/k/") ? "4" : "1600"; if (!container) {
container = document.createElement("div");
const n = document.createElement("div"); container.id = "tel-downloader-progress-bar-container";
n.id = "tel-downloader-progress-" + id; container.style.position = "fixed";
n.style.width = "20rem"; container.style.bottom = "0";
n.style.marginTop = "0.4rem"; container.style.right = "0";
n.style.padding = "0.6rem"; container.style.display = "flex";
n.style.backgroundColor = dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.3)"; container.style.flexDirection = "column";
n.style.borderRadius = "8px"; container.style.gap = "0.4rem";
n.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)"; container.style.padding = "0.4rem";
n.style.color = "white"; container.style.zIndex = "1600";
n.style.fontFamily = "sans-serif"; document.body.appendChild(container);
}
const s = document.createElement("div"); return container;
s.style.display = "flex"; }
s.style.justifyContent = "space-between";
s.style.alignItems = "center"; const createProgressBar = (id, filename) => {
const dark = document.querySelector("html")?.classList?.contains("night") ||
const i = document.createElement("p"); document.querySelector("html")?.classList?.contains("theme-dark");
i.className = "filename"; const container = ensureProgressContainer();
i.style.margin = 0; container.style.zIndex = location.pathname.startsWith("/k/") ? "4" : "1600";
i.style.color = "white";
i.style.fontSize = "0.9rem"; const n = document.createElement("div");
i.style.overflow = "hidden"; n.id = "tel-downloader-progress-" + id;
i.style.textOverflow = "ellipsis"; n.style.width = "20rem";
i.style.whiteSpace = "nowrap"; n.style.marginTop = "0.4rem";
i.innerText = filename || "video"; n.style.padding = "0.6rem";
n.style.backgroundColor = dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.3)";
const d = document.createElement("div"); n.style.borderRadius = "8px";
d.style.cursor = "pointer"; n.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)";
d.style.fontSize = "1.2rem"; n.style.color = "white";
d.style.color = dark ? "#8a8a8a" : "white"; n.style.fontFamily = "sans-serif";
d.innerHTML = "×";
d.onclick = function() { const s = document.createElement("div");
if (n.parentElement) n.parentElement.removeChild(n); s.style.display = "flex";
}; s.style.justifyContent = "space-between";
s.style.alignItems = "center";
const a = document.createElement("div");
a.className = "progress"; const i = document.createElement("p");
a.style.backgroundColor = "#e2e2e2"; i.className = "filename";
a.style.position = "relative"; i.style.margin = 0;
a.style.width = "100%"; i.style.color = "white";
a.style.height = "1.6rem"; i.style.fontSize = "0.9rem";
a.style.borderRadius = "2rem"; i.style.overflow = "hidden";
a.style.overflow = "hidden"; i.style.textOverflow = "ellipsis";
a.style.marginTop = "0.6rem"; i.style.whiteSpace = "nowrap";
i.innerText = filename || "video";
const c = document.createElement("p");
c.style.position = "absolute"; const d = document.createElement("div");
c.style.zIndex = 5; d.style.cursor = "pointer";
c.style.left = "50%"; d.style.fontSize = "1.2rem";
c.style.top = "50%"; d.style.color = dark ? "#8a8a8a" : "white";
c.style.transform = "translate(-50%, -50%)"; d.innerHTML = "×";
c.style.margin = 0; d.onclick = function() {
c.style.color = "black"; if (n.parentElement) n.parentElement.removeChild(n);
c.style.fontSize = "0.85rem"; };
c.innerText = "0%";
const a = document.createElement("div");
const p = document.createElement("div"); a.className = "progress";
p.style.position = "absolute"; a.style.backgroundColor = "#e2e2e2";
p.style.height = "100%"; a.style.position = "relative";
p.style.width = "0%"; a.style.width = "100%";
p.style.backgroundColor = "#3390ec"; a.style.height = "1.6rem";
p.style.left = "0"; a.style.borderRadius = "2rem";
p.style.top = "0"; a.style.overflow = "hidden";
a.style.marginTop = "0.6rem";
a.appendChild(c);
a.appendChild(p); const c = document.createElement("p");
s.appendChild(i); c.style.position = "absolute";
s.appendChild(d); c.style.zIndex = 5;
n.appendChild(s); c.style.left = "50%";
n.appendChild(a); c.style.top = "50%";
container.appendChild(n); c.style.transform = "translate(-50%, -50%)";
}; c.style.margin = 0;
c.style.color = "black";
const updateProgress = (id, filename, percent) => { c.style.fontSize = "0.85rem";
let r = document.getElementById("tel-downloader-progress-" + id); c.innerText = "0%";
if (!r) return;
const p = document.createElement("div");
const fname = filename || r.querySelector("p.filename")?.innerText || "video"; p.style.position = "absolute";
r.querySelector("p.filename").innerText = fname; p.style.height = "100%";
p.style.width = "0%";
const prog = r.querySelector("div.progress"); p.style.backgroundColor = "#3390ec";
if (!prog) return; p.style.left = "0";
p.style.top = "0";
const txt = prog.querySelector("p");
const bar = prog.querySelector("div"); a.appendChild(c);
if (txt) txt.innerText = (percent || 0) + "%"; a.appendChild(p);
if (bar) bar.style.width = (percent || 0) + "%"; s.appendChild(i);
}; s.appendChild(d);
n.appendChild(s);
const completeProgress = (id, clientId, filename, version, locale) => { n.appendChild(a);
let n = document.getElementById("tel-downloader-progress-" + id); container.appendChild(n);
if (!n) return; };
let prog = n.querySelector("div.progress"); const updateProgress = (id, filename, percent) => {
if (prog) { let r = document.getElementById("tel-downloader-progress-" + id);
let txt = prog.querySelector("p"); if (!r) return;
if (txt) txt.innerText = "Completed";
let bar = prog.querySelector("div"); const fname = filename || r.querySelector("p.filename")?.innerText || "video";
if (bar) { r.querySelector("p.filename").innerText = fname;
bar.style.backgroundColor = "#40DCA5";
bar.style.width = "100%"; const prog = r.querySelector("div.progress");
} if (!prog) return;
}
const txt = prog.querySelector("p");
document.dispatchEvent(new CustomEvent('telDownloaderCompleted', { const bar = prog.querySelector("div");
detail: { if (txt) txt.innerText = (percent || 0) + "%";
extensionId: extensionId, if (bar) bar.style.width = (percent || 0) + "%";
action: "videoProgressCompleted", };
videoId: id,
clientId: clientId, const completeProgress = (id, clientId, filename, version, locale) => {
name: filename, let n = document.getElementById("tel-downloader-progress-" + id);
version: version, if (!n) return;
locale: locale
} let prog = n.querySelector("div.progress");
})); if (prog) {
}; let txt = prog.querySelector("p");
if (txt) txt.innerText = "Completed";
const AbortProgress = id => { let bar = prog.querySelector("div");
let t = document.getElementById("tel-downloader-progress-" + id); if (bar) {
if (!t) return; bar.style.backgroundColor = "#40DCA5";
bar.style.width = "100%";
let prog = t.querySelector("div.progress"); }
let txt = prog?.querySelector("p"); }
if (txt) {
txt.innerText = "Download failed. Try again"; document.dispatchEvent(new CustomEvent('telDownloaderCompleted', {
txt.style.fontSize = "12px"; detail: {
} extensionId: extensionId,
let bar = prog?.querySelector("div"); action: "videoProgressCompleted",
if (bar) { videoId: id,
bar.style.backgroundColor = "#D16666"; clientId: clientId,
bar.style.width = "100%"; name: filename,
} version: version,
}; locale: locale
}
// Filename helper }));
function sanitizeFilenamePreserveFriendly(name) { };
if (!name) return 'video';
return name.toString().replace(/[\/\\\?\%\*\:\|\"\<\>]/g, "_").trim(); const AbortProgress = id => {
} let t = document.getElementById("tel-downloader-progress-" + id);
if (!t) return;
// Download-Funktion mit automatischem Dateinamen - OPTIMIERT FÜR GESCHWINDIGKEIT
function tel_download_video(e, t, l, r, o) { let prog = t.querySelector("div.progress");
if (!e) { let txt = prog?.querySelector("p");
console.error('No url provided to tel_download_video'); if (txt) {
return; txt.innerText = "Download failed. Try again";
} txt.style.fontSize = "12px";
}
let baseName = sanitizeFilenamePreserveFriendly(t || 'video'); let bar = prog?.querySelector("div");
const hasExt = /\.\w{1,5}$/.test(baseName); if (bar) {
const id = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString(); bar.style.backgroundColor = "#D16666";
bar.style.width = "100%";
const finishDownload = (blob, finalName) => { }
try { };
const url = URL.createObjectURL(blob);
const link = document.createElement("a"); // Filename helper
document.body.appendChild(link); function sanitizeFilenamePreserveFriendly(name) {
link.href = url; if (!name) return 'video';
link.download = finalName; return name.toString().replace(/[\/\\\?\%\*\:\|\"\<\>]/g, "_").trim();
link.click(); }
document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(url), 2000); // Download-Funktion mit automatischem Dateinamen - OPTIMIERT FÜR GESCHWINDIGKEIT
} catch (err) { function tel_download_video(e, t, l, r, o) {
console.error('Error finishing download', err); if (!e) {
AbortProgress(id); console.error('No url provided to tel_download_video');
} return;
}; }
createProgressBar(id, baseName + (hasExt ? '' : '.mp4')); let baseName = sanitizeFilenamePreserveFriendly(t || 'video');
updateProgress(id, baseName + (hasExt ? '' : '.mp4'), 0); const hasExt = /\.\w{1,5}$/.test(baseName);
const id = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString();
// GESCHWINDIGKEITS-OPTIMIERUNG: Größere Chunks + parallele Downloads
const chunkSize = 10 * 1024 * 1024; // 10MB chunks (statt 1MB) const finishDownload = (blob, finalName) => {
const maxParallel = 3; // 3 parallele Downloads try {
let chunks = []; const url = URL.createObjectURL(blob);
let totalSize = null; const link = document.createElement("a");
let detectedExt = "mp4"; document.body.appendChild(link);
let activeDownloads = 0; link.href = url;
let downloadComplete = false; link.download = finalName;
link.click();
// Zuerst: Hole die Gesamtgröße document.body.removeChild(link);
fetch(e, { setTimeout(() => URL.revokeObjectURL(url), 2000);
method: "HEAD", } catch (err) {
credentials: "include" console.error('Error finishing download', err);
}).then(res => { AbortProgress(id);
const contentLength = res.headers.get("Content-Length"); }
const contentType = res.headers.get("Content-Type") || ""; };
const mime = contentType.split(";")[0] || "";
if (mime.startsWith("video/")) { createProgressBar(id, baseName + (hasExt ? '' : '.mp4'));
detectedExt = mime.split("/")[1] || detectedExt; updateProgress(id, baseName + (hasExt ? '' : '.mp4'), 0);
}
// GESCHWINDIGKEITS-OPTIMIERUNG: Größere Chunks + parallele Downloads
if (contentLength) { const chunkSize = 10 * 1024 * 1024; // 10MB chunks (statt 1MB)
totalSize = parseInt(contentLength, 10); const maxParallel = 3; // 3 parallele Downloads
console.log('Total file size:', (totalSize / 1024 / 1024).toFixed(2), 'MB'); let chunks = [];
let totalSize = null;
// Berechne wie viele Chunks wir brauchen let detectedExt = "mp4";
const numChunks = Math.ceil(totalSize / chunkSize); let activeDownloads = 0;
chunks = new Array(numChunks).fill(null); let downloadComplete = false;
// Starte parallele Downloads // Zuerst: Hole die Gesamtgröße
for (let i = 0; i < Math.min(maxParallel, numChunks); i++) { fetch(e, {
downloadChunk(i); method: "HEAD",
} credentials: "include"
} else { }).then(res => {
// Fallback: sequentieller Download ohne Größeninfo const contentLength = res.headers.get("Content-Length");
sequentialDownload(); const contentType = res.headers.get("Content-Type") || "";
} const mime = contentType.split(";")[0] || "";
}).catch(err => { if (mime.startsWith("video/")) {
console.warn('HEAD request failed, using sequential download'); detectedExt = mime.split("/")[1] || detectedExt;
sequentialDownload(); }
});
if (contentLength) {
function downloadChunk(chunkIndex) { totalSize = parseInt(contentLength, 10);
if (downloadComplete || chunkIndex >= chunks.length) return; console.log('Total file size:', (totalSize / 1024 / 1024).toFixed(2), 'MB');
activeDownloads++; // Berechne wie viele Chunks wir brauchen
const start = chunkIndex * chunkSize; const numChunks = Math.ceil(totalSize / chunkSize);
const end = Math.min(start + chunkSize - 1, totalSize - 1); chunks = new Array(numChunks).fill(null);
fetch(e, { // Starte parallele Downloads
method: "GET", for (let i = 0; i < Math.min(maxParallel, numChunks); i++) {
headers: { Range: `bytes=${start}-${end}` }, downloadChunk(i);
credentials: "include" }
}).then(res => { } else {
if (![200, 206].includes(res.status)) { // Fallback: sequentieller Download ohne Größeninfo
throw Error("Non 200/206 response: " + res.status); sequentialDownload();
} }
return res.blob(); }).catch(err => {
}).then(blob => { console.warn('HEAD request failed, using sequential download');
chunks[chunkIndex] = blob; sequentialDownload();
activeDownloads--; });
// Update Progress function downloadChunk(chunkIndex) {
const downloaded = chunks.filter(c => c !== null).length; if (downloadComplete || chunkIndex >= chunks.length) return;
const percent = Math.round(100 * downloaded / chunks.length);
updateProgress(id, baseName + (hasExt ? "" : "." + detectedExt), percent); activeDownloads++;
const start = chunkIndex * chunkSize;
// Check ob fertig const end = Math.min(start + chunkSize - 1, totalSize - 1);
if (chunks.every(c => c !== null)) {
downloadComplete = true; fetch(e, {
const finalName = hasExt ? baseName : (baseName + "." + detectedExt); method: "GET",
const merged = new Blob(chunks, { type: "video/" + detectedExt }); headers: { Range: `bytes=${start}-${end}` },
finishDownload(merged, finalName); credentials: "include"
completeProgress(id, r, finalName, l, o); }).then(res => {
} else { if (![200, 206].includes(res.status)) {
// Starte nächsten Chunk throw Error("Non 200/206 response: " + res.status);
const nextChunk = chunks.findIndex(c => c === null); }
if (nextChunk !== -1) { return res.blob();
downloadChunk(nextChunk); }).then(blob => {
} chunks[chunkIndex] = blob;
} activeDownloads--;
}).catch(err => {
console.error('Chunk download error:', err); // Update Progress
activeDownloads--; const downloaded = chunks.filter(c => c !== null).length;
// Retry nach kurzer Pause const percent = Math.round(100 * downloaded / chunks.length);
setTimeout(() => downloadChunk(chunkIndex), 500); updateProgress(id, baseName + (hasExt ? "" : "." + detectedExt), percent);
});
} // Check ob fertig
if (chunks.every(c => c !== null)) {
function sequentialDownload() { downloadComplete = true;
// Fallback: Sequentieller Download für Server ohne Range-Support const finalName = hasExt ? baseName : (baseName + "." + detectedExt);
let blobs = [], s = 0; const merged = new Blob(chunks, { type: "video/" + detectedExt });
finishDownload(merged, finalName);
const rangedFetch = () => { completeProgress(id, r, finalName, l, o);
fetch(e, { } else {
method: "GET", // Starte nächsten Chunk
headers: { Range: `bytes=${s}-` }, const nextChunk = chunks.findIndex(c => c === null);
credentials: "include" if (nextChunk !== -1) {
}).then(res2 => { downloadChunk(nextChunk);
if (![200, 206].includes(res2.status)) { }
throw Error("Non 200/206 response: " + res2.status); }
} }).catch(err => {
console.error('Chunk download error:', err);
const contentType2 = res2.headers.get("Content-Type") || ""; activeDownloads--;
const mime2 = contentType2.split(";")[0] || ""; // Retry nach kurzer Pause
if (mime2.startsWith("video/")) { setTimeout(() => downloadChunk(chunkIndex), 500);
detectedExt = mime2.split("/")[1] || detectedExt; });
} }
const cr = res2.headers.get("Content-Range"); function sequentialDownload() {
if (cr) { // Fallback: Sequentieller Download für Server ohne Range-Support
const match = cr.match(/^bytes (\d+)-(\d+)\/(\d+)$/); let blobs = [], s = 0;
if (match) {
const start = parseInt(match[1], 10); const rangedFetch = () => {
const end = parseInt(match[2], 10); fetch(e, {
const total = parseInt(match[3], 10); method: "GET",
headers: { Range: `bytes=${s}-` },
s = end + 1; credentials: "include"
totalSize = total; }).then(res2 => {
if (![200, 206].includes(res2.status)) {
const percent = Math.min(100, Math.round(100 * s / totalSize)); throw Error("Non 200/206 response: " + res2.status);
updateProgress(id, baseName + (hasExt ? "" : "." + detectedExt), percent); }
}
} else if (res2.status === 200) { const contentType2 = res2.headers.get("Content-Type") || "";
const contentLength = res2.headers.get("Content-Length"); const mime2 = contentType2.split(";")[0] || "";
if (contentLength) { if (mime2.startsWith("video/")) {
totalSize = parseInt(contentLength, 10); detectedExt = mime2.split("/")[1] || detectedExt;
} }
}
const cr = res2.headers.get("Content-Range");
return res2.blob(); if (cr) {
}).then(chunkBlob => { const match = cr.match(/^bytes (\d+)-(\d+)\/(\d+)$/);
blobs.push(chunkBlob); if (match) {
const start = parseInt(match[1], 10);
if (totalSize && s >= totalSize) { const end = parseInt(match[2], 10);
const finalName = hasExt ? baseName : (baseName + "." + detectedExt); const total = parseInt(match[3], 10);
const merged = new Blob(blobs, { type: "video/" + detectedExt });
finishDownload(merged, finalName); s = end + 1;
completeProgress(id, r, finalName, l, o); totalSize = total;
} else {
setTimeout(rangedFetch, 10); const percent = Math.min(100, Math.round(100 * s / totalSize));
} updateProgress(id, baseName + (hasExt ? "" : "." + detectedExt), percent);
}).catch(err2 => { }
console.error('Sequential download error', err2); } else if (res2.status === 200) {
AbortProgress(id); const contentLength = res2.headers.get("Content-Length");
}); if (contentLength) {
}; totalSize = parseInt(contentLength, 10);
}
rangedFetch(); }
}
} return res2.blob();
}).then(chunkBlob => {
const tel_download_image = (src, suggestedName) => { blobs.push(chunkBlob);
try {
const safe = sanitizeFilenamePreserveFriendly(suggestedName || 'image') + "_" + Date.now() + ".jpg"; if (totalSize && s >= totalSize) {
const a = document.createElement("a"); const finalName = hasExt ? baseName : (baseName + "." + detectedExt);
document.body.appendChild(a); const merged = new Blob(blobs, { type: "video/" + detectedExt });
a.href = src; finishDownload(merged, finalName);
a.download = safe; completeProgress(id, r, finalName, l, o);
a.click(); } else {
document.body.removeChild(a); setTimeout(rangedFetch, 10);
} catch (err) { }
console.error('tel_download_image error', err); }).catch(err2 => {
} console.error('Sequential download error', err2);
}; AbortProgress(id);
});
window.telDownloader = { };
startVideo: tel_download_video,
startImage: tel_download_image rangedFetch();
}; }
}
const tel_download_image = (src, suggestedName) => {
try {
console.log('=== IMAGE DOWNLOAD START ===');
console.log('Image URL:', src);
console.log('Suggested name:', suggestedName);
const baseName = sanitizeFilenamePreserveFriendly(suggestedName || 'telegram_image');
const hasExt = /\.(jpg|jpeg|png|gif|webp)$/i.test(baseName);
const finalName = hasExt ? baseName : (baseName + '.jpg');
fetch(src, {
method: "GET",
credentials: "include"
}).then(res => {
if (!res.ok) throw new Error("Fetch failed: " + res.status);
// Detect image type
const contentType = res.headers.get("Content-Type") || "";
let ext = "jpg";
if (contentType.includes("png")) ext = "png";
else if (contentType.includes("gif")) ext = "gif";
else if (contentType.includes("webp")) ext = "webp";
const finalNameWithExt = hasExt ? baseName : (baseName + '.' + ext);
return res.blob().then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = finalNameWithExt;
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 2000);
console.log('Image downloaded:', finalNameWithExt);
});
}).catch(err => {
console.error('Image download error:', err);
alert('Bild-Download fehlgeschlagen. Versuche es erneut.');
});
} catch (err) {
console.error('tel_download_image error', err);
}
};
const tel_download_audio = (src, suggestedName) => {
try {
console.log('=== AUDIO DOWNLOAD START ===');
console.log('Audio URL:', src);
console.log('Suggested name:', suggestedName);
const baseName = sanitizeFilenamePreserveFriendly(suggestedName || 'telegram_audio');
const hasExt = /\.(mp3|ogg|wav|m4a|opus)$/i.test(baseName);
const id = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString();
createProgressBar(id, baseName + (hasExt ? '' : '.mp3'));
updateProgress(id, baseName + (hasExt ? '' : '.mp3'), 0);
fetch(src, {
method: "GET",
credentials: "include"
}).then(res => {
if (!res.ok) throw new Error("Fetch failed: " + res.status);
const contentType = res.headers.get("Content-Type") || "";
let ext = "mp3";
if (contentType.includes("ogg")) ext = "ogg";
else if (contentType.includes("wav")) ext = "wav";
else if (contentType.includes("m4a")) ext = "m4a";
else if (contentType.includes("opus")) ext = "opus";
const finalName = hasExt ? baseName : (baseName + '.' + ext);
return res.blob().then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = finalName;
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 2000);
updateProgress(id, finalName, 100);
completeProgress(id, null, finalName, null, null);
console.log('Audio downloaded:', finalName);
});
}).catch(err => {
console.error('Audio download error:', err);
AbortProgress(id);
});
} catch (err) {
console.error('tel_download_audio error', err);
}
};
const tel_download_document = (src, suggestedName) => {
try {
console.log('=== DOCUMENT DOWNLOAD START ===');
console.log('Document URL:', src);
console.log('Suggested name:', suggestedName);
const baseName = sanitizeFilenamePreserveFriendly(suggestedName || 'telegram_file');
const hasExt = /\.\w{2,5}$/i.test(baseName);
const id = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString();
createProgressBar(id, baseName);
updateProgress(id, baseName, 0);
fetch(src, {
method: "GET",
credentials: "include"
}).then(res => {
if (!res.ok) throw new Error("Fetch failed: " + res.status);
const contentType = res.headers.get("Content-Type") || "";
let ext = "";
// Erkenne Extension aus Content-Type
if (contentType.includes("zip")) ext = "zip";
else if (contentType.includes("rar")) ext = "rar";
else if (contentType.includes("pdf")) ext = "pdf";
else if (contentType.includes("msword")) ext = "doc";
else if (contentType.includes("spreadsheet")) ext = "xlsx";
else if (contentType.includes("audio/mpeg")) ext = "mp3";
else if (contentType.includes("audio/")) ext = "mp3";
const finalName = hasExt ? baseName : (baseName + (ext ? '.' + ext : ''));
return res.blob().then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = finalName;
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 2000);
updateProgress(id, finalName, 100);
completeProgress(id, null, finalName, null, null);
console.log('Document downloaded:', finalName);
});
}).catch(err => {
console.error('Document download error:', err);
AbortProgress(id);
});
} catch (err) {
console.error('tel_download_document error', err);
}
};
window.telDownloader = {
startVideo: tel_download_video,
startImage: tel_download_image,
startAudio: tel_download_audio,
startDocument: tel_download_document
};
})(); })();

View File

@@ -1,27 +1,27 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "TG Downloader - Free & Unlimited", "name": "TG Downloader - Free & Unlimited",
"version": "1.4", "version": "1.5",
"description": "Download Videos & Bilder aus Telegram Web.", "description": "Download Videos & Bilder aus Telegram Web.",
"permissions": ["storage", "downloads"], "permissions": ["storage", "downloads"],
"host_permissions": ["https://web.telegram.org/*"], "host_permissions": ["https://web.telegram.org/*"],
"icons": { "icons": {
"128": "icon.png" "128": "icon.png"
}, },
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["https://web.telegram.org/*"], "matches": ["https://web.telegram.org/*"],
"js": ["content_tg.js"], "js": ["content_tg.js"],
"run_at": "document_end" "run_at": "document_end"
} }
], ],
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["inject.js"], "resources": ["inject.js"],
"matches": ["https://web.telegram.org/*"] "matches": ["https://web.telegram.org/*"]
} }
] ]
} }