网页音乐播放自动下载音乐文件

在酷狗音乐播放页面下载所听歌曲到本地,仅在chrome下测试通过,当第一次打开播放界面时,如果仅播放一首歌,那么是通过hash变化触发下载,也就是在列表页再次点击新的一首歌时会触发下载,试听音乐不下载,不会重复下载

  1. /* jshint esversion: 8 */
  2. // ==UserScript==
  3. // @name 网页音乐播放自动下载音乐文件
  4. // @namespace javyliu
  5. // @version 1.2
  6. // @description 在酷狗音乐播放页面下载所听歌曲到本地,仅在chrome下测试通过,当第一次打开播放界面时,如果仅播放一首歌,那么是通过hash变化触发下载,也就是在列表页再次点击新的一首歌时会触发下载,试听音乐不下载,不会重复下载
  7. // @author javy_liu
  8. // @include *://*.kugou.com/*
  9. // @include *://*.xiami.com/*
  10. // @include *://music.163.com/*
  11. // @include *://y.qq.com/*
  12.  
  13. // @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
  14. // @grant GM_download
  15. // @grant GM_openInTab
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant GM_xmlhttpRequest
  19. // @grant unsafeWindow
  20. // @grant GM_setClipboard
  21. // @grant GM_getResourceURL
  22. // @grant GM_getResourceText
  23. // @grant GM_notification
  24. // @grant GM_deleteValue
  25. // @license GPL License
  26.  
  27.  
  28. // @connect *
  29.  
  30. // ==/UserScript==
  31. // kugou.com 音乐下载
  32. (function() {
  33. 'use strict';
  34. if(unsafeWindow.jQuery){
  35. $ = unsafeWindow.jQuery;
  36. }
  37.  
  38. //从url中提取指定key的值
  39. function getHashParams(key) {
  40. var arr = location.hash.replace("#", "").split("&"), keyValue = "";
  41. for (var i = 0;i<arr.length;i++) {
  42. if (arr[i].split("=")[0] == key) {
  43. keyValue = arr[i].split("=")[1];
  44. break;
  45. }
  46. }
  47. return keyValue;
  48. }
  49.  
  50. //通过文档对像得到react组件对像,虾米用
  51. function get_react_com(dom, traverseUp = 0) {
  52. const key = Object.keys(dom).find(key=>key.startsWith("__reactInternalInstance$"));
  53. const domFiber = dom[key];
  54. if (domFiber == null) return null;
  55. // react <16
  56. if (domFiber._currentElement) {
  57. let compFiber = domFiber._currentElement._owner;
  58. for (let i = 0; i < traverseUp; i++) {
  59. compFiber = compFiber._currentElement._owner;
  60. }
  61. return compFiber._instance;
  62. }
  63. // react 16+
  64. const GetCompFiber = fiber=>{
  65. //return fiber._debugOwner; // this also works, but is __DEV__ only
  66. let parentFiber = fiber.return;
  67. while (typeof parentFiber.type == "string") {
  68. parentFiber = parentFiber.return;
  69. }
  70. return parentFiber;
  71. };
  72. let compFiber = GetCompFiber(domFiber);
  73. for (let i = 0; i < traverseUp; i++) {
  74. compFiber = GetCompFiber(compFiber);
  75. }
  76. return compFiber.stateNode;
  77. }
  78.  
  79. //封装通知
  80. function notify(txt,title="通知"){
  81. GM_notification({
  82. title: txt,
  83. text: title,
  84. // highlight: true,
  85. timeout: 5000,
  86. ondone: function(){
  87. console.log("关闭了通知");
  88. }
  89. });
  90. }
  91.  
  92. //下载url指定的资源,并指定文件名
  93. function promise_download(res_url, file_name) {
  94. return new Promise(function (resolve, reject) {
  95. GM_download({
  96. url: res_url,
  97. name: file_name,
  98. onload: function () {
  99. resolve(`${file_name} 下载完成`);
  100. },
  101. onerror: function (error) {
  102. reject(`${file_name} 下载失败 ${error.error}`);
  103. }
  104. });
  105. });
  106.  
  107. }
  108.  
  109. //for kugou url fetch
  110. function promise_fetch(req_url){
  111. return new Promise(function(resolve, reject){
  112. GM_xmlhttpRequest({
  113. method: "GET",
  114. url: req_url,
  115. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  116. responseType: "json",
  117. onload: function(r){
  118. // console.log(r);
  119. if(r.readyState == 4){
  120. var res = r.response.data;
  121. console.log("mp3地址", res.play_url);
  122. resolve(res);
  123. }
  124. },
  125. onerror: function(err){
  126. console.error("请求地址失败", err);
  127. reject("请求地址失败");
  128. }
  129. });
  130. });
  131. }
  132.  
  133.  
  134. //is_free_part 为1时为试听, 不下载试听
  135. //传入一个对像数组[{hash:xxx, encode_album_audio_id:xxx}]
  136. //for of 内部 break, return 会跳出循环.
  137. let list = GM_getValue("download_list") || {};
  138.  
  139.  
  140. let base_url = location.href;
  141. const KuGou = 'kugou.com';
  142. const XiaMi = 'xiami.com';
  143. const Netease = '163.com';
  144. const Qq = 'y.qq.com';
  145. let reg = new RegExp(`${KuGou}|${XiaMi}|${Netease}|${Qq}`.replace(/\./g, "\\."));
  146. let match_domain = base_url.match(reg)[0];
  147.  
  148. let exec_in_kugou = function(){
  149. let play_list = JSON.parse($.jStorage.get("k_play_list"));
  150. //播放页面第一次打开为列表时,批量下载列表,否则通过监听hash地址变化触发下载
  151. if(play_list && play_list.length > 1){
  152. console.log("有列表:", play_list);
  153. download_kugou(play_list).then(function(){
  154. console.log("列表中的已下载完,增加单曲监听");
  155. window.addEventListener("hashchange", function(ev){
  156. download_kugou([{'Hash': ev.target.Hash, 'encode_album_audio_id': ev.target.encode_album_audio_id}]);
  157. });
  158. });
  159. }else{
  160. window.addEventListener("hashchange", function(ev){
  161. download_kugou([{'Hash': ev.target.Hash, 'encode_album_audio_id': ev.target.encode_album_audio_id}])
  162. .then(function(_return){
  163. //单曲时如果列表不只一首,则播放下一首
  164. if(_return == 1 && (JSON.parse($.jStorage.get("k_play_list"))).length > 1){
  165. console.log("跳过--------------");
  166. $("#next").trigger("click");//不播放试听音乐
  167. }
  168. });
  169. });
  170. }
  171. };
  172.  
  173. let download_kugou = async function(ary_obj){
  174. for (var obj of ary_obj) {
  175. let _hash = obj.Hash;
  176. let _album_id = obj.encode_album_audio_id;
  177.  
  178. let req_url = "https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash=" + _hash + "&encode_album_audio_id=" + _album_id + "&dfid=&mid=&platid=4";
  179. // https://wwwapi.kugou.com/yy/index.php?r=play/getdata&dfid=2mSxKE1FMBgg4N2TLT4Y3yic&appid=1014&mid=6bc5c6880bc2ce18c248fb6b88eb0ccc&platid=4&encode_album_audio_id=6eliwgeb&_=1677045356215
  180. console.log("请求地址:", req_url);
  181. try {
  182. //已下载的不下载,也不提醒
  183. if (list[_hash]) {
  184. console.log("已下载",obj);
  185. continue;
  186. }
  187. var res = await promise_fetch(req_url);
  188. //试听音乐也加到已加载列表
  189. if (res.is_free_part) {
  190. list[res.hash] = 1;
  191. var txt = `${res.audio_name}为试听音乐`;
  192. console.log(txt);
  193. notify(txt);
  194. //如果是试听音乐且是单曲播放时,则返回1,利于后期处理
  195. if(ary_obj.length == 1){
  196. return 1;
  197. }
  198. continue;
  199. }
  200. var extname = res.play_url.match(/(\.[\w]+?$)/)[1];
  201. await promise_download(res.play_url, res.audio_name + extname);
  202. list[res.hash] = 1;
  203. GM_setValue("download_list", list);
  204. console.log(res);
  205. notify(res);
  206. } catch (error) {
  207. console.error(error);
  208. notify(error);
  209. }
  210. }
  211. };
  212.  
  213. let exec_in_xiami = function(){
  214. let play_box_com = get_react_com($(".player")[0]);
  215. let inject_methods = ['play','playMusic','playNext','playPrev'];
  216. let ori_mtds = [];
  217. //注入方法
  218. for (let i=0,len=inject_methods.length; i<len; i++) {
  219. ori_mtds[i] = play_box_com[inject_methods[i]];
  220. play_box_com[inject_methods[i]] = function() {
  221. ori_mtds[i].apply(this,arguments);
  222. xiami_download(this);
  223. }.bind(play_box_com);
  224. }
  225. };
  226.  
  227. let xiami_download = function(cobj){
  228. console.count("--------xiami_download called----");
  229. try {
  230. let song_id = cobj.audio.currentSrc.match(/fn=(\d+)_/)[1];
  231. if (!song_id) {
  232. throw new Error("歌曲id不存在");
  233. }
  234.  
  235. if(list[`xm_${song_id}`]){
  236. console.log(`${song_id} 已下载`);
  237. return;
  238. }
  239.  
  240. let song_item = cobj.props.activePlayList.find((item) => {
  241. return item.id == song_id;
  242. });
  243. if (!song_item) {
  244. throw new Error("歌曲不存在");
  245. }
  246.  
  247. console.log(song_item.detail.songName);
  248.  
  249. let play_info = song_item.playInfo.find((item)=>{
  250. return item.listenFile && item.listenFile.length > 0;
  251. });
  252.  
  253. console.log(play_info);
  254. let song_name = `${song_item.detail.songName.replace(/\s+/g,"_")}_${song_item.detail.artistName}.${play_info.format}`;
  255.  
  256. promise_download(play_info.listenFile, song_name).then(res => {
  257. list[`xm_${song_id}`] = 1;
  258. GM_setValue("download_list", list);
  259. notify(`${song_name} 下载完成!` );
  260. });
  261. } catch (error) {
  262. console.error(error);
  263. }
  264.  
  265. /*
  266. // k1=$0.__reactInternalInstance$twjwzf1adie.return.stateNode;
  267. // p1=k1.play;
  268. // k1.play=function(){p1.apply(this,arguments);console.log("------play",arguments)}.bind(k1);
  269. // p2=k1.playMusic;
  270. // k1.playMusic=function(){p2.apply(this,arguments);console.log("------playMusic",arguments)}.bind(k1);
  271. // p3=k1.playNext ;
  272. // k1.playNext=function(){p3.apply(this,arguments);console.log("------playNext",arguments)}.bind(k1);
  273. // p4=k1.playPrev;
  274. // k1.playPrev=function(){p4.apply(this,arguments);console.log("------playPrev",arguments)}.bind(k1);
  275.  
  276. // p5=k1.props.playMusic;
  277. // k1.props.playMusic=function(){p5.apply(this,arguments);console.log("------k1.props.playMusic",arguments)}.bind(k1.props);
  278.  
  279. // p6=k1.props.setPlayListPlayInfo;
  280. // k1.props.setPlayListPlayInfo=function(){p6.apply(this,arguments);console.log("------k1.props.setPlayListPlayInfo",arguments)}.bind(k1.props);
  281.  
  282. // p7=k1.props.setPlayListDetail;
  283. // k1.props.setPlayListDetail=function(){p7.apply(this,arguments);console.log("------k1.props.setPlayListDetail",arguments)}.bind(k1.props);
  284. */
  285. };
  286.  
  287. let exec_in_netease = function(){
  288. let ori_change = unsafeWindow.onplaychange;
  289. unsafeWindow.onplaychange = function(){
  290. ori_change.apply(this,arguments);
  291. console.log("----------onplaychange");
  292. // setTimeout(() => {
  293. netease_download();
  294. // }, 1000);
  295. }.bind(unsafeWindow);
  296. };
  297.  
  298. let netease_download = function(){
  299. try {
  300. if(!unsafeWindow.player) return 0;
  301.  
  302. let url = unsafeWindow.cAi5n;
  303. let playinfo = unsafeWindow.player.getPlaying();
  304.  
  305. console.log("--------播放信息:",playinfo);
  306. if(list[`netease_${playinfo.track.id}`]){
  307. console.log(`${playinfo.track.id} 已下载`);
  308. return;
  309. }
  310.  
  311. let extname = url.match(/\.([\w]+?$)/)[1];
  312. let articles = playinfo.track.artists.map((item) => item.name).join("_");
  313. let song_name = `${playinfo.track.name}_${articles}.${extname}`;
  314.  
  315. promise_download(url, song_name).then(res => {
  316. list[`netease_${playinfo.track.id}`] = 1;
  317. GM_setValue("download_list", list);
  318. notify(`${song_name} 下载完成!` );
  319. })
  320. .catch(error => {
  321. console.error(error);
  322. });
  323. } catch (error) {
  324. console.error(error);
  325. }
  326. };
  327.  
  328. //注入的getSongUrl方法会被调用多次,所以需要限制下,如果下载失败需刷新后才会再次触发下载
  329. let pre_download_list = {};
  330. let exec_in_qq = function(){
  331. let ori_mtd = unsafeWindow.MUSIC.util.getSongUrl;
  332. unsafeWindow.MUSIC.util.getSongUrl = function(arg){
  333. let res = ori_mtd.call(this,arg);
  334. setTimeout(() => void qq_download(arg), 1000 );
  335. return res;
  336. };
  337. };
  338.  
  339. let qq_download = function(playinfo){
  340. try {
  341. if( pre_download_list[playinfo.songid]==1) return 0;
  342. if(!playinfo.songid) return 0;
  343. if(list[`qq_${playinfo.songid}`]){
  344. console.log(`${playinfo.songid} 已下载`);
  345. return 0;
  346. }
  347. //预下载标记
  348. pre_download_list[playinfo.songid]=1;
  349.  
  350. let song_url = playinfo.playUrl || $("audio")[0].src;
  351.  
  352. // console.log("-------------需下载对像地址----------------");
  353. // console.log("1:",playinfo.playUrl);
  354. // console.log("2:",song_url);
  355.  
  356. let extname = song_url.match(/\.(\w+)\?/)[1];
  357. let song_name = `${playinfo.songname}_${playinfo.singername}.${extname}`;
  358.  
  359. promise_download(song_url, song_name).then(res => {
  360. list[`qq_${playinfo.songid}`] = 1;
  361. pre_download_list[playinfo.songid] = 0;
  362. GM_setValue("download_list", list);
  363. notify(`${song_name} 下载完成!` );
  364. })
  365. .catch(error => {
  366. pre_download_list[playinfo.songid] = 0;
  367. console.error(error);
  368. });
  369. } catch (error) {
  370. pre_download_list[playinfo.songid] = 0;
  371. console.error(error);
  372. }
  373. };
  374.  
  375.  
  376. console.log("----------------------",match_domain);
  377. switch (match_domain) {
  378. case KuGou:
  379. exec_in_kugou();
  380. break;
  381. case XiaMi:
  382. exec_in_xiami();
  383. break;
  384. case Netease:
  385. exec_in_netease();
  386. break;
  387. case Qq:
  388. setTimeout(()=>{
  389. exec_in_qq();
  390. },2000);
  391. break;
  392. default:
  393. break;
  394. }
  395. /*
  396. music_title[0]="163.com"
  397. music_title[1]= "y.qq.com"
  398. music_title[2]= "kugou.com"
  399. music_title[3]= "kuwo.cn"
  400. music_title[4]= "xiami.com"
  401. music_title[5]= "taihe.com"
  402. music_title[6]= "1ting.com"
  403. music_title[7]= "migu.cn"
  404. music_title[8]= "qingting.fm"
  405. music_title[9]= "lizhi.fm"
  406. music_title[10]= "ximalaya.com"
  407. */
  408.  
  409.  
  410. //头部添加清除下载记录按钮
  411. $("body").prepend(`<button id='clear_download_list' style="position:absolute;left:0px;top:0;z-index:1000;background-color: green;padding:10px;color:white;opacity: 0.7;">clear download list</button>`);
  412. $("#clear_download_list").on("click", function(){
  413. GM_deleteValue("download_list");
  414. console.log("list:",GM_getValue("download_list"));
  415. notify("清除下载记录成功");
  416. });
  417.  
  418. })();

QingJ © 2025

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