您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Видео, истории и скачивание файлов и другие функции ↴
// ==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 }); })(); })();