SoundCloud: Additional "Add to playlist" button

Adds an "Add to Playlist" button for easier and quicker access.

  1. // ==UserScript==
  2. // @name SoundCloud: Additional "Add to playlist" button
  3. // @description Adds an "Add to Playlist" button for easier and quicker access.
  4. // @version 1.1
  5. // @author iammordaty
  6. // @namespace https://github.com/iammordaty
  7. // @match https://soundcloud.com/*
  8. // @license MIT
  9. // @grant none
  10. // @icon https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
  11. // ==/UserScript==
  12.  
  13. const ELEMENTS = '.soundList__item, .soundBadgeList__item, .listenEngagement__footer, .trackList__item, .historicalPlays__item';
  14.  
  15. const BUTTON_CLASS_NAMES = [
  16. 'sc-button',
  17. 'sc-button-icon',
  18. 'sc-button-medium',
  19. 'sc-button-responsive',
  20. 'sc-button-secondary',
  21. 'sc-button-small',
  22. ];
  23.  
  24. const getButtonClassList = refNodeClassList =>
  25. [...BUTTON_CLASS_NAMES.filter(value => refNodeClassList.includes(value)),
  26. 'sc-button-add-to-playlist',
  27. 'sc-button-addtoset'
  28. ];
  29.  
  30. const createButton = (container, refNode) => {
  31. const button = document.createElement('button');
  32. button.setAttribute('role', 'button');
  33.  
  34. const classList = getButtonClassList([...refNode.classList]);
  35. button.classList.add(...classList);
  36. button.setAttribute('title', 'Add this track to Playlist');
  37.  
  38. const innerDiv = document.createElement('div');
  39. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  40. svg.setAttribute('viewBox', '0 0 16 16');
  41. svg.setAttribute('aria-hidden', 'true');
  42.  
  43. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  44. path.setAttribute('d', 'M3.25 7V4.75H1v-1.5h2.25V1h1.5v2.25H7v1.5H4.75V7h-1.5zM9 4.75h6v-1.5H9v1.5zM15 9.875H1v-1.5h14v1.5zM1 15h14v-1.5H1V15z');
  45. path.setAttribute('fill', 'currentColor');
  46.  
  47. svg.appendChild(path);
  48. innerDiv.appendChild(svg);
  49. button.appendChild(innerDiv);
  50.  
  51. button.addEventListener('click', () => {
  52. container.querySelector('button[aria-label="more" i]').click();
  53. document.querySelector('button[title="add to playlist" i]').click();
  54. }, false);
  55.  
  56. return button;
  57. };
  58.  
  59. const insertAfter = (button, refButton) => refButton.parentNode.insertBefore(button, refButton);
  60.  
  61. const pending = new Set();
  62. let scheduled = false;
  63.  
  64. const scheduleProcess = () => {
  65. if (scheduled) {
  66. return;
  67. }
  68.  
  69. scheduled = true;
  70.  
  71. requestAnimationFrame(() => {
  72. scheduled = false;
  73.  
  74. pending.forEach(container => processContainer(container));
  75. pending.clear();
  76. });
  77. };
  78.  
  79. const processContainer = container => {
  80. if (!(container instanceof HTMLElement)) {
  81. return;
  82. }
  83.  
  84. const refButton = container.querySelector('.sc-button-more');
  85.  
  86. if (!refButton) {
  87. return;
  88. }
  89.  
  90. if (container.querySelector('.sc-button-add-to-playlist')) {
  91. return;
  92. }
  93.  
  94. const button = createButton(container, refButton);
  95.  
  96. insertAfter(button, refButton);
  97. };
  98.  
  99. const scanPage = () => {
  100. document.querySelectorAll(ELEMENTS).forEach(el => pending.add(el));
  101.  
  102. scheduleProcess();
  103. };
  104.  
  105. const observer = new MutationObserver(mutations => {
  106. for (const mutation of mutations) {
  107. for (const node of mutation.addedNodes) {
  108. if (!(node instanceof HTMLElement)) {
  109. continue;
  110. }
  111.  
  112. if (node.matches(ELEMENTS)) {
  113. pending.add(node);
  114. }
  115.  
  116. node.querySelectorAll?.(ELEMENTS).forEach(el => {
  117. pending.add(el);
  118. });
  119. }
  120. }
  121.  
  122. scheduleProcess();
  123. });
  124.  
  125. observer.observe(document.body, {
  126. childList: true,
  127. subtree: true
  128. });
  129.  
  130. document.addEventListener('DOMContentLoaded', scanPage, false);
  131. window.addEventListener('load', scanPage, false);