diff --git a/background.js b/background.js new file mode 100644 index 0000000..446cc13 --- /dev/null +++ b/background.js @@ -0,0 +1,99 @@ +// background.js +// Service worker / background script: startet chrome.downloads.download und sendet Fortschritte an die Content-Tab. + +const downloadsMap = new Map(); // downloadId -> { tabId, filename } + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (!msg || !msg.action) { + sendResponse({ status: "unknown" }); + return; + } + + if (msg.action === "videoProgressCompleted") { + 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 (!url) { + sendResponse({ status: "error", message: "No URL provided" }); + return; + } + + try { + chrome.downloads.download( + { + url: url, + filename: filename, + conflictAction: "uniquify", + saveAs: false + }, + downloadId => { + if (chrome.runtime.lastError) { + console.error('chrome.downloads.download error:', chrome.runtime.lastError.message); + sendResponse({ status: "error", message: chrome.runtime.lastError.message }); + } else { + 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); + sendResponse({ status: "error", message: String(e) }); + return; + } + } + + 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); + if (!info || !info.tabId) return; + + if (delta.bytesReceived) { + chrome.downloads.search({ id: delta.id }, items => { + const item = items && items[0]; + if (!item) return; + + const received = item.bytesReceived || 0; + const total = item.totalBytes || 0; + const percent = total > 0 ? Math.round(100 * received / total) : null; + + chrome.tabs.sendMessage(info.tabId, { + action: 'downloadProgress', + downloadId: delta.id, + bytesReceived: received, + totalBytes: total, + percent + }, () => { /* ignore response */ }); + }); + } + + if (delta.state && delta.state.current === 'complete') { + chrome.tabs.sendMessage(info.tabId, { + action: 'downloadComplete', + downloadId: 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', + downloadId: delta.id, + filename: info.filename, + reason: delta.state && delta.state.current + }, () => { /* ignore response */ }); + downloadsMap.delete(delta.id); + } +}); \ No newline at end of file diff --git a/content_tg.js b/content_tg.js new file mode 100644 index 0000000..890043b --- /dev/null +++ b/content_tg.js @@ -0,0 +1,357 @@ +// 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); \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..9568ac7 Binary files /dev/null and b/icon.png differ diff --git a/inject.js b/inject.js new file mode 100644 index 0000000..1ac612f --- /dev/null +++ b/inject.js @@ -0,0 +1,396 @@ +(function () { + const scriptElement = document.currentScript; + const extensionId = scriptElement ? scriptElement.getAttribute("data-extension-id") : null; + + console.log('inject.js running, extensionId=', extensionId); + + // Empfang von "downloadRequested" (kommt vom content script) + document.addEventListener("downloadRequested", function(e) { + try { + let t = e.detail.url; + let l = e.detail.name; + let r = e.detail.version; + let o = e.detail.clientId; + let n = e.detail.locale; + tel_download_video(t, l, r, o, n); + } catch (err) { + console.error('Error in downloadRequested handler', err); + } + }); + + document.addEventListener("imageDownloadRequested", function(e) { + try { + let t = e.detail.url; + let l = e.detail.name; + tel_download_image(t, l); + } catch (err) { + console.error('Error in imageDownloadRequested handler', err); + } + }); + + // Hilfsfunktion: progress-bar-container + function ensureProgressContainer() { + let container = document.getElementById("tel-downloader-progress-bar-container"); + if (!container) { + container = document.createElement("div"); + container.id = "tel-downloader-progress-bar-container"; + container.style.position = "fixed"; + container.style.bottom = "0"; + container.style.right = "0"; + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = "0.4rem"; + container.style.padding = "0.4rem"; + container.style.zIndex = "1600"; + document.body.appendChild(container); + } + return container; + } + + const createProgressBar = (id, filename) => { + const dark = document.querySelector("html")?.classList?.contains("night") || + document.querySelector("html")?.classList?.contains("theme-dark"); + const container = ensureProgressContainer(); + container.style.zIndex = location.pathname.startsWith("/k/") ? "4" : "1600"; + + const n = document.createElement("div"); + n.id = "tel-downloader-progress-" + id; + n.style.width = "20rem"; + n.style.marginTop = "0.4rem"; + n.style.padding = "0.6rem"; + n.style.backgroundColor = dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.3)"; + n.style.borderRadius = "8px"; + n.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)"; + n.style.color = "white"; + n.style.fontFamily = "sans-serif"; + + const s = document.createElement("div"); + s.style.display = "flex"; + s.style.justifyContent = "space-between"; + s.style.alignItems = "center"; + + const i = document.createElement("p"); + i.className = "filename"; + i.style.margin = 0; + i.style.color = "white"; + i.style.fontSize = "0.9rem"; + i.style.overflow = "hidden"; + i.style.textOverflow = "ellipsis"; + i.style.whiteSpace = "nowrap"; + i.innerText = filename || "video"; + + const d = document.createElement("div"); + d.style.cursor = "pointer"; + d.style.fontSize = "1.2rem"; + d.style.color = dark ? "#8a8a8a" : "white"; + d.innerHTML = "×"; + d.onclick = function() { + if (n.parentElement) n.parentElement.removeChild(n); + }; + + const a = document.createElement("div"); + a.className = "progress"; + a.style.backgroundColor = "#e2e2e2"; + a.style.position = "relative"; + a.style.width = "100%"; + a.style.height = "1.6rem"; + a.style.borderRadius = "2rem"; + a.style.overflow = "hidden"; + a.style.marginTop = "0.6rem"; + + const c = document.createElement("p"); + c.style.position = "absolute"; + c.style.zIndex = 5; + c.style.left = "50%"; + c.style.top = "50%"; + c.style.transform = "translate(-50%, -50%)"; + c.style.margin = 0; + c.style.color = "black"; + c.style.fontSize = "0.85rem"; + c.innerText = "0%"; + + const p = document.createElement("div"); + p.style.position = "absolute"; + p.style.height = "100%"; + p.style.width = "0%"; + p.style.backgroundColor = "#3390ec"; + p.style.left = "0"; + p.style.top = "0"; + + a.appendChild(c); + a.appendChild(p); + s.appendChild(i); + s.appendChild(d); + n.appendChild(s); + n.appendChild(a); + container.appendChild(n); + }; + + const updateProgress = (id, filename, percent) => { + let r = document.getElementById("tel-downloader-progress-" + id); + if (!r) return; + + const fname = filename || r.querySelector("p.filename")?.innerText || "video"; + r.querySelector("p.filename").innerText = fname; + + const prog = r.querySelector("div.progress"); + if (!prog) return; + + const txt = prog.querySelector("p"); + const bar = prog.querySelector("div"); + if (txt) txt.innerText = (percent || 0) + "%"; + if (bar) bar.style.width = (percent || 0) + "%"; + }; + + const completeProgress = (id, clientId, filename, version, locale) => { + let n = document.getElementById("tel-downloader-progress-" + id); + if (!n) return; + + let prog = n.querySelector("div.progress"); + if (prog) { + let txt = prog.querySelector("p"); + if (txt) txt.innerText = "Completed"; + let bar = prog.querySelector("div"); + if (bar) { + bar.style.backgroundColor = "#40DCA5"; + bar.style.width = "100%"; + } + } + + document.dispatchEvent(new CustomEvent('telDownloaderCompleted', { + detail: { + extensionId: extensionId, + action: "videoProgressCompleted", + videoId: id, + clientId: clientId, + name: filename, + version: version, + locale: locale + } + })); + }; + + const AbortProgress = id => { + let t = document.getElementById("tel-downloader-progress-" + id); + if (!t) return; + + let prog = t.querySelector("div.progress"); + let txt = prog?.querySelector("p"); + if (txt) { + txt.innerText = "Download failed. Try again"; + txt.style.fontSize = "12px"; + } + let bar = prog?.querySelector("div"); + if (bar) { + bar.style.backgroundColor = "#D16666"; + bar.style.width = "100%"; + } + }; + + // Filename helper + function sanitizeFilenamePreserveFriendly(name) { + if (!name) return 'video'; + return name.toString().replace(/[\/\\\?\%\*\:\|\"\<\>]/g, "_").trim(); + } + + // Download-Funktion mit automatischem Dateinamen - OPTIMIERT FÜR GESCHWINDIGKEIT + function tel_download_video(e, t, l, r, o) { + if (!e) { + console.error('No url provided to tel_download_video'); + return; + } + + let baseName = sanitizeFilenamePreserveFriendly(t || 'video'); + const hasExt = /\.\w{1,5}$/.test(baseName); + const id = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString(); + + const finishDownload = (blob, finalName) => { + try { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + document.body.appendChild(link); + link.href = url; + link.download = finalName; + link.click(); + document.body.removeChild(link); + setTimeout(() => URL.revokeObjectURL(url), 2000); + } catch (err) { + console.error('Error finishing download', err); + AbortProgress(id); + } + }; + + createProgressBar(id, baseName + (hasExt ? '' : '.mp4')); + updateProgress(id, baseName + (hasExt ? '' : '.mp4'), 0); + + // GESCHWINDIGKEITS-OPTIMIERUNG: Größere Chunks + parallele Downloads + const chunkSize = 10 * 1024 * 1024; // 10MB chunks (statt 1MB) + const maxParallel = 3; // 3 parallele Downloads + let chunks = []; + let totalSize = null; + let detectedExt = "mp4"; + let activeDownloads = 0; + let downloadComplete = false; + + // Zuerst: Hole die Gesamtgröße + fetch(e, { + method: "HEAD", + credentials: "include" + }).then(res => { + const contentLength = res.headers.get("Content-Length"); + const contentType = res.headers.get("Content-Type") || ""; + const mime = contentType.split(";")[0] || ""; + if (mime.startsWith("video/")) { + detectedExt = mime.split("/")[1] || detectedExt; + } + + if (contentLength) { + totalSize = parseInt(contentLength, 10); + console.log('Total file size:', (totalSize / 1024 / 1024).toFixed(2), 'MB'); + + // Berechne wie viele Chunks wir brauchen + const numChunks = Math.ceil(totalSize / chunkSize); + chunks = new Array(numChunks).fill(null); + + // Starte parallele Downloads + for (let i = 0; i < Math.min(maxParallel, numChunks); i++) { + downloadChunk(i); + } + } else { + // Fallback: sequentieller Download ohne Größeninfo + sequentialDownload(); + } + }).catch(err => { + console.warn('HEAD request failed, using sequential download'); + sequentialDownload(); + }); + + function downloadChunk(chunkIndex) { + if (downloadComplete || chunkIndex >= chunks.length) return; + + activeDownloads++; + const start = chunkIndex * chunkSize; + const end = Math.min(start + chunkSize - 1, totalSize - 1); + + fetch(e, { + method: "GET", + headers: { Range: `bytes=${start}-${end}` }, + credentials: "include" + }).then(res => { + if (![200, 206].includes(res.status)) { + throw Error("Non 200/206 response: " + res.status); + } + return res.blob(); + }).then(blob => { + chunks[chunkIndex] = blob; + activeDownloads--; + + // Update Progress + const downloaded = chunks.filter(c => c !== null).length; + const percent = Math.round(100 * downloaded / chunks.length); + updateProgress(id, baseName + (hasExt ? "" : "." + detectedExt), percent); + + // Check ob fertig + if (chunks.every(c => c !== null)) { + downloadComplete = true; + const finalName = hasExt ? baseName : (baseName + "." + detectedExt); + const merged = new Blob(chunks, { type: "video/" + detectedExt }); + finishDownload(merged, finalName); + completeProgress(id, r, finalName, l, o); + } else { + // Starte nächsten Chunk + const nextChunk = chunks.findIndex(c => c === null); + if (nextChunk !== -1) { + downloadChunk(nextChunk); + } + } + }).catch(err => { + console.error('Chunk download error:', err); + activeDownloads--; + // Retry nach kurzer Pause + setTimeout(() => downloadChunk(chunkIndex), 500); + }); + } + + function sequentialDownload() { + // Fallback: Sequentieller Download für Server ohne Range-Support + let blobs = [], s = 0; + + const rangedFetch = () => { + fetch(e, { + method: "GET", + headers: { Range: `bytes=${s}-` }, + credentials: "include" + }).then(res2 => { + if (![200, 206].includes(res2.status)) { + throw Error("Non 200/206 response: " + res2.status); + } + + const contentType2 = res2.headers.get("Content-Type") || ""; + const mime2 = contentType2.split(";")[0] || ""; + if (mime2.startsWith("video/")) { + detectedExt = mime2.split("/")[1] || detectedExt; + } + + const cr = res2.headers.get("Content-Range"); + if (cr) { + const match = cr.match(/^bytes (\d+)-(\d+)\/(\d+)$/); + if (match) { + const start = parseInt(match[1], 10); + const end = parseInt(match[2], 10); + const total = parseInt(match[3], 10); + + s = end + 1; + totalSize = total; + + const percent = Math.min(100, Math.round(100 * s / totalSize)); + updateProgress(id, baseName + (hasExt ? "" : "." + detectedExt), percent); + } + } else if (res2.status === 200) { + const contentLength = res2.headers.get("Content-Length"); + if (contentLength) { + totalSize = parseInt(contentLength, 10); + } + } + + return res2.blob(); + }).then(chunkBlob => { + blobs.push(chunkBlob); + + if (totalSize && s >= totalSize) { + const finalName = hasExt ? baseName : (baseName + "." + detectedExt); + const merged = new Blob(blobs, { type: "video/" + detectedExt }); + finishDownload(merged, finalName); + completeProgress(id, r, finalName, l, o); + } else { + setTimeout(rangedFetch, 10); + } + }).catch(err2 => { + console.error('Sequential download error', err2); + AbortProgress(id); + }); + }; + + rangedFetch(); + } + } + + const tel_download_image = (src, suggestedName) => { + try { + const safe = sanitizeFilenamePreserveFriendly(suggestedName || 'image') + "_" + Date.now() + ".jpg"; + const a = document.createElement("a"); + document.body.appendChild(a); + a.href = src; + a.download = safe; + a.click(); + document.body.removeChild(a); + } catch (err) { + console.error('tel_download_image error', err); + } + }; + + window.telDownloader = { + startVideo: tel_download_video, + startImage: tel_download_image + }; +})(); \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ce3fa7c --- /dev/null +++ b/manifest.json @@ -0,0 +1,27 @@ +{ + "manifest_version": 3, + "name": "TG Downloader - Free & Unlimited", + "version": "1.4", + "description": "Download Videos & Bilder aus Telegram Web.", + "permissions": ["storage", "downloads"], + "host_permissions": ["https://web.telegram.org/*"], + "icons": { + "128": "icon.png" + }, + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": ["https://web.telegram.org/*"], + "js": ["content_tg.js"], + "run_at": "document_end" + } + ], + "web_accessible_resources": [ + { + "resources": ["inject.js"], + "matches": ["https://web.telegram.org/*"] + } + ] +} \ No newline at end of file