[J]Douyin Video Downloader

Download videos from Douyin website

  1. // ==UserScript==
  2. // @name [J]Douyin Video Downloader
  3. // @namespace http://tampermonkey.net/
  4. // @version 2024120821
  5. // @description Download videos from Douyin website
  6. // @author jeffc
  7. // @match *.douyin.com/*
  8. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. function Video(authorName, desc, id) {
  17. this.authorName = authorName || "";
  18. this.videoDesc = desc || "";
  19. this.videoId = id || "";
  20. this.videoUrl = "";
  21. }
  22.  
  23. Video.prototype = {
  24. constructor: Video,
  25. download: function() {
  26. var xhr = new XMLHttpRequest();
  27. xhr.open('GET', `https://www.douyin.com/aweme/v1/web/aweme/detail/?device_platform=webapp&aweme_id=${this.videoId}&screen_width=1920&screen_height=1080`, true);
  28. xhr.responseType = 'json';
  29. xhr.onload = () => {
  30. if (xhr.status === 200) {
  31. var responseData = xhr.response;
  32. this.videoUrl = responseData.aweme_detail.video.bit_rate[0].play_addr.url_list[0];
  33. this.save();
  34. } else {
  35. console.error('request error:', xhr.statusText);
  36. }
  37. };
  38. xhr.onerror = function() {
  39. console.error('request error:', xhr.statusText);
  40. };
  41. xhr.send();
  42. },
  43. clear: function() {
  44. this.authorName = "";
  45. this.videoDesc = "";
  46. this.videoId = "";
  47. this.videoUrl = "";
  48. },
  49. save: function() {
  50. if (this.videoUrl.length > 0) {
  51. let downloadDom = document.createElement('a');
  52. downloadDom.href = this.videoUrl;
  53. downloadDom.target = "_blank";
  54. // downloadDom.download = `${this.authorName}_${this.videoDesc}.mp4`;
  55. document.body.appendChild(downloadDom);
  56. downloadDom.setAttribute("download",((new Date()).getTime())+".mp4")
  57. downloadDom.click();
  58. document.body.removeChild(downloadDom);
  59. this.clear();
  60. } else {
  61. alert("无法解析视频");
  62. }
  63. }
  64. };
  65.  
  66. const _historyWrap = function(type) {
  67. const orig = history[type];
  68. const e = new Event(type);
  69. return function() {
  70. const rv = orig.apply(this, arguments);
  71. e.arguments = arguments;
  72. window.dispatchEvent(e);
  73. return rv;
  74. };
  75. }
  76. history.pushState = _historyWrap('pushState');
  77. history.replaceState = _historyWrap('replaceState');
  78.  
  79. window.addEventListener('pushState', function(e) {
  80. console.log('page change ');
  81. dynamicMonitoring();
  82. });
  83. window.addEventListener('replaceState', function(e) {
  84. console.log('page change ');
  85. dynamicMonitoring();
  86. });
  87.  
  88.  
  89. const PageType = {
  90. Detail: 'Detail', // 详情页
  91. Normal: 'Normal', // 作者主页
  92. Live: 'Live', // 直播
  93. };
  94.  
  95. let currentPageType = PageType.Normal;
  96.  
  97. let liveExecuStatus = false;
  98.  
  99. // 动态监测函数
  100. function dynamicMonitoring() {
  101. if (/www.douyin.com\/video\/[0-9]{9,}/.test(window.location.href)) {
  102. currentPageType = PageType.Detail;
  103. var initialTargetNode = document.querySelector('xg-video-container');
  104. var mobserver = new MutationObserver(function(mutationsList, observer) {
  105. mutationsList.forEach(function(mutation) {
  106. refreshDownloadDom(initialTargetNode);
  107. });
  108. });
  109. var mconfig = {
  110. childList:true,
  111. subtree:true
  112. };
  113. let listener = setInterval(function() {
  114. if (!initialTargetNode ) {
  115. initialTargetNode = document.querySelector('xg-video-container');
  116. }else{
  117. mobserver.observe(initialTargetNode, mconfig);
  118. refreshDownloadDom(initialTargetNode);
  119. clearInterval(listener);
  120. }
  121. }, 500);
  122.  
  123. } else if(!liveExecuStatus && ( /live.douyin.com\/[0-9]{9,}/.test(window.location.href) || /www.douyin.com\/follow\/live\/[0-9]{9,}/.test(window.location.href))){
  124. liveExecuStatus = true;
  125. var targetUrl="";
  126. const originalFetch = window.fetch;
  127. window.fetch = function(url, options) {
  128. if (url.includes('.mp4') || url.includes('.m3u8') || url.includes('.ts') || url.includes('.flv')) {
  129. targetUrl = url;
  130. }
  131. return originalFetch.apply(this, [url, options]);
  132. };
  133.  
  134. var findlive = setInterval(function(){
  135. if(targetUrl != "" && document.querySelector(".__leftContainer"))
  136. {
  137. console.log("=============");
  138. addcss();
  139. var savebtn = document.createElement("button");
  140. savebtn.textContent="直播录制";
  141. savebtn.className="jbtn";
  142. savebtn.addEventListener("click",function(){
  143. var ddd = document.createElement('a');
  144. ddd.href = targetUrl;
  145. ddd.target = "_blank";
  146. ddd.setAttribute("download","");
  147. document.body.appendChild(ddd);
  148. ddd.click();
  149. document.body.removeChild(ddd);
  150. });
  151. document.querySelector(".__leftContainer").appendChild(savebtn);
  152. clearInterval(findlive);
  153. savebtn.previousSibling.style="margin-right:10px";
  154. }
  155. },200);
  156. } else {
  157.  
  158. // 通用页
  159. currentPageType = PageType.Normal;
  160.  
  161.  
  162. monitoringSilder();
  163.  
  164. var observeTargetNode;
  165. var observer = new IntersectionObserver(function(entries) {
  166. entries.forEach(function(entry) {
  167. switchObserverTarget();
  168. });
  169. });
  170. var config = {
  171. attributes: true,
  172. attributeFilter: ['data-e2e']
  173. };
  174.  
  175. let listener = setInterval(function() {
  176. if (!observeTargetNode ) {
  177. observeTargetNode = document.querySelector('div[data-e2e="feed-active-video"]');
  178. }else{
  179. observer.observe(observeTargetNode, config);
  180. refreshDownloadDom(observeTargetNode);
  181. clearInterval(listener);
  182. }
  183. }, 500);
  184.  
  185.  
  186.  
  187. // 切换观察目标节点的函数
  188. function switchObserverTarget() {
  189. // 重新获取目标节点
  190. var newTargetNode = document.querySelector('div[data-e2e="feed-active-video"]');
  191.  
  192. let listener = setInterval(function() {
  193. if (!newTargetNode ) {
  194. newTargetNode = document.querySelector('div[data-e2e="feed-active-video"]');
  195. }else{
  196. if (newTargetNode && newTargetNode !== observeTargetNode) {
  197. observer.unobserve(observeTargetNode);
  198. observer.observe(newTargetNode, config);
  199. observeTargetNode = newTargetNode;
  200. refreshDownloadDom(newTargetNode);
  201. }
  202. clearInterval(listener);
  203. }
  204. }, 500);
  205.  
  206. }
  207. }
  208. }
  209.  
  210. // 更新通用页的下载按钮
  211. function refreshDownloadDom(activeVideoDom) {
  212. var downloadBtn = activeVideoDom.querySelector('#douyin_download_by_jeffc');
  213. if (!downloadBtn) {
  214. downloadBtn = document.createElement('div');
  215. downloadBtn.setAttribute("data-index", "10");
  216. downloadBtn.id = "douyin_download_by_jeffc";
  217. downloadBtn.innerHTML = "DOWNLOAD";
  218. downloadBtn.style = 'cursor:pointer;width: 60px;text-align: center;font-size: 14px;color: rgba(255, 255, 255,0.75);line-height:20px;margin-right: 30px;';
  219. downloadBtn.addEventListener('click', function() {
  220. downloadVideo(activeVideoDom);
  221. });
  222. let listenerActiveVideo = setInterval(function() {
  223. let targetNode = currentPageType == currentPageType.Normal ? activeVideoDom.querySelector("xg-right-grid") : activeVideoDom.parentNode.querySelector("xg-right-grid") ;
  224. if (targetNode ) {
  225. if(!targetNode.querySelector("#douyin_download_by_jeffc"))
  226. {
  227. targetNode.appendChild(downloadBtn);
  228. }
  229. clearInterval(listenerActiveVideo);
  230. }
  231. }, 500);
  232. }
  233. }
  234.  
  235.  
  236. // 下载视频的函数
  237. function downloadVideo(activeVideoDom) {
  238. var authorName = activeVideoDom.querySelector('[data-e2e="feed-video-nickname"]')?.innerText.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '');
  239. var videoDesc = activeVideoDom.querySelector('[data-e2e="video-desc"]')?.innerText.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '');
  240. var videoDom = activeVideoDom.querySelector("video");
  241. var sourceUrl;
  242. if (videoDom) {
  243. if (videoDom.childNodes.length > 0) {
  244. sourceUrl = videoDom.querySelector("source")?.src;
  245. }
  246. } else {
  247. sourceUrl = activeVideoDom.querySelector("xg-video")?.src;
  248. }
  249. if (sourceUrl) {
  250. // 直接下载
  251. var downloadDom = document.createElement('a');
  252. downloadDom.href = sourceUrl;
  253. downloadDom.target = "_blank";
  254. downloadDom.setAttribute("download","");
  255. document.body.appendChild(downloadDom);
  256. downloadDom.click();
  257. document.body.removeChild(downloadDom);
  258. } else {
  259. // 解析视频并下载
  260. var vid = activeVideoDom.getAttribute("data-e2e-vid");
  261. (new Video(authorName, videoDesc, vid)).download();
  262. }
  263. }
  264.  
  265. function addcss()
  266. {
  267. const style = document.createElement('style');
  268. style.innerHTML = `
  269. .jbtn {
  270. magin-left:20px;
  271. padding: 5px 25px;
  272. font-size: 14px;
  273. border: none;
  274. outline: none;
  275. color: rgb(255, 255, 255);
  276. background: #111;
  277. cursor: pointer;
  278. position: relative;
  279. z-index: 0;
  280. border-radius: 10px;
  281. user-select: none;
  282. -webkit-user-select: none;
  283. touch-action: manipulation;
  284. }
  285.  
  286. .jbtn:before {
  287. content: "";
  288. background: linear-gradient(
  289. 45deg,
  290. #ff0000,
  291. #ff7300,
  292. #fffb00,
  293. #48ff00,
  294. #00ffd5,
  295. #002bff,
  296. #7a00ff,
  297. #ff00c8,
  298. #ff0000
  299. );
  300. position: absolute;
  301. top: -2px;
  302. left: -2px;
  303. background-size: 400%;
  304. z-index: -1;
  305. filter: blur(5px);
  306. -webkit-filter: blur(5px);
  307. width: calc(100% + 4px);
  308. height: calc(100% + 4px);
  309. animation: glowing-jbtn 20s linear infinite;
  310. transition: opacity 0.3s ease-in-out;
  311. border-radius: 10px;
  312. }
  313.  
  314. @keyframes glowing-jbtn {
  315. 0% {
  316. background-position: 0 0;
  317. }
  318. 50% {
  319. background-position: 400% 0;
  320. }
  321. 100% {
  322. background-position: 0 0;
  323. }
  324. }
  325.  
  326. .jbtn:after {
  327. z-index: -1;
  328. content: "";
  329. position: absolute;
  330. width: 100%;
  331. height: 100%;
  332. background: #222;
  333. left: 0;
  334. top: 0;
  335. border-radius: 10px;
  336. }
  337. `;
  338. document.head.appendChild(style);
  339. }
  340.  
  341. function monitoringSilder()
  342. {
  343. const mutationObserver = new MutationObserver(mutationsList => {
  344. mutationsList.forEach(mutation => {
  345. if (mutation.type === 'attributes' && mutation.attributeName === 'data-e2e-vid') {
  346. const targetNode = mutation.target;
  347. console.log(targetNode);
  348. dealwith(targetNode);
  349. }
  350.  
  351. });
  352. });
  353. let listener = setInterval(function() {
  354. if (document.querySelector('#slidelist')) {
  355. mutationObserver.observe(document.querySelector('#slidelist'), { childList: true, subtree: true,attributes:true});
  356. clearInterval(listener);
  357. }
  358. }, 500);
  359.  
  360. function dealwith(activeVideoDom){
  361. var downloadBtn = activeVideoDom.querySelector('#douyin_download_by_jeffc');
  362. if (!downloadBtn) {
  363. downloadBtn = document.createElement('div');
  364. downloadBtn.setAttribute("data-index", "10");
  365. downloadBtn.id = "douyin_download_by_jeffc";
  366. downloadBtn.innerHTML = "DOWNLOAD";
  367. downloadBtn.style = 'cursor:pointer;width: 60px;text-align: center;font-size: 14px;color: rgba(255, 255, 255,0.75);line-height:20px;margin-right: 30px;';
  368. downloadBtn.addEventListener('click', function() {
  369. downloadVideo(activeVideoDom);
  370. });
  371. let listenerActiveVideo = setInterval(function() {
  372. let targetNode = currentPageType == currentPageType.Normal ? activeVideoDom.querySelector("xg-right-grid") : activeVideoDom.parentNode.querySelector("xg-right-grid") ;
  373. if (targetNode ) {
  374. if(!targetNode.querySelector("#douyin_download_by_jeffc"))
  375. {
  376. targetNode.appendChild(downloadBtn);
  377. }
  378. clearInterval(listenerActiveVideo);
  379. }
  380. }, 500);
  381. }
  382. }
  383. }
  384.  
  385. // 页面加载完成后开始动态监测
  386. window.onload = function() {
  387. dynamicMonitoring();
  388. };
  389.  
  390. // 监听键盘按下事件
  391. document.addEventListener("keydown", function(event) {
  392. // 获取按下的键的键码
  393. var keyCode = event.keyCode;
  394.  
  395. if(keyCode == 90)
  396. {
  397. var target_dom = document.querySelectorAll("#douyin_download_by_jeffc");
  398. if(target_dom)
  399. {
  400. target_dom[target_dom.length-1].click();
  401. }
  402. }
  403. });
  404.  
  405. })();

QingJ © 2025

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