StreetVoiceLoader

Enables downloading of tracks and albums from StreetVoice.

  1. // ==UserScript==
  2. // @name StreetVoiceLoader
  3. // @name:de StreetVoiceLoader
  4. // @name:en StreetVoiceLoader
  5. // @namespace sun/userscripts
  6. // @version 2.1.12
  7. // @description Enables downloading of tracks and albums from StreetVoice.
  8. // @description:de Erlaubt das Herunterladen von Liedern und Alben von StreetVoice.
  9. // @description:en Enables downloading of tracks and albums from StreetVoice.
  10. // @compatible chrome
  11. // @compatible edge
  12. // @compatible firefox
  13. // @compatible opera
  14. // @compatible safari
  15. // @homepageURL https://forgejo.sny.sh/sun/userscripts
  16. // @supportURL https://forgejo.sny.sh/sun/userscripts/issues
  17. // @contributionURL https://liberapay.com/sun
  18. // @contributionAmount €1.00
  19. // @author Sunny <sunny@sny.sh>
  20. // @include https://streetvoice.com/*/songs/*
  21. // @match https://streetvoice.com/*/songs/*
  22. // @connect streetvoice.com
  23. // @run-at document-end
  24. // @inject-into auto
  25. // @grant GM.xmlHttpRequest
  26. // @grant GM_xmlhttpRequest
  27. // @noframes
  28. // @require https://unpkg.com/@zip.js/zip.js/dist/zip.js
  29. // @require https://unpkg.com/file-saver
  30. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  31. // @icon https://forgejo.sny.sh/sun/userscripts/raw/branch/main/icons/StreetVoiceLoader.ico
  32. // @copyright 2021-present, Sunny (https://sny.sh/)
  33. // @license Hippocratic License; https://forgejo.sny.sh/sun/userscripts/src/branch/main/LICENSE.md
  34. // ==/UserScript==
  35.  
  36. (() => {
  37. const wrapper = document.querySelector(
  38. "#inside_box .col-lg-auto .list-inline",
  39. );
  40. const element = `<li class="list-inline-item">
  41. <button id="btn-svl" class="btn btn-circle btn-outline-white btn-lg">
  42. </button>
  43. </li>`;
  44.  
  45. if (location.pathname.split("/")[3] === "album") {
  46. GM.xmlHttpRequest({
  47. method: "GET",
  48. url: `https://streetvoice.com/api/v5/album/${location.pathname.split("/")[4]}/songs/?limit=100`,
  49. onload: (response) => {
  50. const ids = JSON.parse(response.responseText).results.map(
  51. (result) => result.id,
  52. );
  53. insert(ids);
  54. },
  55. });
  56. } else {
  57. const ids = [Number(location.pathname.split("/")[3])];
  58. insert(ids);
  59. }
  60.  
  61. function insert(ids) {
  62. wrapper.insertAdjacentHTML("afterbegin", element);
  63. document
  64. .getElementById("btn-svl")
  65. .setAttribute("data-svl", JSON.stringify(ids));
  66. document.getElementById("btn-svl").onclick = (event) => file(event);
  67. }
  68.  
  69. function file(event) {
  70. event.target.disabled = true;
  71.  
  72. const ids = JSON.parse(event.target.getAttribute("data-svl"));
  73. const urls = [];
  74.  
  75. function forSync(i) {
  76. document.getElementById("btn-svl").innerHTML =
  77. `<small>${i + 1}/${ids.length}</small>`;
  78.  
  79. GM.xmlHttpRequest({
  80. method: "POST",
  81. url: `https://streetvoice.com/api/v5/song/${ids[i]}/hls/file/`,
  82. onload: (response) => {
  83. urls.push(JSON.parse(response.responseText).file);
  84.  
  85. if (ids[i + 1]) {
  86. forSync(i + 1);
  87. } else {
  88. m3u8(urls);
  89. }
  90. },
  91. });
  92. }
  93.  
  94. forSync(0);
  95. }
  96.  
  97. function m3u8(urls) {
  98. const files = [];
  99.  
  100. function forSync(i) {
  101. document.getElementById("btn-svl").innerHTML =
  102. `<small>${i + 1}/${urls.length}</small>`;
  103.  
  104. const base = `${urls[i].substring(0, urls[i].lastIndexOf("/"))}/`;
  105.  
  106. GM.xmlHttpRequest({
  107. method: "GET",
  108. url: urls[i],
  109. onload: (response) => {
  110. files.push(
  111. response.responseText.match(/.*\.ts/g).map((match) => base + match),
  112. );
  113.  
  114. if (urls[i + 1]) {
  115. forSync(i + 1);
  116. } else {
  117. ts(files);
  118. }
  119. },
  120. });
  121. }
  122.  
  123. forSync(0);
  124. }
  125.  
  126. function ts(files) {
  127. const data = [];
  128.  
  129. function forSyncX(i) {
  130. document.getElementById("btn-svl").innerHTML =
  131. `<small>${i + 1}/${files.length}</small>`;
  132.  
  133. let blob = [];
  134.  
  135. const blobWriter = new zip.BlobWriter("application/zip");
  136. const writer = new zip.ZipWriter(blobWriter);
  137.  
  138. function forSyncY(j) {
  139. GM.xmlHttpRequest({
  140. method: "GET",
  141. url: files[i][j],
  142. responseType: "blob",
  143. onload: (response) => {
  144. blob.push(response.response);
  145.  
  146. if (files[i][j + 1]) {
  147. forSyncY(j + 1);
  148. } else {
  149. blob = new Blob(blob);
  150. data.push(blob);
  151.  
  152. if (files[i + 1]) {
  153. forSyncX(i + 1);
  154. } else {
  155. download(data);
  156. }
  157. }
  158. },
  159. });
  160. }
  161.  
  162. forSyncY(0);
  163. }
  164.  
  165. forSyncX(0);
  166. }
  167.  
  168. function download(data) {
  169. const blobWriter = new zip.BlobWriter("application/zip");
  170. const writer = new zip.ZipWriter(blobWriter);
  171.  
  172. function forSync(i) {
  173. writer.add(`${i + 1}.ts`, new zip.BlobReader(data[i])).then(() => {
  174. if (data[i + 1]) {
  175. forSync(i + 1);
  176. } else {
  177. writer.close().then(() => {
  178. saveAs(
  179. blobWriter.getData(),
  180. `${document.getElementsByTagName("h1")[0].textContent.trim()}.zip`,
  181. );
  182.  
  183. document.getElementById("btn-svl").disabled = false;
  184. document.getElementById("btn-svl").innerText = "⬇";
  185. });
  186. }
  187. });
  188. }
  189.  
  190. forSync(0);
  191. }
  192. })();

QingJ © 2025

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