// ==UserScript==
// @name Telegram +
// @name:en Telegram +
// @namespace by
// @version 1.3
// @author diorhc
// @description Видео, истории и скачивание файлов и другие функции ↴
// @description:en Telegram Downloader and others features ↴
// @match https://web.telegram.org/*
// @match https://webk.telegram.org/*
// @match https://webz.telegram.org/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=telegram.org
// @license MIT
// @grant none
// ==/UserScript==
(() => {
// --- Logger Utility ---
const logger = {
info: (msg, file = "") =>
console.log(`[Tel Download]${file ? ` ${file}:` : ""} ${msg}`),
error: (msg, file = "") =>
console.error(`[Tel Download]${file ? ` ${file}:` : ""} ${msg}`),
};
// --- Constants ---
const DOWNLOAD_ICON = "\uE95A";
const FORWARD_ICON = "\uE976";
const contentRangeRegex = /^bytes (\d+)-(\d+)\/(\d+)$/;
const REFRESH_DELAY = 500;
// --- Utility Functions ---
const hashCode = (s) =>
Array.from(s).reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0) >>> 0;
// --- Progress Bar ---
function createProgressBar(videoId, fileName) {
const isDark =
document.documentElement.classList.contains("night") ||
document.documentElement.classList.contains("theme-dark");
const container = document.getElementById("tel-downloader-progress-bar-container");
const inner = document.createElement("div");
inner.id = `tel-downloader-progress-${videoId}`;
Object.assign(inner.style, {
width: "20rem",
marginTop: "0.4rem",
padding: "0.6rem",
backgroundColor: isDark ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.6)",
});
const flex = document.createElement("div");
Object.assign(flex.style, {
display: "flex",
justifyContent: "space-between",
});
const title = document.createElement("p");
title.className = "filename";
title.style.margin = 0;
title.style.color = "white";
title.innerText = fileName;
const close = document.createElement("div");
Object.assign(close.style, {
cursor: "pointer",
fontSize: "1.2rem",
color: isDark ? "#8a8a8a" : "white",
});
close.innerHTML = "×";
close.onclick = () => container.removeChild(inner);
const progressBar = document.createElement("div");
progressBar.className = "progress";
Object.assign(progressBar.style, {
backgroundColor: "#e2e2e2",
position: "relative",
width: "100%",
height: "1.6rem",
borderRadius: "2rem",
overflow: "hidden",
});
const counter = document.createElement("p");
Object.assign(counter.style, {
position: "absolute",
zIndex: 5,
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
margin: 0,
color: "black",
});
const progress = document.createElement("div");
Object.assign(progress.style, {
position: "absolute",
height: "100%",
width: "0%",
backgroundColor: "#6093B5",
});
progressBar.append(counter, progress);
flex.append(title, close);
inner.append(flex, progressBar);
container.appendChild(inner);
}
function updateProgress(videoId, fileName, percent) {
const inner = document.getElementById(`tel-downloader-progress-${videoId}`);
if (!inner) return;
inner.querySelector("p.filename").innerText = fileName;
const progressBar = inner.querySelector("div.progress");
progressBar.querySelector("p").innerText = percent + "%";
progressBar.querySelector("div").style.width = percent + "%";
}
function completeProgress(videoId) {
const progressBar = document
.getElementById(`tel-downloader-progress-${videoId}`)
.querySelector("div.progress");
progressBar.querySelector("p").innerText = "Completed";
progressBar.querySelector("div").style.backgroundColor = "#B6C649";
progressBar.querySelector("div").style.width = "100%";
}
function abortProgress(videoId) {
const progressBar = document
.getElementById(`tel-downloader-progress-${videoId}`)
.querySelector("div.progress");
progressBar.querySelector("p").innerText = "Aborted";
progressBar.querySelector("div").style.backgroundColor = "#D16666";
progressBar.querySelector("div").style.width = "100%";
}
// --- Downloaders ---
function tel_download_video(url) {
let blobs = [],
nextOffset = 0,
totalSize = null,
fileExt = "mp4";
const videoId = `${Math.random().toString(36).slice(2, 10)}_${Date.now()}`;
let fileName = hashCode(url).toString(36) + "." + fileExt;
// Try to extract fileName from metadata
try {
const meta = JSON.parse(decodeURIComponent(url.split("/").pop()));
if (meta.fileName) fileName = meta.fileName;
} catch {}
logger.info(`URL: ${url}`, fileName);
function fetchNextPart(writable) {
fetch(url, {
method: "GET",
headers: { Range: `bytes=${nextOffset}-` },
})
.then((res) => {
if (![200, 206].includes(res.status))
throw new Error("Non 200/206 response: " + res.status);
const mime = res.headers.get("Content-Type").split(";")[0];
if (!mime.startsWith("video/"))
throw new Error("Non-video MIME: " + mime);
fileExt = mime.split("/")[1];
fileName = fileName.replace(/\.\w+$/, "." + fileExt);
const match = res.headers.get("Content-Range").match(contentRangeRegex);
const start = +match[1],
end = +match[2],
size = +match[3];
if (start !== nextOffset) throw "Gap detected between responses.";
if (totalSize && size !== totalSize) throw "Total size differs";
nextOffset = end + 1;
totalSize = size;
updateProgress(
videoId,
fileName,
((nextOffset * 100) / totalSize).toFixed(0)
);
return res.blob();
})
.then((blob) => {
if (writable) return writable.write(blob);
blobs.push(blob);
})
.then(() => {
if (!totalSize) throw new Error("_total_size is NULL");
if (nextOffset < totalSize) fetchNextPart(writable);
else {
if (writable) writable.close().then(() => logger.info("Download finished", fileName));
else save();
completeProgress(videoId);
}
})
.catch((err) => {
logger.error(err, fileName);
abortProgress(videoId);
});
}
function save() {
logger.info("Finish downloading blobs", fileName);
const blob = new Blob(blobs, { type: "video/mp4" });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.href = blobUrl;
a.download = fileName;
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
logger.info("Download triggered", fileName);
}
const supportsFS =
"showSaveFilePicker" in unsafeWindow &&
(() => {
try {
return unsafeWindow.self === unsafeWindow.top;
} catch {
return false;
}
})();
if (supportsFS) {
unsafeWindow
.showSaveFilePicker({ suggestedName: fileName })
.then((handle) =>
handle.createWritable().then((writable) => {
fetchNextPart(writable);
createProgressBar(videoId, fileName);
})
)
.catch((err) => {
if (err.name !== "AbortError") logger.error(err.message, fileName);
});
} else {
fetchNextPart(null);
createProgressBar(videoId, fileName);
}
}
function tel_download_audio(url) {
let blobs = [],
nextOffset = 0,
totalSize = null;
const fileName = hashCode(url).toString(36) + ".ogg";
function fetchNextPart(writable) {
fetch(url, {
method: "GET",
headers: { Range: `bytes=${nextOffset}-` },
})
.then((res) => {
if (![200, 206].includes(res.status))
throw new Error("Non 200/206 response: " + res.status);
const mime = res.headers.get("Content-Type").split(";")[0];
if (!mime.startsWith("audio/"))
throw new Error("Non-audio MIME: " + mime);
const match = res.headers.get("Content-Range").match(contentRangeRegex);
const start = +match[1],
end = +match[2],
size = +match[3];
if (start !== nextOffset) throw "Gap detected between responses.";
if (totalSize && size !== totalSize) throw "Total size differs";
nextOffset = end + 1;
totalSize = size;
return res.blob();
})
.then((blob) => {
if (writable) return writable.write(blob);
blobs.push(blob);
})
.then(() => {
if (nextOffset < totalSize) fetchNextPart(writable);
else {
if (writable) writable.close().then(() => logger.info("Download finished", fileName));
else save();
}
})
.catch((err) => logger.error(err, fileName));
}
function save() {
logger.info("Finish downloading blobs", fileName);
const blob = new Blob(blobs, { type: "audio/ogg" });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.href = blobUrl;
a.download = fileName;
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
logger.info("Download triggered", fileName);
}
const supportsFS =
"showSaveFilePicker" in unsafeWindow &&
(() => {
try {
return unsafeWindow.self === unsafeWindow.top;
} catch {
return false;
}
})();
if (supportsFS) {
unsafeWindow
.showSaveFilePicker({ suggestedName: fileName })
.then((handle) =>
handle.createWritable().then((writable) => fetchNextPart(writable))
)
.catch((err) => {
if (err.name !== "AbortError") logger.error(err.message, fileName);
});
} else {
fetchNextPart(null);
}
}
function tel_download_image(imageUrl) {
const fileName = `${Math.random().toString(36).slice(2, 10)}.jpeg`;
const a = document.createElement("a");
document.body.appendChild(a);
a.href = imageUrl;
a.download = fileName;
a.click();
document.body.removeChild(a);
logger.info("Download triggered", fileName);
}
// --- Progress Bar Container Setup ---
(() => {
const body = document.body;
const container = document.createElement("div");
container.id = "tel-downloader-progress-bar-container";
Object.assign(container.style, {
position: "fixed",
bottom: 0,
right: 0,
zIndex: location.pathname.startsWith("/k/") ? 4 : 1600,
});
body.appendChild(container);
})();
logger.info("Initialized");
// --- Main Interval: UI Button Injection ---
setInterval(() => {
// Voice/Circle Audio Download Button
const pinnedAudio = document.body.querySelector(".pinned-audio");
let dataMid;
let downloadBtn =
document.body.querySelector("._tel_download_button_pinned_container") ||
document.createElement("button");
if (pinnedAudio) {
dataMid = pinnedAudio.getAttribute("data-mid");
downloadBtn.className =
"btn-icon tgico-download _tel_download_button_pinned_container";
downloadBtn.innerHTML = `<span class="tgico button-icon">${DOWNLOAD_ICON}</span>`;
}
const audioElements = document.body.querySelectorAll("audio-element");
audioElements.forEach((audioElement) => {
const bubble = audioElement.closest(".bubble");
if (!bubble || bubble.querySelector("._tel_download_button_pinned_container")) return;
if (
dataMid &&
downloadBtn.getAttribute("data-mid") !== dataMid &&
audioElement.getAttribute("data-mid") === dataMid
) {
const link = audioElement.audio && audioElement.audio.getAttribute("src");
const isAudio = audioElement.audio && audioElement.audio instanceof HTMLAudioElement;
downloadBtn.onclick = (e) => {
e.stopPropagation();
if (isAudio) tel_download_audio(link);
else tel_download_video(link);
};
downloadBtn.setAttribute("data-mid", dataMid);
if (link) {
pinnedAudio
.querySelector(".pinned-container-wrapper-utils")
.appendChild(downloadBtn);
}
}
});
// Stories Download Button
const storiesContainer = document.getElementById("stories-viewer");
if (storiesContainer) {
const createDownloadButton = () => {
const btn = document.createElement("button");
btn.className = "btn-icon rp tel-download";
btn.innerHTML = `<span class="tgico">${DOWNLOAD_ICON}</span><div class="c-ripple"></div>`;
btn.type = "button";
btn.title = "Download";
btn.onclick = () => {
const video = storiesContainer.querySelector("video.media-video");
const videoSrc = video?.src || video?.currentSrc || video?.querySelector("source")?.src;
if (videoSrc) tel_download_video(videoSrc);
else {
const imageSrc = storiesContainer.querySelector("img.media-photo")?.src;
if (imageSrc) tel_download_image(imageSrc);
}
};
return btn;
};
const storyHeader = storiesContainer.querySelector("[class^='_ViewerStoryHeaderRight']");
if (storyHeader && !storyHeader.querySelector(".tel-download")) {
storyHeader.prepend(createDownloadButton());
}
}
// Media Viewer Download Buttons
const mediaContainer = document.querySelector(".media-viewer-whole");
if (!mediaContainer) return;
const mediaAspecter = mediaContainer.querySelector(
".media-viewer-movers .media-viewer-aspecter"
);
const mediaButtons = mediaContainer.querySelector(
".media-viewer-topbar .media-viewer-buttons"
);
if (!mediaAspecter || !mediaButtons) return;
// Unhide hidden buttons and use official download if present
const hiddenButtons = mediaButtons.querySelectorAll("button.btn-icon.hide");
let onDownload = null;
for (const btn of hiddenButtons) {
btn.classList.remove("hide");
if (btn.textContent === FORWARD_ICON) btn.classList.add("tgico-forward");
if (btn.textContent === DOWNLOAD_ICON) {
btn.classList.add("tgico-download");
onDownload = () => btn.click();
}
}
// Video player
if (mediaAspecter.querySelector(".ckin__player")) {
const controls = mediaAspecter.querySelector(".default__controls.ckin__controls");
if (controls && !controls.querySelector(".tel-download")) {
const brControls = controls.querySelector(".bottom-controls .right-controls");
const btn = document.createElement("button");
btn.className = "btn-icon default__button tgico-download tel-download";
btn.innerHTML = `<span class="tgico">${DOWNLOAD_ICON}</span>`;
btn.type = "button";
btn.title = "Download";
btn.ariaLabel = "Download";
btn.onclick = onDownload
? onDownload
: () => tel_download_video(mediaAspecter.querySelector("video").src);
brControls.prepend(btn);
}
} else if (
mediaAspecter.querySelector("video") &&
!mediaButtons.querySelector("button.btn-icon.tgico-download")
) {
// Video HTML element
const btn = document.createElement("button");
btn.className = "btn-icon tgico-download tel-download";
btn.innerHTML = `<span class="tgico button-icon">${DOWNLOAD_ICON}</span>`;
btn.type = "button";
btn.ariaLabel = "Download";
btn.onclick = onDownload
? onDownload
: () => tel_download_video(mediaAspecter.querySelector("video").src);
mediaButtons.prepend(btn);
} else if (!mediaButtons.querySelector("button.btn-icon.tgico-download")) {
// Image
const img = mediaAspecter.querySelector("img.thumbnail");
if (!img || !img.src) return;
const btn = document.createElement("button");
btn.className = "btn-icon tgico-download tel-download";
btn.innerHTML = `<span class="tgico button-icon">${DOWNLOAD_ICON}</span>`;
btn.type = "button";
btn.title = "Download";
btn.ariaLabel = "Download";
btn.onclick = onDownload
? onDownload
: () => tel_download_image(img.src);
mediaButtons.prepend(btn);
}
}, REFRESH_DELAY);
logger.info("Completed script setup.");
// --- Media Player Keyboard Controls ---
document.addEventListener("keydown", (e) => {
const mediaViewer = document.querySelector(".media-viewer-whole");
if (!mediaViewer) return;
const video = mediaViewer.querySelector("video");
if (!video) return;
if (
["INPUT", "TEXTAREA"].includes(e.target.tagName) ||
e.target.isContentEditable
)
return;
// Notification
let notification = document.querySelector(".video-control-notification");
if (!notification) {
notification = document.createElement("div");
notification.className = "video-control-notification";
Object.assign(notification.style, {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
backgroundColor: "rgba(0, 0, 0, 0.7)",
color: "white",
padding: "10px 20px",
borderRadius: "5px",
fontSize: "18px",
opacity: "0",
transition: "opacity 0.3s ease",
zIndex: "10000",
pointerEvents: "none",
});
document.body.appendChild(notification);
}
let fadeTimeout;
const showNotification = (msg) => {
notification.innerHTML = msg;
notification.style.opacity = "1";
notification.classList.add("notification-pulse");
if (fadeTimeout) cancelAnimationFrame(fadeTimeout);
let start;
function fade(ts) {
if (!start) start = ts;
if (ts - start > 1500) {
notification.style.opacity = "0";
notification.classList.remove("notification-pulse");
} else {
fadeTimeout = requestAnimationFrame(fade);
}
}
fadeTimeout = requestAnimationFrame(fade);
};
// Add styles if not present
if (!document.getElementById("video-control-animations")) {
const style = document.createElement("style");
style.id = "video-control-animations";
style.textContent = `
@keyframes notification-pulse {
0% { transform: translate(-50%, -50%) scale(0.95); }
50% { transform: translate(-50%, -50%) scale(1.05); }
100% { transform: translate(-50%, -50%) scale(1); }
}
.notification-pulse {
animation: notification-pulse 0.3s ease-in-out;
}
.video-control-notification {
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
}
`;
document.head.appendChild(style);
}
if (!document.getElementById("video-control-glassmorphism")) {
const style = document.createElement("style");
style.id = "video-control-glassmorphism";
style.textContent = `
.video-control-notification {
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
background: rgba(32, 38, 57, 0.55);
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
color: #fff;
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
font-size: 1.1em;
letter-spacing: 0.01em;
transition: opacity 0.3s, background 0.3s;
padding: 18px 32px;
min-width: 120px;
max-width: 90vw;
text-align: center;
user-select: none;
}
`;
document.head.appendChild(style);
}
// Keyboard Shortcuts
switch (e.code) {
case "ArrowRight":
e.preventDefault();
video.currentTime = Math.min(video.duration, video.currentTime + 5);
showNotification(`<span style="opacity:0.7;">(${Math.floor(video.currentTime)}s)</span>`);
break;
case "ArrowLeft":
e.preventDefault();
video.currentTime = Math.max(0, video.currentTime - 5);
showNotification(`<span style="opacity:0.7;">(${Math.floor(video.currentTime)}s)</span>`);
break;
case "ArrowUp":
e.preventDefault();
video.volume = Math.min(1, video.volume + 0.1);
showNotification(`<b>${Math.round(video.volume * 100)}%</b>`);
break;
case "ArrowDown":
e.preventDefault();
video.volume = Math.max(0, video.volume - 0.1);
showNotification(`<b>${Math.round(video.volume * 100)}%</b>`);
break;
case "KeyM":
e.preventDefault();
video.muted = !video.muted;
showNotification(video.muted ? "Muted" : "Unmuted");
break;
case "KeyP":
e.preventDefault();
if (document.pictureInPictureElement) {
document.exitPictureInPicture().catch((err) => logger.error(err.message));
showNotification("Exited PiP");
} else {
video.requestPictureInPicture().catch((err) => logger.error(err.message));
showNotification("Entered PiP");
}
break;
case "Home":
e.preventDefault();
video.currentTime = 0;
showNotification(`<span style="opacity:0.7;">(0s)</span>`);
break;
default:
return;
}
// Video Progress Persistence
(function () {
const STORAGE_KEY = "tg_video_progress";
const load = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
const save = (obj) => localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
let intervalId = null;
const observer = new MutationObserver(() => {
const nameEl = document.querySelector(".media-viewer-name .peer-title");
const dateEl = document.querySelector(".media-viewer-date");
const video = document.querySelector("video");
if (!nameEl || !dateEl || !video) return;
const name = nameEl.textContent.trim();
const date = dateEl.textContent.trim();
const key = `${name} @ ${date}`;
const store = load();
if (store[key] && !video.dataset.restored) {
video.currentTime = store[key];
video.dataset.restored = "1";
}
if (!video.dataset.listened) {
video.dataset.listened = "1";
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
if (!video.paused && !video.ended) {
store[key] = video.currentTime;
save(store);
}
}, 2000);
video.addEventListener(
"ended",
() => {
delete store[key];
save(store);
},
{ once: true }
);
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
e.stopPropagation();
});
(function removeTelegramSpeedLimit() {
// Patch fetch to bypass artificial speed limits on media downloads
const originalFetch = window.fetch;
window.fetch = function (...args) {
return originalFetch.apply(this, args).then(async (res) => {
const contentType = res.headers.get("Content-Type") || "";
// Only patch for media and binary files
if (
/^video\//.test(contentType) ||
/^audio\//.test(contentType) ||
contentType === "application/octet-stream"
) {
// Read the full body eagerly to avoid slow streams
const blob = await res.clone().blob();
// Copy headers to a new Headers object to avoid issues with immutable headers
const headers = new Headers();
res.headers.forEach((v, k) => headers.append(k, v));
return new Response(blob, {
status: res.status,
statusText: res.statusText,
headers,
});
}
return res;
});
};
})();
(function removeTelegramAds() {
// Remove sponsored messages and ad banners
const adSelectors = [
'[class*="Sponsored"]',
'[class*="sponsored"]',
'[class*="AdBanner"]',
'[class*="ad-banner"]',
'[data-testid="sponsored-message"]',
'[data-testid="ad-banner"]'
];
function removeAds(root = document) {
adSelectors.forEach(selector => {
root.querySelectorAll(selector).forEach(el => {
el.remove();
});
});
}
// Initial cleanup
removeAds();
// Observe DOM for dynamically inserted ads
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
removeAds(node);
}
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
})();