theworldwatch downloader

Video downloader for theworldwatch.com

  1. // ==UserScript==
  2. // @name theworldwatch downloader
  3. // @namespace Violentmonkey Scripts
  4. // @match *://theworldwatch.com/*
  5. // @match *://*.theworldwatch.com/*
  6. // @grant GM.xmlHttpRequest
  7. // @grant GM_addElement
  8. // @grant GM_addStyle
  9. // @grant GM_download
  10. // @grant GM_openInTab
  11. // @grant GM_log
  12. // @version 2.6
  13. // @author https://greasyfork.org/en/users/1409235-paywalldespiser
  14. // @description Video downloader for theworldwatch.com
  15. // @license MIT
  16. // @require https://update.greasyfork.org/scripts/523012/1516081/WaitForKeyElement.js
  17. // @require https://update.greasyfork.org/scripts/523457/1518908/GM_DL.js
  18. // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js
  19. // @connect theworldwatch.com
  20. // @connect cdn.theworldwatch.com
  21. // ==/UserScript==
  22.  
  23. /**
  24. * Gets filename from theworldwatch.com video URL
  25. *
  26. * @param {string} url
  27. * @returns {string}
  28. */
  29. function getFilenameFromURL(url) {
  30. const title = document.title;
  31. for (const [, videoURL] of url.matchAll(/\/get_file\/.*\/(.*?)\.mp4/gm)) {
  32. if (videoURL) {
  33. return `${title} [${videoURL}].mp4`;
  34. }
  35. }
  36.  
  37. return `${title}.mp4`;
  38. }
  39.  
  40. (function () {
  41. GM_addStyle(`
  42. .loader {
  43. border: 0.25em solid #f3f3f3;
  44. border-top: 0.25em solid rgba(0, 0, 0, 0);
  45. border-radius: 50%;
  46. width: 1em;
  47. height: 1em;
  48. animation: spin 2s linear infinite;
  49. }
  50. @keyframes spin {
  51. 0% {
  52. transform: rotate(0deg);
  53. }
  54. 100% {
  55. transform: rotate(360deg);
  56. }
  57. }
  58. .kt-player .fp-controls .fp-download {
  59. display: inline-flex;
  60. float: right;
  61. height: 100%;
  62. justify-content: center;
  63. align-items: center;
  64. margin: 2px 4px 4px 10px;
  65. }
  66. `);
  67. waitForKeyElement('.player video').then((video) => {
  68. const fullScreenButton = video.nextElementSibling.querySelector(
  69. '.fp-controls > .fp-screen'
  70. );
  71. const downloadButtonContainer = GM_addElement('div');
  72. downloadButtonContainer.classList.add('fp-download');
  73.  
  74. const downloadButton = GM_addElement('a');
  75. downloadButton.style.fontSize = '1.4em';
  76. downloadButton.textContent = '⬇';
  77.  
  78. downloadButtonContainer.appendChild(downloadButton);
  79.  
  80. const loadingElement = GM_addElement('div');
  81. loadingElement.classList.add('loader');
  82.  
  83. downloadButton.addEventListener('click', () => {
  84. downloadButtonContainer.removeChild(downloadButton);
  85. downloadButtonContainer.appendChild(loadingElement);
  86. GM_DL({url: video.src, filename: getFilenameFromURL(video.src)}).then(
  87. () => {
  88. downloadButtonContainer.appendChild(downloadButton);
  89.  
  90. downloadButtonContainer.removeChild(loadingElement);
  91.  
  92. downloadButton.textContent = '✓';
  93. },
  94. (e) => {
  95. GM_log(`error: ${e}`);
  96. console.error(e);
  97. downloadButtonContainer.appendChild(downloadButton);
  98.  
  99. downloadButtonContainer.removeChild(loadingElement);
  100.  
  101. downloadButton.textContent = '⮿';
  102. }
  103. );
  104. });
  105. fullScreenButton.parentNode?.insertBefore(
  106. downloadButtonContainer,
  107. fullScreenButton.nextSibling
  108. );
  109. });
  110. })();