Telegram Media Downloader

Used to download images, GIFs, videos and voice messages on Telegram webapp even from channels restricting downloading and saving content

目前為 2023-08-23 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Telegram Media Downloader
  3. // @name:zh-CN Telegram下载器
  4. // @version 1.031
  5. // @namespace https://github.com/Neet-Nestor/Telegram-Media-Downloader
  6. // @description Used to download images, GIFs, videos and voice messages on Telegram webapp even from channels restricting downloading and saving content
  7. // @description:zh-cn 从禁止下载的Telegram频道中下载图片、视频及语音消息
  8. // @author Nestor Qin
  9. // @license GNU GPLv3
  10. // @website https://github.com/Neet-Nestor/Telegram-Media-Downloader
  11. // @match https://web.telegram.org/*
  12. // @match https://webk.telegram.org/*
  13. // @match https://webz.telegram.org/*
  14. // @icon https://img.icons8.com/color/452/telegram-app--v5.png
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. const logger = {
  19. info: (message) => {
  20. console.log("[Tel Download] " + message);
  21. },
  22. error: (message) => {
  23. console.error("[Tel Download] " + message);
  24. },
  25. };
  26. const contentRangeRegex = /^bytes (\d+)-(\d+)\/(\d+)$/;
  27. const REFRESH_DELAY = 500;
  28.  
  29. const tel_download_video = (url) => {
  30. let _blobs = [];
  31. let _next_offset = 0;
  32. let _total_size = null;
  33. let _file_extension = "mp4";
  34.  
  35. const fetchNextPart = () => {
  36. fetch(url, {
  37. method: "GET",
  38. headers: {
  39. Range: `bytes=${_next_offset}-`,
  40. },
  41. })
  42. .then((res) => {
  43. logger.info("get response ", res);
  44. if (![200, 206].includes(res.status)) {
  45. logger.error("Non 200/206 response was received: " + res.status);
  46. return;
  47. }
  48.  
  49. const mime = res.headers.get("Content-Type").split(";")[0];
  50. if (!mime.startsWith("video/")) {
  51. logger.error("Get non video response with MIME type " + mime);
  52. throw "Get non video response with MIME type " + mime;
  53. }
  54. _file_extension = mime.split("/")[1];
  55.  
  56. const match = res.headers
  57. .get("Content-Range")
  58. .match(contentRangeRegex);
  59.  
  60. const startOffset = parseInt(match[1]);
  61. const endOffset = parseInt(match[2]);
  62. const totalSize = parseInt(match[3]);
  63.  
  64. if (startOffset !== _next_offset) {
  65. logger.error("Gap detected between responses.");
  66. logger.info("Last offset: " + _next_offset);
  67. logger.info("New start offset " + match[1]);
  68. throw "Gap detected between responses.";
  69. }
  70. if (_total_size && totalSize !== _total_size) {
  71. logger.error("Total size differs");
  72. throw "Total size differs";
  73. }
  74.  
  75. _next_offset = endOffset + 1;
  76. _total_size = totalSize;
  77.  
  78. logger.info(
  79. `Get response: ${res.headers.get(
  80. "Content-Length"
  81. )} bytes data from ${res.headers.get("Content-Range")}`
  82. );
  83. logger.info(
  84. `Progress: ${((_next_offset * 100) / _total_size).toFixed(0)}%`
  85. );
  86. return res.blob();
  87. })
  88. .then((resBlob) => {
  89. _blobs.push(resBlob);
  90. })
  91. .then(() => {
  92. if (_next_offset < _total_size) {
  93. fetchNextPart();
  94. } else {
  95. save();
  96. }
  97. })
  98. .catch((reason) => {
  99. logger.error(reason);
  100. });
  101. };
  102.  
  103. const save = () => {
  104. logger.info("Finish downloading blobs");
  105. logger.info("Concatenating blobs and downloading...");
  106.  
  107. let fileName =
  108. (Math.random() + 1).toString(36).substring(2, 10) +
  109. "." +
  110. _file_extension;
  111.  
  112. // Some video src is in format:
  113. // 'stream/{"dcId":5,"location":{...},"size":...,"mimeType":"video/mp4","fileName":"xxxx.MP4"}'
  114. try {
  115. const metadata = JSON.parse(
  116. decodeURIComponent(url.split("/")[url.split("/").length - 1])
  117. );
  118. logger.info(metadata);
  119. if (metadata.fileName) {
  120. fileName = metadata.fileName;
  121. }
  122. } catch (e) {
  123. // Invalid JSON string, pass extracting filename
  124. }
  125.  
  126. const blob = new Blob(_blobs, { type: "video/mp4" });
  127. const blobUrl = window.URL.createObjectURL(blob);
  128.  
  129. logger.info("Final blob size: " + blob.size + " bytes");
  130.  
  131. const a = document.createElement("a");
  132. document.body.appendChild(a);
  133. a.href = blobUrl;
  134. a.download = fileName;
  135. a.click();
  136. document.body.removeChild(a);
  137. window.URL.revokeObjectURL(blobUrl);
  138.  
  139. logger.info("Download triggered");
  140. };
  141.  
  142. fetchNextPart();
  143. };
  144.  
  145. const tel_download_audio = (url, _chatName, _sender) => {
  146. let _blobs = [];
  147. let _file_extension = ".ogg";
  148.  
  149. const fetchNextPart = () => {
  150. fetch(url, { method: "GET" })
  151. .then((res) => {
  152. if (res.status !== 206 && res.status !== 200) {
  153. console.error("Non 200/206 response was received: " + res.status);
  154. return;
  155. }
  156.  
  157. const mime = res.headers.get("Content-Type").split(";")[0];
  158. if (!mime.startsWith("audio/")) {
  159. console.error("Get non audio response with MIME type " + mime);
  160. throw "Get non audio response with MIME type " + mime;
  161. }
  162. return res.blob();
  163. })
  164. .then((resBlob) => {
  165. _blobs.push(resBlob);
  166. })
  167. .then(() => {
  168. save();
  169. })
  170. .catch((reason) => {
  171. console.error(reason);
  172. });
  173. };
  174.  
  175. const save = () => {
  176. console.info(
  177. "Finish downloading blobs. Concatenating blobs and downloading..."
  178. );
  179. const fileName =
  180. (Math.random() + 1).toString(36).substring(2, 10) + _file_extension;
  181.  
  182. let blob = new Blob(_blobs, { type: "audio/ogg" });
  183. const blobUrl = window.URL.createObjectURL(blob);
  184.  
  185. console.info("Final blob size in bytes: " + blob.size);
  186. blob = 0;
  187.  
  188. const a = document.createElement("a");
  189. document.body.appendChild(a);
  190. a.href = blobUrl;
  191. a.download = fileName;
  192. a.click();
  193. document.body.removeChild(a);
  194. window.URL.revokeObjectURL(blobUrl);
  195.  
  196. console.info("Download triggered");
  197. };
  198.  
  199. fetchNextPart();
  200. };
  201.  
  202. const tel_download_image = (imageUrl) => {
  203. const fileName =
  204. (Math.random() + 1).toString(36).substring(2, 10) + ".jpeg"; // assume jpeg
  205.  
  206. const a = document.createElement("a");
  207. document.body.appendChild(a);
  208. a.href = imageUrl;
  209. a.download = fileName;
  210. a.click();
  211. document.body.removeChild(a);
  212.  
  213. logger.info("Download triggered");
  214. };
  215.  
  216. logger.info("Initialized");
  217.  
  218. // For webz /a/ webapp
  219. setInterval(() => {
  220. // All media opened are located in .media-viewer-movers > .media-viewer-aspecter
  221. const mediaContainer = document.querySelector(
  222. "#MediaViewer .MediaViewerSlide--active"
  223. );
  224. if (!mediaContainer) return;
  225. const mediaViewerActions = document.querySelector(
  226. "#MediaViewer .MediaViewerActions"
  227. );
  228. if (!mediaViewerActions) return;
  229.  
  230. const videoPlayer = mediaContainer.querySelector(
  231. ".MediaViewerContent > .VideoPlayer"
  232. );
  233. const img = mediaContainer.querySelector(".MediaViewerContent > div > img");
  234. // 1. Video player detected - Video or GIF
  235. // container > .MediaViewerSlides > .MediaViewerSlide > .MediaViewerContent > .VideoPlayer > video[src]
  236. if (videoPlayer) {
  237. const videoUrl = videoPlayer.querySelector("video").currentSrc;
  238. const downloadIcon = document.createElement("i");
  239. downloadIcon.className = "icon icon-download";
  240. const downloadButton = document.createElement("button");
  241. downloadButton.className =
  242. "Button smaller translucent-white round tel-download";
  243. downloadButton.setAttribute("type", "button");
  244. downloadButton.setAttribute("title", "Download");
  245. downloadButton.setAttribute("aria-label", "Download");
  246. downloadButton.setAttribute("data-tel-download-url", videoUrl);
  247. downloadButton.appendChild(downloadIcon);
  248. downloadButton.onclick = () => {
  249. tel_download_video(videoUrl);
  250. };
  251.  
  252. // Add download button to video controls
  253. const controls = videoPlayer.querySelector(".VideoPlayerControls");
  254. if (controls) {
  255. const buttons = controls.querySelector(".buttons");
  256. if (!buttons.querySelector("button.tel-download")) {
  257. const spacer = buttons.querySelector(".spacer");
  258. spacer.after(downloadButton);
  259. }
  260. }
  261.  
  262. // Add/Update/Remove download button to topbar
  263. if (mediaViewerActions.querySelector("button.tel-download")) {
  264. const telDownloadButton = mediaViewerActions.querySelector(
  265. "button.tel-download"
  266. );
  267. if (
  268. mediaViewerActions.querySelectorAll('button[title="Download"]')
  269. .length > 1
  270. ) {
  271. // There's existing download button, remove ours
  272. mediaViewerActions.querySelector("button.tel-download").remove();
  273. } else if (
  274. telDownloadButton.getAttribute("data-tel-download-url") !== videoUrl
  275. ) {
  276. // Update existing button
  277. telDownloadButton.onclick = () => {
  278. tel_download_video(videoUrl);
  279. };
  280. telDownloadButton.setAttribute("data-tel-download-url", videoUrl);
  281. }
  282. } else if (
  283. !mediaViewerActions.querySelector('button[title="Download"]')
  284. ) {
  285. // Add the button if there's no download button at all
  286. mediaViewerActions.prepend(downloadButton);
  287. }
  288. } else if (img && img.src) {
  289. const downloadIcon = document.createElement("i");
  290. downloadIcon.className = "icon icon-download";
  291. const downloadButton = document.createElement("button");
  292. downloadButton.className =
  293. "Button smaller translucent-white round tel-download";
  294. downloadButton.setAttribute("type", "button");
  295. downloadButton.setAttribute("title", "Download");
  296. downloadButton.setAttribute("aria-label", "Download");
  297. downloadButton.setAttribute("data-tel-download-url", img.src);
  298. downloadButton.appendChild(downloadIcon);
  299. downloadButton.onclick = () => {
  300. tel_download_image(img.src);
  301. };
  302.  
  303. // Add/Update/Remove download button to topbar
  304. if (mediaViewerActions.querySelector("button.tel-download")) {
  305. const telDownloadButton = mediaViewerActions.querySelector(
  306. "button.tel-download"
  307. );
  308. if (
  309. mediaViewerActions.querySelectorAll('button[title="Download"]')
  310. .length > 1
  311. ) {
  312. // There's existing download button, remove ours
  313. mediaViewerActions.querySelector("button.tel-download").remove();
  314. } else if (
  315. telDownloadButton.getAttribute("data-tel-download-url") !== img.src
  316. ) {
  317. // Update existing button
  318. telDownloadButton.onclick = () => {
  319. tel_download_image(img.src);
  320. };
  321. telDownloadButton.setAttribute("data-tel-download-url", img.src);
  322. }
  323. } else if (
  324. !mediaViewerActions.querySelector('button[title="Download"]')
  325. ) {
  326. // Add the button if there's no download button at all
  327. mediaViewerActions.prepend(downloadButton);
  328. }
  329. }
  330. }, REFRESH_DELAY);
  331.  
  332. // For webk /k/ webapp
  333. setInterval(() => {
  334. /* Voice Message */
  335. const voiceMessages = document.querySelectorAll("audio-element");
  336. voiceMessages.forEach((voiceMessage) => {
  337. if (voiceMessage.querySelector(".audio-waveform") === null) {
  338. return; /* Skip non-voice message */
  339. }
  340. if (voiceMessage.querySelector("_tel_download_button_voice_container")) {
  341. return; /* Skip if there's already a download button */
  342. }
  343. const link = voiceMessage.audio.getAttribute("src");
  344. if (link) {
  345. const container = document.createElement("div");
  346. container.className = "_tel_download_button_voice_container";
  347. container.style.position = "absolute";
  348. container.style.width = "100%";
  349. container.style.height = "100%";
  350. container.style.display = "flex";
  351. container.style.justifyContent = "center";
  352. container.style.alignItems = "end";
  353. const downloadButton = document.createElement("button");
  354. downloadButton.className =
  355. "btn-icon default__button tgico-download tel-download";
  356. downloadButton.style.marginBottom = "16px";
  357. downloadButton.style.backgroundColor = "black";
  358. downloadButton.onclick = (e) => {
  359. e.stopPropagation();
  360. tel_download_audio(voiceMessage.audio.getAttribute("src"));
  361. };
  362. voiceMessage.closest(".bubble").appendChild(container);
  363. container.appendChild(downloadButton);
  364. }
  365. });
  366.  
  367. // All media opened are located in .media-viewer-movers > .media-viewer-aspecter
  368. const mediaContainer = document.querySelector(".media-viewer-whole");
  369. if (!mediaContainer) return;
  370. const mediaAspecter = mediaContainer.querySelector(
  371. ".media-viewer-movers .media-viewer-aspecter"
  372. );
  373. const mediaButtons = mediaContainer.querySelector(
  374. ".media-viewer-topbar .media-viewer-buttons"
  375. );
  376. if (!mediaAspecter || !mediaButtons) return;
  377.  
  378. // If the download button is hidden, we can simply unhide it
  379. if (mediaButtons.querySelector(".btn-icon.tgico-download")) {
  380. const button = mediaButtons.querySelector(
  381. "button.btn-icon.tgico-download"
  382. );
  383. if (button.classList.contains("hide")) {
  384. button.classList.remove("hide");
  385. }
  386. }
  387. // If forward button is hidden, we can simply unhide it too
  388. if (mediaButtons.querySelector("button.btn-icon.tgico-forward")) {
  389. const button = mediaButtons.querySelector(
  390. "button.btn-icon.tgico-forward"
  391. );
  392. if (button.classList.contains("hide")) {
  393. button.classList.remove("hide");
  394. }
  395. }
  396.  
  397. if (mediaAspecter.querySelector(".ckin__player")) {
  398. // 1. Video player detected - Video and it has finished initial loading
  399. // container > .ckin__player > video[src]
  400.  
  401. // add download button to videos
  402. const controls = mediaAspecter.querySelector(
  403. ".default__controls.ckin__controls"
  404. );
  405. const videoUrl = mediaAspecter.querySelector("video").src;
  406.  
  407. if (controls && !controls.querySelector(".tel-download")) {
  408. const brControls = controls.querySelector(
  409. ".bottom-controls .right-controls"
  410. );
  411. const downloadButton = document.createElement("button");
  412. downloadButton.className =
  413. "btn-icon default__button tgico-download tel-download";
  414. downloadButton.setAttribute("type", "button");
  415. downloadButton.setAttribute("title", "Download");
  416. downloadButton.setAttribute("aria-label", "Download");
  417. downloadButton.onclick = () => {
  418. tel_download_video(videoUrl);
  419. };
  420. brControls.prepend(downloadButton);
  421. }
  422. } else if (
  423. mediaAspecter.querySelector("video") &&
  424. mediaAspecter.querySelector("video") &&
  425. !mediaButtons.querySelector("button.btn-icon.tgico-download")
  426. ) {
  427. // 2. Video HTML element detected, could be either GIF or unloaded video
  428. // container > video[src]
  429. const videoUrl = mediaAspecter.querySelector("video").src;
  430. const downloadButton = document.createElement("button");
  431. downloadButton.className = "btn-icon tgico-download tel-download";
  432. downloadButton.setAttribute("type", "button");
  433. downloadButton.setAttribute("title", "Download");
  434. downloadButton.setAttribute("aria-label", "Download");
  435. downloadButton.onclick = () => {
  436. tel_download_video(videoUrl);
  437. };
  438. mediaButtons.prepend(downloadButton);
  439. } else if (!mediaButtons.querySelector("button.btn-icon.tgico-download")) {
  440. // 3. Image without download button detected
  441. // container > img.thumbnail
  442. const imageUrl = mediaAspecter.querySelector("img.thumbnail").src;
  443. const downloadButton = document.createElement("button");
  444. downloadButton.className = "btn-icon tgico-download tel-download";
  445. downloadButton.setAttribute("type", "button");
  446. downloadButton.setAttribute("title", "Download");
  447. downloadButton.setAttribute("aria-label", "Download");
  448. downloadButton.onclick = () => {
  449. tel_download_image(imageUrl);
  450. };
  451. mediaButtons.prepend(downloadButton);
  452. }
  453. }, REFRESH_DELAY);
  454. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址