Dateien nach "/" hochladen

This commit is contained in:
2025-12-20 18:03:26 +00:00
parent f4b4c22459
commit fe3b313549
5 changed files with 879 additions and 0 deletions

99
background.js Normal file
View File

@@ -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);
}
});

357
content_tg.js Normal file
View File

@@ -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);

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

396
inject.js Normal file
View File

@@ -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
};
})();

27
manifest.json Normal file
View File

@@ -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/*"]
}
]
}