Files
TG-Video-Downloader/inject.js
2025-12-20 18:03:26 +00:00

396 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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
};
})();