SoundCloud RSS Feed

Get RSS feed URL for SoundCloud user pages

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         SoundCloud RSS Feed
// @namespace    https://github.com/26d0/userscripts
// @version      0.1.0
// @description  Get RSS feed URL for SoundCloud user pages
// @match        https://soundcloud.com/*
// @grant        none
// @icon         https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
// ==/UserScript==

(function () {
  "use strict";

  // ===================
  // Icons
  // ===================

  const ICONS = {
    rss: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#f70" stroke="none">
      <circle cx="6.18" cy="17.82" r="2.18"/>
      <path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/>
    </svg>`,
    check: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="20 6 9 17 4 12"/>
    </svg>`,
    error: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
      <line x1="18" y1="6" x2="6" y2="18"/>
      <line x1="6" y1="6" x2="18" y2="18"/>
    </svg>`,
  };

  // ===================
  // Button Management
  // ===================

  const btn = {
    init() {
      this.el = document.createElement("button");
      this.el.innerHTML = ICONS.rss;
      this.el.title = "Copy RSS feed URL";
      this.el.classList.add("sc-button");
      this.el.classList.add("sc-button-medium");
      this.el.classList.add("sc-button-icon");
      this.el.classList.add("sc-button-responsive");
      this.el.classList.add("sc-button-secondary");
    },

    cb() {
      // Try multiple selectors for different page layouts
      const selectors = [
        // User profile page - header action buttons
        ".userInfoBar__buttons .sc-button-group",
        ".profileHeaderInfo__buttons .sc-button-group",
        ".userMain__headerButtons .sc-button-group",
        // Generic button group in user header
        ".soundHeader__actions .sc-button-group",
        ".header__actions .sc-button-group",
        // Fallback: any button group containing Follow/Station buttons
        ".sc-button-group:has(.sc-button-follow)",
        ".sc-button-group:has(.sc-button-station)",
      ];

      let par = null;
      for (const selector of selectors) {
        par = document.querySelector(selector);
        if (par) {
          console.log("[RSS] Found button container with selector:", selector);
          break;
        }
      }

      if (par && this.el.parentElement !== par) {
        par.insertAdjacentElement("beforeend", this.el);
      }
    },

    attach() {
      this.detach();
      this.observer = new MutationObserver(this.cb.bind(this));
      this.observer.observe(document.body, { childList: true, subtree: true });
      this.cb();
    },

    detach() {
      if (this.observer) {
        this.observer.disconnect();
      }
    },
  };

  btn.init();

  // ===================
  // Utility Functions
  // ===================

  function hook(obj, name, callback, type) {
    const fn = obj[name];
    obj[name] = function (...args) {
      if (type === "before") callback.apply(this, args);
      fn.apply(this, args);
      if (type === "after") callback.apply(this, args);
    };
    return () => {
      obj[name] = fn;
    };
  }

  function isUserPage() {
    const excludedPaths = [
      "/you",
      "/stations",
      "/discover",
      "/stream",
      "/upload",
      "/search",
      "/settings",
      "/messages",
      "/notifications",
      "/charts",
      "/people",
      "/pages",
      "/pro",
      "/jobs",
      "/creators",
      "/terms-of-use",
      "/privacy",
    ];

    const pathname = location.pathname;

    // Check if it's an excluded path
    for (const excluded of excludedPaths) {
      if (pathname.startsWith(excluded)) {
        return false;
      }
    }

    // User page pattern: /username or /username/tracks etc.
    // Should have at least one path segment that looks like a username
    const match = pathname.match(/^\/([^/]+)/);
    if (!match) return false;

    const username = match[1];
    // Username should not be empty
    return username.length > 0;
  }

  function extractUsername() {
    const match = location.pathname.match(/^\/([^/]+)/);
    return match ? match[1] : null;
  }

  function extractUserId() {
    const html = document.documentElement.innerHTML;
    const match = html.match(/soundcloud:\/\/users:(\d+)/);
    return match ? match[1] : null;
  }

  function buildRssFeedUrl(userId) {
    return `http://feeds.soundcloud.com/users/soundcloud:users:${userId}/sounds.rss`;
  }

  // ===================
  // Main Logic
  // ===================

  function load(by) {
    btn.detach();
    console.log("[RSS] load triggered by:", by, location.href);

    if (!isUserPage()) {
      console.log("[RSS] Not a user page, skipping");
      return;
    }

    const username = extractUsername();
    if (!username) {
      console.log("[RSS] Could not extract username");
      return;
    }

    console.log("[RSS] Detected user page for:", username);

    btn.el.onclick = async () => {
      const userId = extractUserId();
      if (!userId) {
        console.log("[RSS] Could not find user ID in page");
        btn.el.innerHTML = ICONS.error;
        setTimeout(() => {
          btn.el.innerHTML = ICONS.rss;
        }, 2000);
        return;
      }

      const rssUrl = buildRssFeedUrl(userId);
      console.log("[RSS] RSS Feed URL:", rssUrl);

      try {
        await navigator.clipboard.writeText(rssUrl);
        btn.el.innerHTML = ICONS.check;
      } catch (err) {
        console.error("[RSS] Failed to copy to clipboard:", err);
        btn.el.innerHTML = ICONS.error;
      }

      setTimeout(() => {
        btn.el.innerHTML = ICONS.rss;
      }, 2000);
    };

    btn.attach();
    console.log("[RSS] Button attached");
  }

  // ===================
  // Initialization
  // ===================

  load("init");
  hook(history, "pushState", () => load("pushState"), "after");
  window.addEventListener("popstate", () => load("popstate"));
})();