Telegram Media Downloader

Download images, GIFs, videos, and voice messages on the Telegram webapp from private channels that disable downloading and restrict saving content

05.01.2024 itibariyledir. En son verisyonu görün.

  1. // ==UserScript==
  2. // @name Telegram Media Downloader
  3. // @name:zh-CN Telegram图片视频下载器
  4. // @version 1.100
  5. // @namespace https://github.com/Neet-Nestor/Telegram-Media-Downloader
  6. // @description Download images, GIFs, videos, and voice messages on the Telegram webapp from private channels that disable downloading and restrict 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, fileName = null) => {
  20. console.log(
  21. `[Tel Download] ${fileName ? `${fileName}: ` : ""}${message}`
  22. );
  23. },
  24. error: (message, fileName = null) => {
  25. console.error(
  26. `[Tel Download] ${fileName ? `${fileName}: ` : ""}${message}`
  27. );
  28. },
  29. };
  30. const contentRangeRegex = /^bytes (\d+)-(\d+)\/(\d+)$/;
  31. const REFRESH_DELAY = 500;
  32. const hashCode = (s) => {
  33. var h = 0, l = s.length, i = 0;
  34. if ( l > 0 ) {
  35. while (i < l) {
  36. h = (h << 5) - h + s.charCodeAt(i++) | 0;
  37. }
  38. }
  39. return h >>> 0;
  40. };
  41.  
  42. const createProgressBar = (videoId, fileName) => {
  43. const container = document.getElementById("tel-downloader-progress-bar-container");
  44. const innerContainer = document.createElement("div");
  45. innerContainer.id = "tel-downloader-progress-" + videoId;
  46. innerContainer.style.width = "20rem";
  47. innerContainer.style.backgroundColor = "#0003";
  48. innerContainer.style.marginTop = "1rem";
  49.  
  50. const flexContainer = document.createElement("div");
  51. flexContainer.style.display = "flex";
  52. flexContainer.style.justifyContent = "space-between";
  53.  
  54. const title = document.createElement("p");
  55. title.className = "filename";
  56. title.style.margin = 0;
  57. title.innerText = fileName;
  58.  
  59. const closeButton = document.createElement("div");
  60. closeButton.style.color = '#8a8a8a';
  61. closeButton.style.cursor = 'pointer';
  62. closeButton.style.fontSize = '1.2rem';
  63. closeButton.innerHTML = '&times;';
  64. closeButton.onclick = function() {
  65. container.removeChild(innerContainer);
  66. };
  67.  
  68. const progressBar = document.createElement("div");
  69. progressBar.className = 'progress';
  70. progressBar.style.backgroundColor = '#e2e2e2';
  71. progressBar.style.position = "relative";
  72. progressBar.style.width = "100%";
  73. progressBar.style.height = "1.6rem";
  74. progressBar.style.borderRadius = "2rem";
  75. progressBar.style.overflow = "hidden";
  76. const counter = document.createElement("p");
  77. counter.style.position = "absolute";
  78. counter.style.zIndex = 5;
  79. counter.style.left = "50%";
  80. counter.style.top = "50%";
  81. counter.style.transform = "translate(-50%, -50%)";
  82. counter.style.margin = 0;
  83. counter.style.color = "black";
  84. const progress = document.createElement("div");
  85. progress.style.position = "absolute";
  86. progress.style.height = "100%";
  87. progress.style.width = "0%";
  88. progress.style.backgroundColor = "#6093B5";
  89.  
  90. progressBar.appendChild(counter);
  91. progressBar.appendChild(progress);
  92. flexContainer.appendChild(title);
  93. flexContainer.appendChild(closeButton);
  94. innerContainer.appendChild(flexContainer);
  95. innerContainer.appendChild(progressBar);
  96. container.appendChild(innerContainer);
  97. }
  98.  
  99. const updateProgress = (videoId, fileName, progress) => {
  100. const innerContainer = document.getElementById("tel-downloader-progress-" + videoId);
  101. innerContainer.querySelector("p.filename").innerText = fileName;
  102. const progressBar = innerContainer.querySelector("div.progress");
  103. progressBar.querySelector("p").innerText = progress + "%";
  104. progressBar.querySelector("div").style.width = progress + "%";
  105. }
  106.  
  107. const completeProgress = (videoId) => {
  108. const progressBar = document.getElementById("tel-downloader-progress-" + videoId).querySelector("div.progress");
  109. progressBar.querySelector("p").innerText = "Completed";
  110. progressBar.querySelector("div").style.backgroundColor = "#B6C649";
  111. progressBar.querySelector("div").style.width = "100%";
  112. }
  113.  
  114. const AbortProgress = (videoId) => {
  115. const progressBar = document.getElementById("tel-downloader-progress-" + videoId).querySelector("div.progress");
  116. progressBar.querySelector("p").innerText = "Aborted";
  117. progressBar.querySelector("div").style.backgroundColor = "#D16666";
  118. progressBar.querySelector("div").style.width = "100%";
  119. }
  120.  
  121. const tel_download_video = (url) => {
  122. let _blobs = [];
  123. let _next_offset = 0;
  124. let _total_size = null;
  125. let _file_extension = "mp4";
  126.  
  127. const videoId = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString();
  128. let fileName = hashCode(url).toString(36) + "." + _file_extension;
  129.  
  130. // Some video src is in format:
  131. // 'stream/{"dcId":5,"location":{...},"size":...,"mimeType":"video/mp4","fileName":"xxxx.MP4"}'
  132. try {
  133. const metadata = JSON.parse(
  134. decodeURIComponent(url.split("/")[url.split("/").length - 1])
  135. );
  136. if (metadata.fileName) {
  137. fileName = metadata.fileName;
  138. }
  139. } catch (e) {
  140. // Invalid JSON string, pass extracting fileName
  141. }
  142. logger.info(`URL: ${url}`, fileName);
  143.  
  144. const fetchNextPart = (_writable) => {
  145. fetch(url, {
  146. method: "GET",
  147. headers: {
  148. Range: `bytes=${_next_offset}-`,
  149. },
  150. "User-Agent":
  151. "User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0",
  152. })
  153. .then((res) => {
  154. if (![200, 206].includes(res.status)) {
  155. throw new Error("Non 200/206 response was received: " + res.status);
  156. }
  157. const mime = res.headers.get("Content-Type").split(";")[0];
  158. if (!mime.startsWith("video/")) {
  159. throw new Error("Get non video response with MIME type " + mime);
  160. }
  161. _file_extension = mime.split("/")[1];
  162. fileName =
  163. fileName.substring(0, fileName.indexOf(".") + 1) + _file_extension;
  164.  
  165. const match = res.headers
  166. .get("Content-Range")
  167. .match(contentRangeRegex);
  168.  
  169. const startOffset = parseInt(match[1]);
  170. const endOffset = parseInt(match[2]);
  171. const totalSize = parseInt(match[3]);
  172.  
  173. if (startOffset !== _next_offset) {
  174. logger.error("Gap detected between responses.", fileName);
  175. logger.info("Last offset: " + _next_offset, fileName);
  176. logger.info("New start offset " + match[1], fileName);
  177. throw "Gap detected between responses.";
  178. }
  179. if (_total_size && totalSize !== _total_size) {
  180. logger.error("Total size differs", fileName);
  181. throw "Total size differs";
  182. }
  183.  
  184. _next_offset = endOffset + 1;
  185. _total_size = totalSize;
  186.  
  187. logger.info(
  188. `Get response: ${res.headers.get(
  189. "Content-Length"
  190. )} bytes data from ${res.headers.get("Content-Range")}`,
  191. fileName
  192. );
  193. logger.info(
  194. `Progress: ${((_next_offset * 100) / _total_size).toFixed(0)}%`,
  195. fileName
  196. );
  197. updateProgress(videoId, fileName, ((_next_offset * 100) / _total_size).toFixed(0));
  198. return res.blob();
  199. })
  200. .then((resBlob) => {
  201. if (_writable !== null) {
  202. _writable.write(resBlob).then(() => {});
  203. } else {
  204. _blobs.push(resBlob);
  205. }
  206. })
  207. .then(() => {
  208. if (!_total_size) {
  209. throw new Error("_total_size is NULL");
  210. }
  211.  
  212. if (_next_offset < _total_size) {
  213. fetchNextPart(_writable);
  214. } else {
  215. if (_writable !== null) {
  216. _writable.close().then(() => {
  217. logger.info("Download finished", fileName);
  218. });
  219. } else {
  220. save();
  221. }
  222. completeProgress(videoId);
  223. }
  224. })
  225. .catch((reason) => {
  226. logger.error(reason, fileName);
  227. AbortProgress(videoId);
  228. });
  229. };
  230.  
  231. const save = () => {
  232. logger.info("Finish downloading blobs", fileName);
  233. logger.info("Concatenating blobs and downloading...", fileName);
  234.  
  235. const blob = new Blob(_blobs, { type: "video/mp4" });
  236. const blobUrl = window.URL.createObjectURL(blob);
  237.  
  238. logger.info("Final blob size: " + blob.size + " bytes", fileName);
  239.  
  240. const a = document.createElement("a");
  241. document.body.appendChild(a);
  242. a.href = blobUrl;
  243. a.download = fileName;
  244. a.click();
  245. document.body.removeChild(a);
  246. window.URL.revokeObjectURL(blobUrl);
  247.  
  248. logger.info("Download triggered", fileName);
  249. };
  250.  
  251. const supportsFileSystemAccess =
  252. 'showSaveFilePicker' in unsafeWindow &&
  253. (() => {
  254. try {
  255. return unsafeWindow.self === unsafeWindow.top;
  256. } catch {
  257. return false;
  258. }
  259. })();
  260. if (supportsFileSystemAccess) {
  261. unsafeWindow.showSaveFilePicker({
  262. suggestedName: fileName,
  263. }).then((handle) => {
  264. handle.createWritable().then((writable) => {
  265. fetchNextPart(writable);
  266. createProgressBar(videoId);
  267. }).catch((err) => {
  268. console.error(err.name, err.message);
  269. });
  270. }).catch((err) => {
  271. if (err.name !== 'AbortError') {
  272. console.error(err.name, err.message);
  273. }
  274. });
  275. } else {
  276. fetchNextPart(null);
  277. createProgressBar(videoId);
  278. }
  279. };
  280.  
  281. const tel_download_audio = (url) => {
  282. let _blobs = [];
  283. let _next_offset = 0;
  284. let _total_size = null;
  285. const fileName = hashCode(url).toString(36) + ".ogg";
  286.  
  287. const fetchNextPart = (_writable) => {
  288. fetch(url, {
  289. method: "GET",
  290. headers: {
  291. Range: `bytes=${_next_offset}-`,
  292. },
  293. })
  294. .then((res) => {
  295. if (res.status !== 206 && res.status !== 200) {
  296. logger.error(
  297. "Non 200/206 response was received: " + res.status,
  298. fileName
  299. );
  300. return;
  301. }
  302.  
  303. const mime = res.headers.get("Content-Type").split(";")[0];
  304. if (!mime.startsWith("audio/")) {
  305. logger.error(
  306. "Get non audio response with MIME type " + mime,
  307. fileName
  308. );
  309. throw "Get non audio response with MIME type " + mime;
  310. }
  311.  
  312. try {
  313. const match = res.headers
  314. .get("Content-Range")
  315. .match(contentRangeRegex);
  316.  
  317. const startOffset = parseInt(match[1]);
  318. const endOffset = parseInt(match[2]);
  319. const totalSize = parseInt(match[3]);
  320.  
  321. if (startOffset !== _next_offset) {
  322. logger.error("Gap detected between responses.");
  323. logger.info("Last offset: " + _next_offset);
  324. logger.info("New start offset " + match[1]);
  325. throw "Gap detected between responses.";
  326. }
  327. if (_total_size && totalSize !== _total_size) {
  328. logger.error("Total size differs");
  329. throw "Total size differs";
  330. }
  331.  
  332. _next_offset = endOffset + 1;
  333. _total_size = totalSize;
  334. } finally {
  335. logger.info(
  336. `Get response: ${res.headers.get(
  337. "Content-Length"
  338. )} bytes data from ${res.headers.get("Content-Range")}`
  339. );
  340. return res.blob();
  341. }
  342. })
  343. .then((resBlob) => {
  344. if (_writable !== null) {
  345. _writable.write(resBlob).then(() => {});
  346. } else {
  347. _blobs.push(resBlob);
  348. }
  349. })
  350. .then(() => {
  351. if (_next_offset < _total_size) {
  352. fetchNextPart(_writable);
  353. } else {
  354. if (_writable !== null) {
  355. _writable.close().then(() => {
  356. logger.info("Download finished", fileName);
  357. });
  358. } else {
  359. save();
  360. }
  361. }
  362. })
  363. .catch((reason) => {
  364. logger.error(reason, fileName);
  365. });
  366. };
  367.  
  368. const save = () => {
  369. logger.info(
  370. "Finish downloading blobs. Concatenating blobs and downloading...",
  371. fileName
  372. );
  373.  
  374. let blob = new Blob(_blobs, { type: "audio/ogg" });
  375. const blobUrl = window.URL.createObjectURL(blob);
  376.  
  377. logger.info("Final blob size in bytes: " + blob.size, fileName);
  378.  
  379. blob = 0;
  380.  
  381. const a = document.createElement("a");
  382. document.body.appendChild(a);
  383. a.href = blobUrl;
  384. a.download = fileName;
  385. a.click();
  386. document.body.removeChild(a);
  387. window.URL.revokeObjectURL(blobUrl);
  388.  
  389. logger.info("Download triggered", fileName);
  390. };
  391.  
  392. const supportsFileSystemAccess =
  393. 'showSaveFilePicker' in unsafeWindow &&
  394. (() => {
  395. try {
  396. return unsafeWindow.self === unsafeWindow.top;
  397. } catch {
  398. return false;
  399. }
  400. })();
  401. if (supportsFileSystemAccess) {
  402. unsafeWindow.showSaveFilePicker({
  403. suggestedName: fileName,
  404. }).then((handle) => {
  405. handle.createWritable().then((writable) => {
  406. fetchNextPart(writable);
  407. }).catch((err) => {
  408. console.error(err.name, err.message);
  409. });
  410. }).catch((err) => {
  411. if (err.name !== 'AbortError') {
  412. console.error(err.name, err.message);
  413. }
  414. });
  415. } else {
  416. fetchNextPart(null);
  417. }
  418. };
  419.  
  420. const tel_download_image = (imageUrl) => {
  421. const fileName =
  422. (Math.random() + 1).toString(36).substring(2, 10) + ".jpeg"; // assume jpeg
  423.  
  424. const a = document.createElement("a");
  425. document.body.appendChild(a);
  426. a.href = imageUrl;
  427. a.download = fileName;
  428. a.click();
  429. document.body.removeChild(a);
  430.  
  431. logger.info("Download triggered", fileName);
  432. };
  433.  
  434. logger.info("Initialized");
  435.  
  436. // For webz /a/ webapp
  437. setInterval(() => {
  438. // All media opened are located in .media-viewer-movers > .media-viewer-aspecter
  439. const mediaContainer = document.querySelector(
  440. "#MediaViewer .MediaViewerSlide--active"
  441. );
  442. if (!mediaContainer) return;
  443. const mediaViewerActions = document.querySelector(
  444. "#MediaViewer .MediaViewerActions"
  445. );
  446. if (!mediaViewerActions) return;
  447.  
  448. const videoPlayer = mediaContainer.querySelector(
  449. ".MediaViewerContent > .VideoPlayer"
  450. );
  451. const img = mediaContainer.querySelector(".MediaViewerContent > div > img");
  452. // 1. Video player detected - Video or GIF
  453. // container > .MediaViewerSlides > .MediaViewerSlide > .MediaViewerContent > .VideoPlayer > video[src]
  454. const downloadIcon = document.createElement("i");
  455. downloadIcon.className = "icon icon-download";
  456. const downloadButton = document.createElement("button");
  457. downloadButton.className =
  458. "Button smaller translucent-white round tel-download";
  459. downloadButton.setAttribute("type", "button");
  460. downloadButton.setAttribute("title", "Download");
  461. downloadButton.setAttribute("aria-label", "Download");
  462. if (videoPlayer) {
  463. const videoUrl = videoPlayer.querySelector("video").currentSrc;
  464. downloadButton.setAttribute("data-tel-download-url", videoUrl);
  465. downloadButton.appendChild(downloadIcon);
  466. downloadButton.onclick = () => {
  467. tel_download_video(videoPlayer.querySelector("video").currentSrc);
  468. };
  469.  
  470. // Add download button to video controls
  471. const controls = videoPlayer.querySelector(".VideoPlayerControls");
  472. if (controls) {
  473. const buttons = controls.querySelector(".buttons");
  474. if (!buttons.querySelector("button.tel-download")) {
  475. const spacer = buttons.querySelector(".spacer");
  476. spacer.after(downloadButton);
  477. }
  478. }
  479.  
  480. // Add/Update/Remove download button to topbar
  481. if (mediaViewerActions.querySelector("button.tel-download")) {
  482. const telDownloadButton = mediaViewerActions.querySelector(
  483. "button.tel-download"
  484. );
  485. if (
  486. mediaViewerActions.querySelectorAll('button[title="Download"]')
  487. .length > 1
  488. ) {
  489. // There's existing download button, remove ours
  490. mediaViewerActions.querySelector("button.tel-download").remove();
  491. } else if (
  492. telDownloadButton.getAttribute("data-tel-download-url") !== videoUrl
  493. ) {
  494. // Update existing button
  495. telDownloadButton.onclick = () => {
  496. tel_download_video(videoPlayer.querySelector("video").currentSrc);
  497. };
  498. telDownloadButton.setAttribute("data-tel-download-url", videoUrl);
  499. }
  500. } else if (
  501. !mediaViewerActions.querySelector('button[title="Download"]')
  502. ) {
  503. // Add the button if there's no download button at all
  504. mediaViewerActions.prepend(downloadButton);
  505. }
  506. } else if (img && img.src) {
  507. downloadButton.setAttribute("data-tel-download-url", img.src);
  508. downloadButton.appendChild(downloadIcon);
  509. downloadButton.onclick = () => {
  510. tel_download_image(img.src);
  511. };
  512.  
  513. // Add/Update/Remove download button to topbar
  514. if (mediaViewerActions.querySelector("button.tel-download")) {
  515. const telDownloadButton = mediaViewerActions.querySelector(
  516. "button.tel-download"
  517. );
  518. if (
  519. mediaViewerActions.querySelectorAll('button[title="Download"]')
  520. .length > 1
  521. ) {
  522. // There's existing download button, remove ours
  523. mediaViewerActions.querySelector("button.tel-download").remove();
  524. } else if (
  525. telDownloadButton.getAttribute("data-tel-download-url") !== img.src
  526. ) {
  527. // Update existing button
  528. telDownloadButton.onclick = () => {
  529. tel_download_image(img.src);
  530. };
  531. telDownloadButton.setAttribute("data-tel-download-url", img.src);
  532. }
  533. } else if (
  534. !mediaViewerActions.querySelector('button[title="Download"]')
  535. ) {
  536. // Add the button if there's no download button at all
  537. mediaViewerActions.prepend(downloadButton);
  538. }
  539. }
  540. }, REFRESH_DELAY);
  541.  
  542. // For webk /k/ webapp
  543. setInterval(() => {
  544. /* Voice Message */
  545. const pinnedAudio = document.body.querySelector(".pinned-audio");
  546. let dataMid;
  547. let downloadButtonPinnedAudio =
  548. document.body.querySelector("._tel_download_button_pinned_container") ||
  549. document.createElement("button");
  550. if (pinnedAudio) {
  551. dataMid = pinnedAudio.getAttribute("data-mid");
  552. downloadButtonPinnedAudio.className =
  553. "btn-icon tgico-download _tel_download_button_pinned_container";
  554. downloadButtonPinnedAudio.innerHTML =
  555. '<span class="tgico button-icon">\uE93E</span>';
  556. }
  557. const voiceMessages = document.body.querySelectorAll("audio-element");
  558. voiceMessages.forEach((voiceMessage) => {
  559. const bubble = voiceMessage.closest(".bubble");
  560. if (
  561. !bubble ||
  562. bubble.querySelector("._tel_download_button_pinned_container")
  563. ) {
  564. return; /* Skip if there's already a download button */
  565. }
  566. if (
  567. dataMid &&
  568. downloadButtonPinnedAudio.getAttribute("data-mid") !== dataMid &&
  569. voiceMessage.getAttribute("data-mid") === dataMid
  570. ) {
  571. downloadButtonPinnedAudio.onclick = (e) => {
  572. e.stopPropagation();
  573. tel_download_audio(link);
  574. };
  575. downloadButtonPinnedAudio.setAttribute("data-mid", dataMid);
  576. const link =
  577. voiceMessage.audio && voiceMessage.audio.getAttribute("src");
  578. if (link) {
  579. pinnedAudio
  580. .querySelector(".pinned-container-wrapper-utils")
  581. .appendChild(downloadButtonPinnedAudio);
  582. }
  583. }
  584. });
  585.  
  586. // All media opened are located in .media-viewer-movers > .media-viewer-aspecter
  587. const mediaContainer = document.querySelector(".media-viewer-whole");
  588. if (!mediaContainer) return;
  589. const mediaAspecter = mediaContainer.querySelector(
  590. ".media-viewer-movers .media-viewer-aspecter"
  591. );
  592. const mediaButtons = mediaContainer.querySelector(
  593. ".media-viewer-topbar .media-viewer-buttons"
  594. );
  595. if (!mediaAspecter || !mediaButtons) return;
  596.  
  597. // If the download button is hidden, we can simply unhide it
  598. if (mediaButtons.querySelector(".btn-icon.tgico-download")) {
  599. const button = mediaButtons.querySelector(
  600. "button.btn-icon.tgico-download"
  601. );
  602. if (button.classList.contains("hide")) {
  603. button.classList.remove("hide");
  604. }
  605. }
  606. // If forward button is hidden, we can simply unhide it too
  607. if (mediaButtons.querySelector("button.btn-icon.tgico-forward")) {
  608. const button = mediaButtons.querySelector(
  609. "button.btn-icon.tgico-forward"
  610. );
  611. if (button.classList.contains("hide")) {
  612. button.classList.remove("hide");
  613. }
  614. }
  615.  
  616. if (mediaAspecter.querySelector(".ckin__player")) {
  617. // 1. Video player detected - Video and it has finished initial loading
  618. // container > .ckin__player > video[src]
  619.  
  620. // add download button to videos
  621. const controls = mediaAspecter.querySelector(
  622. ".default__controls.ckin__controls"
  623. );
  624. const videoUrl = mediaAspecter.querySelector("video").src;
  625.  
  626. if (controls && !controls.querySelector(".tel-download")) {
  627. const brControls = controls.querySelector(
  628. ".bottom-controls .right-controls"
  629. );
  630. const downloadButton = document.createElement("button");
  631. downloadButton.className =
  632. "btn-icon default__button tgico-download tel-download";
  633. downloadButton.innerHTML =
  634. '<span class="tgico button-icon">\uE93E</span>';
  635. downloadButton.setAttribute("type", "button");
  636. downloadButton.setAttribute("title", "Download");
  637. downloadButton.setAttribute("aria-label", "Download");
  638. downloadButton.onclick = () => {
  639. tel_download_video(mediaAspecter.querySelector("video").src);
  640. };
  641. brControls.prepend(downloadButton);
  642. }
  643. } else if (
  644. mediaAspecter.querySelector("video") &&
  645. mediaAspecter.querySelector("video") &&
  646. !mediaButtons.querySelector("button.btn-icon.tgico-download")
  647. ) {
  648. // 2. Video HTML element detected, could be either GIF or unloaded video
  649. // container > video[src]
  650. const videoUrl = mediaAspecter.querySelector("video").src;
  651. const downloadButton = document.createElement("button");
  652. downloadButton.className = "btn-icon tgico-download tel-download";
  653. downloadButton.innerHTML =
  654. '<span class="tgico button-icon">\uE93E</span>';
  655. downloadButton.setAttribute("type", "button");
  656. downloadButton.setAttribute("title", "Download");
  657. downloadButton.setAttribute("aria-label", "Download");
  658. downloadButton.onclick = () => {
  659. tel_download_video(mediaAspecter.querySelector("video").src);
  660. };
  661. mediaButtons.prepend(downloadButton);
  662. } else if (!mediaButtons.querySelector("button.btn-icon.tgico-download")) {
  663. // 3. Image without download button detected
  664. // container > img.thumbnail
  665. if (!mediaAspecter.querySelector("img.thumbnail") || !mediaAspecter.querySelector("img.thumbnail").src) {
  666. return;
  667. }
  668. const downloadButton = document.createElement("button");
  669. downloadButton.className = "btn-icon tgico-download tel-download";
  670. downloadButton.innerHTML =
  671. '<span class="tgico button-icon">\uE93E</span>';
  672. downloadButton.setAttribute("type", "button");
  673. downloadButton.setAttribute("title", "Download");
  674. downloadButton.setAttribute("aria-label", "Download");
  675. downloadButton.onclick = () => {
  676. tel_download_image(mediaAspecter.querySelector("img.thumbnail").src);
  677. };
  678. mediaButtons.prepend(downloadButton);
  679. }
  680. }, REFRESH_DELAY);
  681.  
  682. // Progress bar container setup
  683. (function() {
  684. const body = document.querySelector("body");
  685. const container = document.createElement("div");
  686. container.id = "tel-downloader-progress-bar-container";
  687. container.style.position = "fixed";
  688. container.style.bottom = 0;
  689. container.style.right = 0;
  690. if (location.pathname.startsWith('/k/')) {
  691. container.style.zIndex = 4;
  692. } else{
  693. container.style.zIndex = 1500;
  694. }
  695. container.style.padding = '1.6rem';
  696. container.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
  697. body.appendChild(container);
  698. })();
  699. })();