喜马拉雅专辑下载器

可能是你见过最丝滑的喜马拉雅下载器啦!登录后支持VIP音频下载,支持专辑批量下载,支持添加编号,链接导出、调用aria2等功能,直接下载M4A,MP3、MP4文件。

  1. // ==UserScript==
  2. // @name 喜马拉雅专辑下载器
  3. // @version 1.3.1
  4. // @description 可能是你见过最丝滑的喜马拉雅下载器啦!登录后支持VIP音频下载,支持专辑批量下载,支持添加编号,链接导出、调用aria2等功能,直接下载M4A,MP3、MP4文件。
  5. // @author Priate
  6. // @match *://www.ximalaya.com/*
  7. // @grant GM_xmlhttpRequest
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_addStyle
  11. // @grant GM_setClipboard
  12. // @grant GM_download
  13. // @icon https://www.ximalaya.com/favicon.ico
  14. // @require https://registry.npmmirror.com/vue/2.7.16/files/dist/vue.min.js
  15. // @require https://registry.npmmirror.com/sweetalert/2.1.2/files/dist/sweetalert.min.js
  16. // @require https://registry.npmmirror.com/jquery/3.2.1/files/dist/jquery.min.js
  17. // @require https://greasyfork.org/scripts/435476-priatelib/code/PriateLib.js?version=1202493
  18. // @require https://registry.npmmirror.com/ajax-hook/2.0.3/files/dist/ajaxhook.min.js
  19. // @require https://registry.npmmirror.com/crypto-js/4.1.1/files/crypto-js.js
  20. // @supportURL https://greasyfork.org/zh-CN/scripts/435495/feedback
  21. // @homepageURL https://greasyfork.org/zh-CN/scripts/435495
  22. // @contributionURL https://afdian.net/@cyberubbish
  23. // @license MIT
  24. // @namespace https://greasyfork.org/users/219866
  25. // ==/UserScript==
  26.  
  27. (function() {
  28. 'use strict';
  29.  
  30. function initSetting() {
  31. var setting;
  32. if (!GM_getValue('priate_script_xmly_data')) {
  33. GM_setValue('priate_script_xmly_data', {
  34. // 多线程下载
  35. multithreading: false,
  36. left: 20,
  37. top: 100,
  38. manualMusicURL: null,
  39. quality: 1,
  40. showNumber: true,
  41. numberOffset: 0,
  42. pageSize: 30,
  43. aria2: "ws://127.0.0.1:16800/jsonrpc"
  44. })
  45. }
  46. setting = GM_getValue('priate_script_xmly_data')
  47. //后期添加内容
  48. if (!setting.quality) setting.quality = 1;
  49. // 暂时统一为高清音质
  50. setting.quality = 1
  51. if (setting.showNumber === null) setting.showNumber = true;
  52. if (!setting.numberOffset) setting.numberOffset = 0;
  53. if (!setting.pageSize) setting.pageSize = 30;
  54. if (!setting.aria2) setting.aria2 = "ws://127.0.0.1:16800/jsonrpc"
  55. GM_setValue('priate_script_xmly_data', setting)
  56. }
  57.  
  58. // 手动获取音频地址功能
  59. function manualGetMusicURL() {
  60. let windowID = getRandStr("1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", 100)
  61.  
  62. function getRandStr(chs, len) {
  63. let str = "";
  64. while (len--) {
  65. str += chs[parseInt(Math.random() * chs.length)];
  66. }
  67. return str;
  68. }
  69. (function() {
  70. let playOriginal = HTMLAudioElement.prototype.play;
  71.  
  72. function play() {
  73. let link = this.src;
  74. window.top.postMessage(Array("audioVideoCapturer", link, windowID, "link"), "*");
  75. return playOriginal.call(this);
  76. }
  77. HTMLAudioElement.prototype.play = play;
  78. HTMLAudioElement.prototype.play.toString = HTMLAudioElement.prototype.play.toString.bind(playOriginal);
  79. })();
  80. if (window.top == window) {
  81. window.addEventListener("message", function(event) {
  82. if (event.data[0] == "audioVideoCapturer") {
  83. var setting = GM_getValue('priate_script_xmly_data')
  84. setting.manualMusicURL = event.data[1]
  85. GM_setValue('priate_script_xmly_data', setting)
  86. }
  87. });
  88. }
  89. }
  90.  
  91. manualGetMusicURL()
  92.  
  93. function injectDiv() {
  94. var priate_script_div = document.createElement("div")
  95. priate_script_div.innerHTML = `
  96. <div id="priate_script_div">
  97. <b style='font-size:30px; font-weight:300; margin: 10px 20px'>喜马拉雅专辑下载器</b>
  98. <p id='priate_script_setting' style='margin: 0 0'>
  99. ❤️ by <a @click='openDonate' style='color:#337ab7'>Priate</a> |
  100. v <a href="//greasyfork.org/zh-CN/scripts/435495" target="_blank" style='color:#ff6666'>{{version}}</a> |
  101. 音质 : <a @click='changeQuality' :style='"color:" + qualityColor'>{{qualityStr}}</a>
  102. <br>
  103. 编号 : <a @click='switchShowNumber' :style='"color:" + (setting.showNumber ? "#00947e" : "#CC0F35")'> {{ setting.showNumber ? "开启" : "关闭"}} </a> -
  104. <a @click='addNumberOffset' @contextmenu.prevent='subNumberOffset' :style='"color:" + (setting.showNumber ? "#3311AA" : "#CC0F35")'> {{ setting.numberOffset }} </a> |
  105. 数量 : <a @click='changePageSize' style='color:#3311AA'> {{ setting.pageSize }} </a> |
  106. <a style='color:#ff6666' @click='clearMusicData'>❌</a>
  107. </p>
  108. <button v-show="!isDownloading" @click="loadMusic">{{filterData.length > 0 ? '重载数据' : '加载数据'}}</button>
  109. <button id='readme' @click="downloadAllMusics" v-show="!isDownloading && (musicList.length > 0)">下载所选</button>
  110. <button @click="exportAllMusicURL" v-show="!isDownloading && (musicList.length > 0)">导出数据 <b v-show="copyMusicURLProgress">{{copyMusicURLProgress}}%</b></button>
  111. <button @click="cancelDownload" v-show="isDownloading">取消下载</button>
  112. </br>
  113. <table v-show="filterData.length > 0">
  114. <thead><tr><th><a style='color:#337ab7' @click='selectAllMusic'>全选</a></th><th>标题</th><th>操作</th></tr></thead>
  115. <tbody id="priate_script_table">
  116. <tr v-for="(item, index) in filterData" :key="index">
  117. <td><input class="checkMusicBox" v-model="musicList" :value='item' type="checkbox" :disabled="item.isDownloaded || isDownloading"></td>
  118. <td><a @click="openMusicURL(item)" style='color:#337ab7'>{{item.title}}</a></td>
  119. <td>
  120. <a v-show="!item.isDownloading && !item.isDownloaded && !isDownloading" style='color:#993333' @click="downloadMusic(item)">下载</a>
  121. <a v-show="isDownloading && !item.isDownloading && !item.isDownloaded" style='color:gray'>等待中</a>
  122. <a v-show="item.isDownloading" style='color:#C01D07'>{{item.progress}}</a>
  123. <a v-show="item.isDownloaded" style='color:#00947E'>已下载</a>
  124. <a v-show="item.isFailued" style='color:red'>下载失败</a> |
  125. <a :style="'color:' + (item.url ? '#00947E' : '#993333')" @click="copyMusic(item)">地址</a></td>
  126. </tr>
  127. </tbody>
  128. </table>
  129. </div>
  130. `
  131. GM_addStyle(`
  132. #priate_script_div{
  133. font-size : 15px;
  134. position: fixed;
  135. background-color: rgb(240, 223, 175);
  136. color : #660000;
  137. text-align : center;
  138. padding: 10px;
  139. z-index : 9999;
  140. border-radius : 20px;
  141. border:2px solid #660000;
  142. font-weight: 300;
  143. -webkit-text-stroke: 0.5px;
  144. text-stroke: 0.5px;
  145. box-shadow: 5px 15px 15px rgba(0,0,0,0.4);
  146. user-select : none;
  147. -webkit-user-select : none;
  148. -moz-user-select : none;
  149. -ms-user-select:none;
  150. }
  151. #priate_script_div:hover{
  152. box-shadow: 5px 15px 15px rgba(0,0,0,0.8);
  153. transition: box-shadow 0.3s;
  154. }
  155. .priate_script_hide{
  156. padding: 0 !important;
  157. border:none !important;
  158. }
  159. a{
  160. cursor : pointer;
  161. text-decoration : none;
  162. }
  163. /*表格样式*/
  164. #priate_script_div table{
  165. text-align: center;
  166. // border:2px solid #660000;
  167. margin: 5px auto;
  168. padding: 2px;
  169. border-collapse: collapse;
  170. display: block;
  171. height : 400px;
  172. overflow-y: scroll;
  173. }
  174. /*表格框样式*/
  175. #priate_script_div td{
  176. border:2px solid #660000;
  177. padding: 8px 12px;
  178. max-width : 300px;
  179. word-wrap : break-word;
  180. }
  181. /*表头样式*/
  182. #priate_script_div th{
  183. border:2px solid #660000;
  184. padding: 8px 12px;
  185. font-weight: 300;
  186. -webkit-text-stroke: 0.5px;
  187. text-stroke: 0.5px;
  188. }
  189.  
  190. /*脚本按钮样式*/
  191. #priate_script_div button{
  192. display: inline-block;
  193. border-radius: 4px;
  194. border: 1px solid #660000;
  195. background-color: transparent;
  196. color: #660000;
  197. text-decoration: none;
  198. padding: 5px 10px;
  199. margin : 5px 10px;
  200. font-weight: 300;
  201. -webkit-text-stroke: 0.5px;
  202. text-stroke: 0.5px;
  203. }
  204. /*脚本按钮悬浮样式*/
  205. #priate_script_div button:hover{
  206. cursor : pointer;
  207. color: rgb(240, 223, 175);
  208. background-color: #660000;
  209. transition: background-color 0.2s;
  210. }
  211. /*设置区域 p 标签*/
  212. #priate_script_setting{
  213. user-select : none;
  214. -webkit-user-select : none;
  215. -moz-user-select : none;
  216. -ms-user-select:none;
  217. }
  218. /*输入框样式*/
  219. #priate_script_div textarea{
  220. height : 50px;
  221. width : 200px;
  222. background-color: #fff;
  223. border:1px solid #000000;
  224. padding: 4px;
  225. }
  226. /*swal按钮*/
  227. .swal-button--1{
  228. background-color: #FFFAEB !important;
  229. color: #946C00;
  230. }
  231. .swal-button--2{
  232. background-color: #ebfffc !important;
  233. color: #00947e;
  234. }
  235. .swal-button--3{
  236. background-color: #ECF6FD !important;
  237. color: #55ACEE;
  238. }
  239. .checkMusicBox{
  240. transform: scale(1.7,1.7);
  241. cursor: pointer;
  242. }
  243. `);
  244. document.querySelector("html").appendChild(priate_script_div)
  245. var setting = GM_getValue('priate_script_xmly_data')
  246. document.getElementById("priate_script_div").style.left = (setting.left || 20) + "px";
  247. document.getElementById("priate_script_div").style.top = (setting.top || 100) + "px";
  248. }
  249.  
  250. function dragFunc(id) {
  251. var Drag = document.getElementById(id);
  252. var setting = GM_getValue('priate_script_xmly_data')
  253. Drag.onmousedown = function(event) {
  254. var ev = event || window.event;
  255. event.stopPropagation();
  256. var disX = ev.clientX - Drag.offsetLeft;
  257. var disY = ev.clientY - Drag.offsetTop;
  258. document.onmousemove = function(event) {
  259. var ev = event || window.event;
  260. setting.left = ev.clientX - disX
  261. Drag.style.left = setting.left + "px";
  262. setting.top = ev.clientY - disY
  263. Drag.style.top = setting.top + "px";
  264. Drag.style.cursor = "move";
  265. GM_setValue('priate_script_xmly_data', setting)
  266. };
  267. };
  268. Drag.onmouseup = function() {
  269. document.onmousemove = null;
  270. this.style.cursor = "default";
  271. };
  272. };
  273. // 初始化音质修改
  274. function initQuality() {
  275. ah.proxy({
  276. onRequest: (config, handler) => {
  277. handler.next(config);
  278. },
  279. onError: (err, handler) => {
  280. handler.next(err)
  281. },
  282. onResponse: (response, handler) => {
  283. const setting = GM_getValue('priate_script_xmly_data')
  284. // hook返回数据
  285. if (response.config.url.indexOf("mobile.ximalaya.com/mobile-playpage/track/v3/baseInfo") != -1) {
  286. const setting = GM_getValue('priate_script_xmly_data')
  287. const data = JSON.parse(response.response)
  288. const playUrlList = data.trackInfo.playUrlList
  289. var replaceUrl;
  290. for (var num = 0; num < playUrlList.length; num++) {
  291. var item = playUrlList[num]
  292. if (item.qualityLevel == setting.quality) {
  293. replaceUrl = item.url
  294. break
  295. }
  296. }
  297. replaceUrl && playUrlList.forEach((item) => {
  298. item.url = replaceUrl
  299. })
  300. response.response = JSON.stringify(data)
  301. }
  302. // hook普通音频获取高品质,实际上只需删除获取到的src即可
  303. if (setting.quality == 2 && response.config.url.indexOf("www.ximalaya.com/revision/play/v1/audio") != -1) {
  304. const setting = GM_getValue('priate_script_xmly_data')
  305. var resp = JSON.parse(response.response)
  306. var data = resp.data
  307. delete data.src
  308. response.response = JSON.stringify(resp)
  309. }
  310. handler.next(response)
  311. }
  312. })
  313. unsafeWindow.XMLHttpRequest = XMLHttpRequest
  314. }
  315. // 修改翻页大小
  316. function initPageSize() {
  317. const originFetch = fetch;
  318. const setting = GM_getValue('priate_script_xmly_data')
  319. window.unsafeWindow.fetch = (url, options) => {
  320. if (url.indexOf('/revision/album/v1/getTracksList') != -1) {
  321. url = url.replace('pageSize=30', `pageSize=${setting.pageSize}`)
  322. }
  323. return originFetch(url, options).then(async (response) => {
  324. return response;
  325. });
  326. };
  327. }
  328. //初始化脚本设置
  329. initSetting()
  330. //注入脚本div
  331. injectDiv()
  332. // 初始化音质修改
  333. initQuality()
  334. // 修改翻页大小
  335. initPageSize()
  336.  
  337. // 第一种获取musicURL的方式,任意用户均可获得,不可获得VIP音频
  338. async function getSimpleMusicURL1(item) {
  339. var res = null
  340. if (item.url) {
  341. res = item.url
  342. } else {
  343. const timestamp = Date.parse(new Date());
  344. var url = `https://mobwsa.ximalaya.com/mobile-playpage/playpage/tabs/${item.id}/${timestamp}`
  345. $.ajax({
  346. type: 'get',
  347. url: url,
  348. async: false,
  349. dataType: "json",
  350. success: function(resp) {
  351. if (resp.ret === 0) {
  352. const setting = GM_getValue('priate_script_xmly_data')
  353. const trackInfo = resp.data.playpage.trackInfo;
  354. if (setting.quality == 0) {
  355. res = trackInfo.playUrl32
  356. } else if (setting.quality == 1) {
  357. res = trackInfo.playUrl64
  358. }
  359. // res = res || trackInfo.downloadUrl
  360. }
  361. }
  362. });
  363. }
  364. return res
  365. }
  366. // 第二种获取musicURL的方式,任意用户均可获得,不可获得VIP音频
  367. async function getSimpleMusicURL2(item) {
  368. var res = null
  369. if (item.url) {
  370. res = item.url
  371. } else {
  372. var url = `https://www.ximalaya.com/revision/play/v1/audio?id=${item.id}&ptype=1`
  373. $.ajax({
  374. type: 'get',
  375. url: url,
  376. async: false,
  377. dataType: "json",
  378. success: function(resp) {
  379. if (resp.ret == 200) res = resp.data.src;
  380. }
  381. });
  382. }
  383. return res
  384. }
  385.  
  386. //获取任意音频方法
  387. async function getAllMusicURL1(item) {
  388. var res = null
  389. var setting;
  390. if (item.url) {
  391. res = item.url
  392. } else {
  393. const all_li = document.querySelectorAll('.sound-list>ul li');
  394. for (var num = 0; num < all_li.length; num++) {
  395. var li = all_li[num]
  396. const item_a = li.querySelector('a');
  397. const id = item_a.href.split('/')[item_a.href.split('/').length - 1]
  398. if (id == item.id) {
  399. li.querySelector('div.all-icon').click()
  400. while (!res) {
  401. await Sleep(1)
  402. setting = GM_getValue('priate_script_xmly_data')
  403. res = setting.manualMusicURL
  404. }
  405. setting.manualMusicURL = null
  406. GM_setValue('priate_script_xmly_data', setting)
  407. li.querySelector('div.all-icon').click()
  408. break
  409. }
  410. }
  411. }
  412. if (!res && item.isSingle) {
  413. document.querySelector('div.play-btn').click()
  414. while (!res) {
  415. await Sleep(1)
  416. setting = GM_getValue('priate_script_xmly_data')
  417. res = setting.manualMusicURL
  418. }
  419. setting.manualMusicURL = null
  420. GM_setValue('priate_script_xmly_data', setting)
  421. document.querySelector('div.play-btn').click()
  422. }
  423. return res
  424. }
  425. // 通过解密数据的方式获取 URL
  426. async function getAllMusicURL2(item) {
  427. function decrypt(t) {
  428. return CryptoJS.AES.decrypt({
  429. ciphertext: CryptoJS.enc.Base64url.parse(t)
  430. }, CryptoJS.enc.Hex.parse('aaad3e4fd540b0f79dca95606e72bf93'), {
  431. mode: CryptoJS.mode.ECB,
  432. padding: CryptoJS.pad.Pkcs7
  433. }).toString(CryptoJS.enc.Utf8)
  434. }
  435. var res = null
  436. if (item.url) {
  437. res = item.url
  438. } else {
  439. const timestamp = Date.parse(new Date());
  440. var url = `https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/${timestamp}?device=web&trackId=${item.id}`
  441. $.ajax({
  442. type: 'get',
  443. url: url,
  444. async: false,
  445. dataType: "json",
  446. success: function(resp) {
  447. try {
  448. res = decrypt(resp.trackInfo.playUrlList[0].url)
  449. } catch (e) {
  450. console.log("解密错误")
  451. res = null
  452. }
  453. }
  454. });
  455. }
  456. return res
  457. }
  458.  
  459. // 处理数据等逻辑
  460. var vm = new Vue({
  461. el: '#priate_script_div',
  462. data: {
  463. version: "1.3.1",
  464. copyMusicURLProgress: 0,
  465. setting: GM_getValue('priate_script_xmly_data'),
  466. data: [],
  467. musicList: [],
  468. isDownloading: false,
  469. cancelDownloadObj: null,
  470. stopDownload: false,
  471. },
  472. methods: {
  473. loadMusic() {
  474. const whiteList = ['sound', 'album']
  475. const type = location.pathname.split('/')[location.pathname.split('/').length - 2]
  476. if (whiteList.indexOf(type) < 0) {
  477. swal("请先进入一个专辑页面并等待页面完全加载!", {
  478. icon: "error",
  479. buttons: false,
  480. timer: 3000,
  481. });
  482. this.data = []
  483. this.musicList = []
  484. return
  485. }
  486. const all_li = document.querySelectorAll('.sound-list>ul li');
  487. var result = [];
  488. var _this = this
  489. all_li.forEach((item) => {
  490. const item_a = item.querySelector('a');
  491. const number = item.querySelector('span.num') ? parseInt(item.querySelector('span.num').innerText) : 0
  492. const title = item_a.title.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-')
  493. const music = {
  494. id: item_a.href.split('/')[item_a.href.split('/').length - 1],
  495. number,
  496. title: _this.setting.showNumber ? `${number + _this.setting.numberOffset - 1}-${title}` : title,
  497. isDownloading: false,
  498. isDownloaded: false,
  499. progress: 0,
  500. }
  501. result.push(music)
  502. })
  503. // 如果没有获取到数据,则判断为单个音频
  504. if (result.length == 0 && type == 'sound') {
  505. const music = {
  506. id: location.pathname.split('/')[location.pathname.split('/').length - 1],
  507. title: document.querySelector('h1.title-wrapper').innerText,
  508. isDownloading: false,
  509. isDownloaded: false,
  510. progress: 0,
  511. isSingle: true
  512. }
  513. result.push(music)
  514. }
  515.  
  516. // 如果仍未获取到数据
  517. if (result.length == 0) {
  518. swal("未获取到数据,请进入一个专辑页面并等待页面完全加载!", {
  519. icon: "error",
  520. buttons: false,
  521. timer: 3000,
  522. });
  523. }
  524.  
  525. this.data = result
  526. this.musicList = []
  527. this.data.forEach((item) => {
  528. this.musicList.push(item)
  529. })
  530. },
  531. async getMusicURL(item) {
  532. var res = await getSimpleMusicURL1(item)
  533. res = res || await getSimpleMusicURL2(item)
  534. res = res || await getAllMusicURL2(item)
  535. res = res || await getAllMusicURL1(item)
  536. this.$set(item, 'url', res)
  537. return res
  538. },
  539. async openMusicURL(item) {
  540. item.url = item.url || await this.getMusicURL(item)
  541. window.open(item.url)
  542. },
  543. async downloadMusic(item) {
  544. //this.isDownloading = true
  545. item.isDownloading = true
  546. item.isFailued = false
  547. var _this = this
  548. const details = {
  549. url: item.url || await this.getMusicURL(item),
  550. name: item.title.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-'),
  551. onload: function(e) {
  552. _this.isDownloading = false
  553. item.isDownloading = false
  554. item.isDownloaded = true
  555. _this.selectAllMusic()
  556. },
  557. onerror: function(e) {
  558. _this.isDownloading = false
  559. console.log(e)
  560. item.isDownloading = false
  561. if (e.error != 'aborted') item.isFailued = true
  562. },
  563. onprogress: function(d) {
  564. item.progress = (Math.round(d.loaded / d.total * 10000) / 100.00) + "%";
  565. }
  566. }
  567. this.cancelDownloadObj = GM_download(details)
  568. },
  569. // 顺序下载
  570. async sequenceDownload(index, data) {
  571. this.isDownloading = true
  572. const item = data[index]
  573. if (!item) {
  574. this.isDownloading = false
  575. this.selectAllMusic()
  576. this.stopDownload = false
  577. return;
  578. };
  579. if (item.isDownloading || item.isDownloaded || this.stopDownload) return this.sequenceDownload(index + 1, data);
  580. item.isDownloading = true
  581. item.isFailued = false
  582. const _this = this
  583. const details = {
  584. url: item.url || await this.getMusicURL(item),
  585. name: item.title.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-'),
  586. onload: function(e) {
  587. item.isDownloading = false
  588. item.isDownloaded = true
  589. _this.cancelDownloadObj = _this.sequenceDownload(index + 1, data)
  590. },
  591. onerror: function(e) {
  592. console.log(e)
  593. item.isDownloading = false
  594. if (e.error != 'aborted') item.isFailued = true
  595. _this.cancelDownloadObj = _this.sequenceDownload(index + 1, data)
  596. },
  597. onprogress: function(d) {
  598. item.progress = (Math.round(d.loaded / d.total * 10000) / 100.00) + "%";
  599. }
  600. }
  601. this.cancelDownloadObj = GM_download(details)
  602. return this.cancelDownloadObj
  603. },
  604. async copyMusic(item) {
  605. item.url = item.url || await this.getMusicURL(item)
  606. GM_setClipboard(item.url)
  607. },
  608. // 下载当前列表全部音频
  609. async downloadAllMusics() {
  610. await this.sequenceDownload(0, this.musicList)
  611. },
  612. async copyAllMusicURL() {
  613. this.copyMusicURLProgress = 0
  614. var res = []
  615. for (var num = 0; num < this.musicList.length; num++) {
  616. var item = this.musicList[num];
  617. const url = await this.getMusicURL(item)
  618. await Sleep(0.01)
  619. this.copyMusicURLProgress = Math.round((num + 1) / this.musicList.length * 10000) / 100.00;
  620. res.push(url)
  621. }
  622. GM_setClipboard(res.join('\n'))
  623. swal("复制成功!", {
  624. icon: "success",
  625. buttons: false,
  626. timer: 1000,
  627. });
  628. this.copyMusicURLProgress = 0
  629. },
  630. async csvAllMusicURL() {
  631. this.copyMusicURLProgress = 0
  632. var dir = document.querySelector('h1.title').innerText
  633. dir = dir || Date.parse(new Date()) / 1000
  634. // var res = ["url,subfolder,filename"]
  635. var res = []
  636. for (var num = 0; num < this.musicList.length; num++) {
  637. var item = this.musicList[num];
  638. const url = await this.getMusicURL(item)
  639. await Sleep(0.01)
  640. this.copyMusicURLProgress = Math.round((num + 1) / this.musicList.length * 10000) / 100.00;
  641. res.push(`${item.number},${item.title.replaceAll(',',',')},${url},${dir}`)
  642. }
  643. GM_setClipboard(res.join('\n'))
  644. this.copyMusicURLProgress = 0
  645.  
  646. function download(filename, text) {
  647. var element = document.createElement('a');
  648. element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
  649. element.setAttribute('download', filename);
  650. element.style.display = 'none';
  651. document.body.appendChild(element);
  652. element.click();
  653. document.body.removeChild(element);
  654. }
  655.  
  656. download(`${dir}.csv`, res.join('\n'));
  657. swal("下载 CSV 文件成功!", {
  658. icon: "success",
  659. buttons: false,
  660. timer: 1000,
  661. });
  662. },
  663. async aria2AllMusicURL(wsurl) {
  664. this.setting.aria2 = wsurl
  665. GM_setValue('priate_script_xmly_data', this.setting)
  666. this.copyMusicURLProgress = 0
  667. const config = {
  668. wsurl
  669. }
  670. var dir = document.querySelector('h1.title').innerText
  671. dir = dir || (Date.parse(new Date()) / 1000 + '')
  672. dir = dir.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-') + '/'
  673. for (var num = 0; num < this.musicList.length; num++) {
  674. var item = this.musicList[num];
  675. const url = await this.getMusicURL(item)
  676. var ext = url.split('.')[url.split('.').length - 1]
  677. ext = ext.toLowerCase()
  678. if (ext != 'mp3' || ext != 'm4a') {
  679. ext = 'mp3'
  680. }
  681. await Sleep(0.01)
  682. this.copyMusicURLProgress = Math.round((num + 1) / this.musicList.length * 10000) / 100.00;
  683. Aria2(url, dir + item.title + '.' + ext, config)
  684. }
  685. swal(`Aria2 任务下发成功!文件保存至 ${dir} ,请自行检查下载状态。`, {
  686. icon: "success",
  687. buttons: false,
  688. timer: 5000,
  689. });
  690. this.copyMusicURLProgress = 0
  691. },
  692. async exportAllMusicURL() {
  693. var _this = this
  694. var swalField = document.createElement('input');
  695. swalField.setAttribute("placeholder", "Aria2 下载地址");
  696. swalField.setAttribute("value", this.setting.aria2);
  697. swalField.setAttribute("class", "swal-content__input");
  698. swal("URL : 仅复制选中音频的 URL,不附带文件名等信息。\n\nCSV : 下载包含专辑名、音频名、URL等信息的 CSV 文件,可用 EXCEL 编辑,便于自己实现批量下载。\n\nAria2 : 调用 Aria2 进行下载,请先在文本框中填写 RPC 地址,并在 RPC 服务端将授权密钥设置为空,默认为 Motrix 的 RPC 下载地址。", {
  699. buttons: {
  700. 1: "URL",
  701. 2: "CSV",
  702. 3: "Aria2",
  703. },
  704. content: swalField,
  705. }).then(async (value) => {
  706. const method = parseInt(value)
  707. switch (method) {
  708. case 1:
  709. await _this.copyAllMusicURL();
  710. break;
  711. case 2:
  712. await _this.csvAllMusicURL();
  713. break;
  714. case 3:
  715. await _this.aria2AllMusicURL(swalField.value);
  716. break;
  717. default:
  718. if (value) swal(`导出失败!导出方法 ${value} 不存在。`, {
  719. icon: "error",
  720. buttons: false,
  721. timer: 3000,
  722. });
  723. break;
  724. }
  725. })
  726. },
  727. selectAllMusic() {
  728. if (this.musicList.length == this.notDownloadedData.length) {
  729. this.musicList = []
  730. } else {
  731. this.musicList = []
  732. this.data.forEach((item) => {
  733. !item.isDownloaded && this.musicList.push(item)
  734. })
  735.  
  736. }
  737. },
  738. //取消下载功能
  739. cancelDownload() {
  740. this.stopDownload = true
  741. this.cancelDownloadObj.abort()
  742. },
  743. // 修改音质功能
  744. changeQuality() {
  745. const _this = this
  746. swal("由于喜马拉雅接口变动,此功能暂时不可用,目前统一为高清。", {
  747. buttons: false,
  748. timer: 3000,
  749. // buttons: {
  750. // 1: "标准",
  751. // 2: "高清",
  752. // 3: "超高(仅VIP)",
  753. // },
  754. }).then((value) => {
  755. var changeFlag = true
  756. switch (value) {
  757. case "1":
  758. _this.setting.quality = 0;
  759. break;
  760. case "2":
  761. _this.setting.quality = 1;
  762. break;
  763. case "3":
  764. _this.setting.quality = 2;
  765. break;
  766. default:
  767. changeFlag = false
  768. }
  769. _this.setting.quality = 1
  770. GM_setValue('priate_script_xmly_data', _this.setting)
  771. changeFlag && location.reload()
  772. });
  773. },
  774. // 切换是否显示编号功能
  775. switchShowNumber() {
  776. this.setting.showNumber = !this.setting.showNumber
  777. this.setting.numberOffset = 0
  778. GM_setValue('priate_script_xmly_data', this.setting)
  779. if (this.filterData.length > 0) {
  780. this.loadMusic()
  781. }
  782. },
  783. // 增加编号偏移量
  784. addNumberOffset() {
  785. if (!this.setting.showNumber) swal("请先开启编号功能再设置编号偏移量!", {
  786. buttons: false,
  787. timer: 2000,
  788. })
  789. if (this.setting.showNumber) this.setting.numberOffset += 1
  790.  
  791. GM_setValue('priate_script_xmly_data', this.setting)
  792. if (this.filterData.length > 0) {
  793. this.loadMusic()
  794. }
  795. },
  796. // 减少编号偏移量
  797. subNumberOffset() {
  798. if (!this.setting.showNumber) swal("请先开启编号功能再设置编号偏移量!", {
  799. buttons: false,
  800. timer: 2000,
  801. })
  802. if (this.setting.showNumber) this.setting.numberOffset -= 1
  803.  
  804. GM_setValue('priate_script_xmly_data', this.setting)
  805. if (this.filterData.length > 0) {
  806. this.loadMusic()
  807. }
  808. },
  809. // 修改每页容量
  810. changePageSize() {
  811. const _this = this
  812. swal("请设置每页展示的音频数量,最小为 30 ,最大为 100。\n\n注意:此设置仅会改变每页展示数据,底部的分页导航不受影响,因此后面部分页面出现空白为正常现象。\n\n设置后将刷新页面。", {
  813. content: {
  814. element: "input",
  815. attributes: {
  816. placeholder: "每页展示的音频数量",
  817. type: "number",
  818. value: _this.setting.pageSize ? _this.setting.pageSize : 30
  819. }
  820. }
  821. }).then((value) => {
  822. const number = parseInt(value)
  823. if (number > 100 || number < 30) {
  824. swal(`每页数量不得超过 100 或少于 30!`, {
  825. icon: "error",
  826. buttons: false,
  827. timer: 4000,
  828. });
  829. } else {
  830. _this.setting.pageSize = number || _this.setting.pageSize
  831. GM_setValue('priate_script_xmly_data', _this.setting)
  832. if (value) location.reload()
  833. }
  834. });
  835. },
  836. clearMusicData() {
  837. if (this.data.length == 0) swal(`已经是最简形态了!`, {
  838. buttons: false,
  839. timer: 2000,
  840. });
  841. this.data = []
  842. this.musicList = []
  843. },
  844. openDonate() {
  845. showDonate()
  846. }
  847. },
  848. computed: {
  849. filterData() {
  850. if (this.isDownloading) {
  851. return this.musicList
  852. } else {
  853. return this.data
  854. }
  855.  
  856. },
  857. notDownloadedData() {
  858. return this.data.filter((item) => {
  859. return item.isDownloaded == false
  860. })
  861. },
  862. qualityStr() {
  863. var quality = (this.setting.quality >= 0 && this.setting.quality <= 2) ? this.setting.quality : 3
  864. const str = ["标准", "高清", "超高", "未知"]
  865. return str[quality]
  866. },
  867. qualityColor() {
  868. var quality = (this.setting.quality >= 0 && this.setting.quality <= 2) ? this.setting.quality : 3
  869. const color = ["#946C00", "#55ACEE", "#00947e", "#337ab7"]
  870. return color[quality]
  871. }
  872. },
  873. mounted() {}
  874. })
  875. //设置div可拖动
  876. dragFunc("priate_script_div");
  877. })();