SoundCloud Direct Downloader

Integrate a download button for SoundCloud tracks and open original cover art.

  1. // ==UserScript==
  2. // @name SoundCloud Direct Downloader
  3. // @description Integrate a download button for SoundCloud tracks and open original cover art.
  4. // @icon https://www.google.com/s2/favicons?sz=64&domain=soundcloud.com
  5. // @version 1.3
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://soundcloud.com/*
  11. // @grant GM.xmlHttpRequest
  12. // @grant GM_xmlhttpRequest
  13. // @connect c.blahaj.ca
  14. // @connect dwnld.nichind.dev
  15. // @connect soundcloud.com
  16. // @require https://cdn.jsdelivr.net/npm/browser-id3-writer@4.4.0/dist/browser-id3-writer.min.js
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. const PRIMARY_API_ENDPOINT = "https://c.blahaj.ca/";
  23. const FALLBACK_API_ENDPOINT = "https://dwnld.nichind.dev/";
  24.  
  25. function isDiscoverPage() {
  26. return window.location.pathname === '/discover';
  27. }
  28.  
  29. function isSetUrl(url) {
  30. return /\/[^\/]+\/sets\//.test(url);
  31. }
  32.  
  33. function isSetUrlForListenArtwork(url) {
  34. return /\/sets\//.test(url) && !url.includes('?in=');
  35. }
  36.  
  37. function createSvgElement(color, size = 16) {
  38. const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  39. svgElement.setAttribute("viewBox", "0 0 448 512");
  40. svgElement.setAttribute("width", size.toString());
  41. svgElement.setAttribute("height", size.toString());
  42. svgElement.style.transition = "0.2s";
  43. svgElement.style.fill = color;
  44.  
  45. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  46. path.setAttribute("d", "M378.1 198.6L249.5 341.4c-6.1 6.7-14.7 10.6-23.8 10.6l-3.5 0c-9.1 0-17.7-3.8-23.8-10.6L69.9 198.6c-3.8-4.2-5.9-9.8-5.9-15.5C64 170.4 74.4 160 87.1 160l72.9 0 0-128c0-17.7 14.3-32 32-32l64 0c17.7 0 32 14.3 32 32l0 128 72.9 0c12.8 0 23.1 10.4 23.1 23.1c0 5.7-2.1 11.2-5.9 15.5zM64 352l0 64c0 17.7 14.3 32 32 32l256 0c17.7 0 32-14.3 32-32l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 64c0 53-43 96-96 96L96 512c-53 0-96-43-96-96l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32z");
  47. svgElement.appendChild(path);
  48.  
  49. return svgElement;
  50. }
  51.  
  52. function addDownloadIcon() {
  53. if (isDiscoverPage()) return;
  54.  
  55. const volumeControl = document.querySelector('.playControls__volume');
  56. if (!volumeControl || document.querySelector('.playControls__cobalt')) return;
  57.  
  58. const iconWrapper = document.createElement('div');
  59. iconWrapper.className = 'playControls__cobalt playControls__control';
  60. iconWrapper.style.cssText = `
  61. display: flex;
  62. align-items: center;
  63. justify-content: center;
  64. width: 24px;
  65. height: 24px;
  66. cursor: pointer;
  67. `;
  68.  
  69. const svgElement = createSvgElement("#333", 16);
  70. iconWrapper.appendChild(svgElement);
  71. volumeControl.parentNode.insertBefore(iconWrapper, volumeControl.nextSibling);
  72.  
  73. iconWrapper.addEventListener('click', (e) => {
  74. e.preventDefault();
  75. e.stopPropagation();
  76. handleDownload(svgElement);
  77. });
  78.  
  79. iconWrapper.addEventListener('mouseenter', () => {
  80. svgElement.style.fill = "#f50";
  81. });
  82.  
  83. iconWrapper.addEventListener('mouseleave', () => {
  84. svgElement.style.fill = "#333";
  85. });
  86. }
  87.  
  88. function addDownloadIconToTiles() {
  89. if (isDiscoverPage()) return;
  90.  
  91. const tiles = document.querySelectorAll('.playableTile__artwork');
  92. tiles.forEach(tile => {
  93. if (!tile.querySelector('.playableTile__cobalt')) {
  94. const artworkLink = tile.querySelector('.playableTile__artworkLink');
  95. if (artworkLink) {
  96. const href = artworkLink.getAttribute('href');
  97. if (isSetUrl(href)) return;
  98.  
  99. const iconWrapper = document.createElement('div');
  100. iconWrapper.className = 'playableTile__cobalt';
  101. iconWrapper.style.cssText = `
  102. position: absolute;
  103. top: 8px;
  104. right: 8px;
  105. z-index: 3;
  106. cursor: pointer;
  107. padding: 4px;
  108. background-color: rgba(0, 0, 0, 0.9);
  109. border-radius: 4px;
  110. `;
  111.  
  112. const svgElement = createSvgElement("#ffffff", 14);
  113. iconWrapper.appendChild(svgElement);
  114. tile.appendChild(iconWrapper);
  115.  
  116. iconWrapper.addEventListener('click', (e) => {
  117. e.preventDefault();
  118. e.stopPropagation();
  119. const artworkLink = tile.querySelector('.playableTile__artworkLink');
  120. if (artworkLink) {
  121. const href = artworkLink.getAttribute('href');
  122. const trackUrl = href.startsWith('http') ? href : `https://soundcloud.com${href}`;
  123. handleDownload(svgElement, trackUrl);
  124. }
  125. });
  126.  
  127. iconWrapper.addEventListener('mouseenter', () => {
  128. svgElement.style.fill = "#f50";
  129. });
  130.  
  131. iconWrapper.addEventListener('mouseleave', () => {
  132. svgElement.style.fill = "#ffffff";
  133. });
  134. }
  135. }
  136. });
  137. }
  138.  
  139. function addDownloadIconToSoundCoverArt() {
  140. if (isDiscoverPage()) return;
  141.  
  142. const coverArts = document.querySelectorAll('.sound__coverArt');
  143. coverArts.forEach(coverArt => {
  144. if (!coverArt.querySelector('.sound__coverArt__cobalt')) {
  145. const href = coverArt.getAttribute('href');
  146. if (isSetUrl(href)) return;
  147.  
  148. const iconWrapper = document.createElement('div');
  149. iconWrapper.className = 'sound__coverArt__cobalt';
  150. iconWrapper.style.cssText = `
  151. position: absolute;
  152. top: 8px;
  153. right: 8px;
  154. z-index: 3;
  155. cursor: pointer;
  156. padding: 4px;
  157. background-color: rgba(0, 0, 0, 0.9);
  158. border-radius: 4px;
  159. `;
  160.  
  161. const svgElement = createSvgElement("#ffffff", 14);
  162. iconWrapper.appendChild(svgElement);
  163. coverArt.appendChild(iconWrapper);
  164.  
  165. iconWrapper.addEventListener('click', (e) => {
  166. e.preventDefault();
  167. e.stopPropagation();
  168. const href = coverArt.getAttribute('href');
  169. const trackUrl = href.startsWith('http') ? href : `https://soundcloud.com${href}`;
  170. handleDownload(svgElement, trackUrl);
  171. });
  172.  
  173. iconWrapper.addEventListener('mouseenter', () => {
  174. svgElement.style.fill = "#f50";
  175. });
  176.  
  177. iconWrapper.addEventListener('mouseleave', () => {
  178. svgElement.style.fill = "#ffffff";
  179. });
  180. }
  181. });
  182. }
  183.  
  184. function addDownloadIconToListenArtwork() {
  185. if (isDiscoverPage()) return;
  186.  
  187. const artworkWrapper = document.querySelector('.listenArtworkWrapper__artwork');
  188. if (!artworkWrapper) return;
  189.  
  190. const isSetUrl = isSetUrlForListenArtwork(window.location.href);
  191. const hasDownloadButton = !isSetUrl;
  192.  
  193. addOriginalArtButton(artworkWrapper, hasDownloadButton);
  194.  
  195. if (hasDownloadButton && !artworkWrapper.querySelector('.listenArtworkWrapper__cobalt')) {
  196. const iconWrapper = document.createElement('div');
  197. iconWrapper.className = 'listenArtworkWrapper__cobalt';
  198. iconWrapper.style.cssText = `
  199. position: absolute;
  200. top: 12px;
  201. right: 12px;
  202. z-index: 3;
  203. cursor: pointer;
  204. padding: 6px;
  205. background-color: rgba(0, 0, 0, 0.9);
  206. border-radius: 4px;
  207. `;
  208.  
  209. const svgElement = createSvgElement("#ffffff", 18);
  210. iconWrapper.appendChild(svgElement);
  211. artworkWrapper.appendChild(iconWrapper);
  212.  
  213. iconWrapper.addEventListener('click', (e) => {
  214. e.preventDefault();
  215. e.stopPropagation();
  216. handleDownloadURL(svgElement);
  217. });
  218.  
  219. iconWrapper.addEventListener('mouseenter', () => {
  220. svgElement.style.fill = "#f50";
  221. });
  222.  
  223. iconWrapper.addEventListener('mouseleave', () => {
  224. svgElement.style.fill = "#ffffff";
  225. });
  226. }
  227. }
  228.  
  229. function addOriginalArtButton(artworkWrapper, hasDownloadButton) {
  230. if (artworkWrapper.querySelector('.listenArtworkWrapper__originalCoverArt')) return;
  231.  
  232. const originalArtWrapper = document.createElement('div');
  233. originalArtWrapper.className = 'listenArtworkWrapper__originalCoverArt';
  234. originalArtWrapper.style.cssText = `
  235. position: absolute;
  236. top: 12px;
  237. right: ${hasDownloadButton ? '54px' : '12px'};
  238. z-index: 4;
  239. cursor: pointer;
  240. padding: 6px;
  241. background-color: rgba(0, 0, 0, 0.9);
  242. border-radius: 4px;
  243. `;
  244.  
  245. const originalArtSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  246. originalArtSvg.setAttribute("viewBox", "0 0 512 512");
  247. originalArtSvg.setAttribute("width", "18");
  248. originalArtSvg.setAttribute("height", "18");
  249. originalArtSvg.style.fill = "#ffffff";
  250.  
  251. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  252. path.setAttribute("d", "M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z");
  253. originalArtSvg.appendChild(path);
  254.  
  255. originalArtWrapper.appendChild(originalArtSvg);
  256. artworkWrapper.appendChild(originalArtWrapper);
  257.  
  258. originalArtWrapper.addEventListener('click', (e) => {
  259. e.preventDefault();
  260. e.stopPropagation();
  261. openOriginalCoverArt();
  262. });
  263.  
  264. originalArtWrapper.addEventListener('mouseenter', () => {
  265. originalArtSvg.style.fill = "#f50";
  266. });
  267.  
  268. originalArtWrapper.addEventListener('mouseleave', () => {
  269. originalArtSvg.style.fill = "#ffffff";
  270. });
  271. }
  272.  
  273. async function getDownloadUrl(trackUrl) {
  274. return new Promise(async (resolve, reject) => {
  275. const jsonData = {
  276. filenameStyle: "basic",
  277. url: trackUrl,
  278. audioFormat: "mp3",
  279. downloadMode: "audio",
  280. audioBitrate: "320"
  281. };
  282. try {
  283. const result = await makeAPIRequest(PRIMARY_API_ENDPOINT, jsonData);
  284. resolve(result);
  285. } catch (primaryError) {
  286. console.log("Primary API failed, trying fallback...", primaryError);
  287. try {
  288. const fallbackResult = await makeAPIRequest(FALLBACK_API_ENDPOINT, jsonData);
  289. resolve(fallbackResult);
  290. } catch (fallbackError) {
  291. reject(fallbackError);
  292. }
  293. }
  294. });
  295. }
  296. function makeAPIRequest(endpoint, jsonData) {
  297. return new Promise((resolve, reject) => {
  298. GM.xmlHttpRequest({
  299. method: "POST",
  300. url: endpoint,
  301. headers: {
  302. "Accept": "application/json",
  303. "Content-Type": "application/json"
  304. },
  305. data: JSON.stringify(jsonData),
  306. onload: function(response) {
  307. try {
  308. const responseData = JSON.parse(response.responseText);
  309. if (responseData.url) {
  310. resolve({ url: responseData.url });
  311. } else {
  312. reject(new Error("No download URL found in response"));
  313. }
  314. } catch (error) {
  315. reject(new Error("Failed to parse response from server"));
  316. }
  317. },
  318. onerror: function(error) {
  319. reject(new Error("Request failed: " + error.statusText));
  320. }
  321. });
  322. });
  323. }
  324.  
  325. async function scrapeSoundcloudMetadata(trackUrl) {
  326. return new Promise((resolve, reject) => {
  327. GM.xmlHttpRequest({
  328. method: "GET",
  329. url: trackUrl,
  330. headers: {
  331. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
  332. },
  333. onload: function(response) {
  334. try {
  335. const parser = new DOMParser();
  336. const doc = parser.parseFromString(response.responseText, "text/html");
  337. const trackInfo = {
  338. cover: null,
  339. title: null,
  340. artist: null,
  341. pubdate: null
  342. };
  343. const coverImg = doc.querySelector('img[itemprop="image"]');
  344. if (coverImg) {
  345. trackInfo.cover = coverImg.getAttribute('src');
  346. }
  347. const nameTag = doc.querySelector('h1[itemprop="name"]');
  348. if (nameTag) {
  349. const trackLinks = nameTag.querySelectorAll('a');
  350. if (trackLinks.length) {
  351. if (trackLinks.length > 1) {
  352. trackInfo.title = trackLinks[0].textContent.trim();
  353. trackInfo.artist = trackLinks[1].textContent.trim();
  354. } else {
  355. trackInfo.title = trackLinks[0].textContent.trim();
  356. }
  357. }
  358. }
  359. const timeTag = doc.querySelector('time');
  360. if (timeTag) {
  361. trackInfo.pubdate = timeTag.textContent.trim();
  362. }
  363. resolve(trackInfo);
  364. } catch (error) {
  365. reject(new Error("Failed to parse SoundCloud page: " + error.message));
  366. }
  367. },
  368. onerror: function(error) {
  369. reject(new Error("Request failed: " + error.statusText));
  370. }
  371. });
  372. });
  373. }
  374.  
  375. function formatDateForID3(dateString) {
  376. const date = new Date(dateString);
  377. return {
  378. year: date.getFullYear(),
  379. date: `${String(date.getDate()).padStart(2, '0')}${String(date.getMonth() + 1).padStart(2, '0')}`
  380. };
  381. }
  382.  
  383. async function handleDownload(svgElement, trackUrl) {
  384. let fullUrl;
  385.  
  386. if (trackUrl) {
  387. fullUrl = trackUrl;
  388. } else {
  389. const trackLink = document.querySelector('a.playbackSoundBadge__titleLink');
  390. if (!trackLink) {
  391. showError(svgElement);
  392. return;
  393. }
  394. const href = trackLink.getAttribute('href');
  395. fullUrl = href.startsWith('http') ? href : `https://soundcloud.com${href}`;
  396. }
  397.  
  398. showLoading(svgElement);
  399.  
  400. try {
  401. const [audioData, metadata] = await Promise.all([
  402. getDownloadUrl(fullUrl),
  403. scrapeSoundcloudMetadata(fullUrl)
  404. ]);
  405.  
  406. if (!audioData.url) {
  407. throw new Error('Download URL not found in the response');
  408. }
  409.  
  410. const audioBlob = await fetch(audioData.url).then(r => r.blob());
  411. const arrayBuffer = await new Response(audioBlob).arrayBuffer();
  412. const writer = new ID3Writer(arrayBuffer);
  413. writer.removeTag();
  414.  
  415. if (metadata.pubdate) {
  416. const { year, date } = formatDateForID3(metadata.pubdate);
  417. writer
  418. .setFrame('TYER', year.toString())
  419. .setFrame('TDAT', date);
  420. }
  421.  
  422. writer
  423. .setFrame('TIT2', metadata.title)
  424. .setFrame('TPE1', [metadata.artist]);
  425.  
  426. if (metadata.cover) {
  427. const coverResponse = await fetch(metadata.cover);
  428. const coverArrayBuffer = await coverResponse.arrayBuffer();
  429. writer.setFrame('APIC', {
  430. type: 3,
  431. data: coverArrayBuffer,
  432. description: 'Cover'
  433. });
  434. }
  435.  
  436. writer.addTag();
  437. const taggedArrayBuffer = writer.arrayBuffer;
  438. const finalBlob = new Blob([taggedArrayBuffer], { type: 'audio/mpeg' });
  439.  
  440. showSuccess(svgElement);
  441. setTimeout(() => {
  442. const downloadLink = document.createElement('a');
  443. downloadLink.href = URL.createObjectURL(finalBlob);
  444. let fileName = metadata.title && metadata.artist ?
  445. `${metadata.title} - ${metadata.artist}` :
  446. metadata.title || 'soundcloud_track';
  447. fileName = fileName.replace(/[<>:"/\\|?*]/g, '-');
  448. if (!fileName.toLowerCase().endsWith('.mp3')) {
  449. fileName += '.mp3';
  450. }
  451. downloadLink.download = fileName;
  452. document.body.appendChild(downloadLink);
  453. downloadLink.click();
  454. document.body.removeChild(downloadLink);
  455. URL.revokeObjectURL(downloadLink.href);
  456. }, 1000);
  457.  
  458. } catch (error) {
  459. console.error("Download error:", error);
  460. showError(svgElement);
  461. }
  462. }
  463.  
  464. async function handleDownloadURL(svgElement) {
  465. const currentURL = window.location.href;
  466. showLoading(svgElement);
  467.  
  468. try {
  469. const [audioData, metadata] = await Promise.all([
  470. getDownloadUrl(currentURL),
  471. scrapeSoundcloudMetadata(currentURL)
  472. ]);
  473.  
  474. if (!audioData.url) {
  475. throw new Error('Download URL not found in the response');
  476. }
  477.  
  478. const audioBlob = await fetch(audioData.url).then(r => r.blob());
  479. const arrayBuffer = await new Response(audioBlob).arrayBuffer();
  480. const writer = new ID3Writer(arrayBuffer);
  481. writer.removeTag();
  482.  
  483. if (metadata.pubdate) {
  484. const { year, date } = formatDateForID3(metadata.pubdate);
  485. writer
  486. .setFrame('TYER', year.toString())
  487. .setFrame('TDAT', date);
  488. }
  489.  
  490. writer
  491. .setFrame('TIT2', metadata.title)
  492. .setFrame('TPE1', [metadata.artist]);
  493.  
  494. if (metadata.cover) {
  495. const coverResponse = await fetch(metadata.cover);
  496. const coverArrayBuffer = await coverResponse.arrayBuffer();
  497. writer.setFrame('APIC', {
  498. type: 3,
  499. data: coverArrayBuffer,
  500. description: 'Cover'
  501. });
  502. }
  503.  
  504. writer.addTag();
  505. const taggedArrayBuffer = writer.arrayBuffer;
  506. const finalBlob = new Blob([taggedArrayBuffer], { type: 'audio/mpeg' });
  507.  
  508. showSuccess(svgElement);
  509. setTimeout(() => {
  510. const downloadLink = document.createElement('a');
  511. downloadLink.href = URL.createObjectURL(finalBlob);
  512. let fileName = metadata.title && metadata.artist ?
  513. `${metadata.title} - ${metadata.artist}` :
  514. metadata.title || 'soundcloud_track';
  515. fileName = fileName.replace(/[<>:"/\\|?*]/g, '-');
  516. if (!fileName.toLowerCase().endsWith('.mp3')) {
  517. fileName += '.mp3';
  518. }
  519. downloadLink.download = fileName;
  520. document.body.appendChild(downloadLink);
  521. downloadLink.click();
  522. document.body.removeChild(downloadLink);
  523. URL.revokeObjectURL(downloadLink.href);
  524. }, 1000);
  525.  
  526. } catch (error) {
  527. console.error("Download error:", error);
  528. showError(svgElement);
  529. }
  530. }
  531.  
  532. function openOriginalCoverArt() {
  533. const selectors = [
  534. '.fullHero__artwork .image__full',
  535. '.listenArtworkWrapper__artwork .image__full',
  536. '.listenArtworkWrapper__artwork .sc-artwork'
  537. ];
  538.  
  539. let artworkElement = null;
  540. for (const selector of selectors) {
  541. artworkElement = document.querySelector(selector);
  542. if (artworkElement) break;
  543. }
  544.  
  545. if (artworkElement) {
  546. console.log('Artwork element found:', artworkElement);
  547.  
  548. let backgroundImage = window.getComputedStyle(artworkElement).backgroundImage;
  549.  
  550. if (!backgroundImage || backgroundImage === 'none') {
  551. backgroundImage = artworkElement.getAttribute('src');
  552. console.log('Using src attribute:', backgroundImage);
  553. } else {
  554. console.log('Using background-image:', backgroundImage);
  555. }
  556.  
  557. let originalUrl = extractAndCleanUrl(backgroundImage);
  558.  
  559. if (originalUrl) {
  560. console.log('Opening URL:', originalUrl);
  561. window.open(originalUrl, '_blank');
  562. } else {
  563. console.error('Could not extract cover art URL from:', backgroundImage);
  564. }
  565. } else {
  566. console.error('Could not find artwork element. Tried selectors:', selectors);
  567. }
  568. }
  569.  
  570. function extractAndCleanUrl(input) {
  571. const urlMatch = input.match(/https?:\/\/[^"']*?sndcdn\.com\/[^"')]+/);
  572.  
  573. if (urlMatch) {
  574. let url = urlMatch[0];
  575.  
  576. url = url.split('?')[0];
  577.  
  578. url = url.replace(/-t\d+x\d+/, '-original');
  579.  
  580. if (!/\.(jpg|jpeg|png|gif)$/i.test(url)) {
  581. url += '.jpg';
  582. }
  583.  
  584. return url;
  585. }
  586.  
  587. return null;
  588. }
  589.  
  590. function showLoading(svgElement) {
  591. while (svgElement.firstChild) {
  592. svgElement.removeChild(svgElement.firstChild);
  593. }
  594. svgElement.setAttribute("viewBox", "0 0 512 512");
  595. const originalColor = svgElement.style.fill;
  596. svgElement.style.fill = originalColor === "#ffffff" ? "#ffffff" : "#f50";
  597.  
  598. const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  599. secondaryPath.setAttribute("d", "M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z");
  600. secondaryPath.style.opacity = "0.4";
  601. svgElement.appendChild(secondaryPath);
  602.  
  603. const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  604. primaryPath.setAttribute("d", "M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z");
  605. svgElement.appendChild(primaryPath);
  606.  
  607. svgElement.style.animation = 'spin 1s linear infinite';
  608. }
  609.  
  610. function showSuccess(svgElement) {
  611. while (svgElement.firstChild) {
  612. svgElement.removeChild(svgElement.firstChild);
  613. }
  614. svgElement.style.animation = '';
  615. const originalColor = svgElement.style.fill;
  616. svgElement.style.fill = originalColor === "#ffffff" ? "#ffffff" : "#f50";
  617. svgElement.setAttribute("viewBox", "0 0 512 512");
  618. const successPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  619. successPath.setAttribute("d", "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z");
  620. svgElement.appendChild(successPath);
  621. setTimeout(() => resetIcon(svgElement), 2000);
  622. }
  623.  
  624. function showError(svgElement) {
  625. while (svgElement.firstChild) {
  626. svgElement.removeChild(svgElement.firstChild);
  627. }
  628. svgElement.style.animation = '';
  629. const originalColor = svgElement.style.fill;
  630. svgElement.style.fill = originalColor === "#ffffff" ? "#ffffff" : "#333";
  631. svgElement.setAttribute("viewBox", "0 0 512 512");
  632. const errorPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  633. errorPath.setAttribute("d", "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z");
  634. svgElement.appendChild(errorPath);
  635. setTimeout(() => resetIcon(svgElement), 2000);
  636. }
  637.  
  638. function resetIcon(svgElement) {
  639. while (svgElement.firstChild) {
  640. svgElement.removeChild(svgElement.firstChild);
  641. }
  642. const originalColor = svgElement.getAttribute('data-original-color') || "#333";
  643. svgElement.style.fill = originalColor;
  644. svgElement.setAttribute("viewBox", "0 0 448 512");
  645. svgElement.style.animation = '';
  646. const originalPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  647. originalPath.setAttribute("d", "M378.1 198.6L249.5 341.4c-6.1 6.7-14.7 10.6-23.8 10.6l-3.5 0c-9.1 0-17.7-3.8-23.8-10.6L69.9 198.6c-3.8-4.2-5.9-9.8-5.9-15.5C64 170.4 74.4 160 87.1 160l72.9 0 0-128c0-17.7 14.3-32 32-32l64 0c17.7 0 32 14.3 32 32l0 128 72.9 0c12.8 0 23.1 10.4 23.1 23.1c0 5.7-2.1 11.2-5.9 15.5zM64 352l0 64c0 17.7 14.3 32 32 32l256 0c17.7 0 32-14.3 32-32l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 64c0 53-43 96-96 96L96 512c-53 0-96-43-96-96l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32z");
  648. svgElement.appendChild(originalPath);
  649. }
  650.  
  651. const styleSheet = document.createElement('style');
  652. styleSheet.textContent = `
  653. @keyframes spin {
  654. from { transform: rotate(0deg); }
  655. to { transform: rotate(360deg); }
  656. }
  657. .sc-classic .playControls__control, .sc-classic .playControls__control:not(:first-child) {
  658. margin-right: 12px;
  659. }
  660. .playableTile__cobalt, .listenArtworkWrapper__cobalt, .listenArtworkWrapper__originalCoverArt {
  661. display: flex;
  662. align-items: center;
  663. justify-content: center;
  664. }
  665. .playableTile__cobalt:hover, .listenArtworkWrapper__cobalt:hover, .listenArtworkWrapper__originalCoverArt:hover {
  666. background-color: rgba(0, 0, 0, 0.9);
  667. }
  668. .playableTile__cobalt:hover svg, .listenArtworkWrapper__cobalt:hover svg, .listenArtworkWrapper__originalCoverArt:hover svg {
  669. fill: #f50 !important;
  670. }
  671. .playableTile__cobalt svg, .listenArtworkWrapper__cobalt svg, .listenArtworkWrapper__originalCoverArt svg {
  672. fill: #ffffff !important;
  673. }
  674. .playControls__cobalt svg {
  675. width: 16px;
  676. height: 16px;
  677. }
  678. .playableTile__cobalt svg {
  679. width: 14px;
  680. height: 14px;
  681. }
  682. .listenArtworkWrapper__cobalt svg, .listenArtworkWrapper__originalCoverArt svg {
  683. width: 18px;
  684. height: 18px;
  685. }
  686. .sound__coverArt__cobalt {
  687. display: flex;
  688. align-items: center;
  689. justify-content: center;
  690. }
  691. .sound__coverArt__cobalt:hover {
  692. background-color: rgba(0, 0, 0, 0.9);
  693. }
  694. .sound__coverArt__cobalt:hover svg {
  695. fill: #f50 !important;
  696. }
  697. .sound__coverArt__cobalt svg {
  698. fill: #ffffff !important;
  699. width: 14px;
  700. height: 14px;
  701. }
  702. `;
  703. document.head.appendChild(styleSheet);
  704.  
  705. if (!isDiscoverPage()) {
  706. addDownloadIcon();
  707. addDownloadIconToTiles();
  708. addDownloadIconToSoundCoverArt();
  709. addDownloadIconToListenArtwork();
  710. }
  711.  
  712. const observer = new MutationObserver((mutations) => {
  713. if (!isDiscoverPage()) {
  714. for (let mutation of mutations) {
  715. if (mutation.type === 'childList') {
  716. addDownloadIcon();
  717. addDownloadIconToTiles();
  718. addDownloadIconToSoundCoverArt();
  719. addDownloadIconToListenArtwork();
  720. }
  721. }
  722. }
  723. });
  724.  
  725. observer.observe(document.body, {
  726. childList: true,
  727. subtree: true
  728. });
  729.  
  730. let lastUrl = location.href;
  731. new MutationObserver(() => {
  732. const url = location.href;
  733. if (url !== lastUrl) {
  734. lastUrl = url;
  735. if (isDiscoverPage()) {
  736. document.querySelectorAll('.playableTile__cobalt, .playControls__cobalt, .sound__coverArt__cobalt, .listenArtworkWrapper__cobalt').forEach(el => el.remove());
  737. } else {
  738. addDownloadIcon();
  739. addDownloadIconToTiles();
  740. addDownloadIconToSoundCoverArt();
  741. addDownloadIconToListenArtwork();
  742. }
  743. }
  744. }).observe(document, {subtree: true, childList: true});
  745. })();