Skip to content

Adding a New Music Site

KanashiiDev edited this page Jul 11, 2026 · 8 revisions

This guide explains how to create parsers that extract song information (title, artist, album art, duration) from any music website and display it on Discord.


Which Method Should You Choose?

Method Requires Code? When to use?
UI (Popup) No If you want a quick setup and point-and-click configuration is enough for the site
UserScript Yes (JS) If you know JavaScript and the UI Method isn't enough for the site
Build Method Yes (JS) If you're a developer and want to add a built-in website

Domain Matching

You determine which pages the parser will run on using the domain. It is important to understand this before moving on to setup.

Exact Match

The parser will only run on the exact domain you specify.

example.com   →   only runs on example.com

Wildcard Match

You can cover all subdomains of a domain by adding *.. But beware: the root domain is not included.

*.example.com   →   runs on subdomains like player.example.com, app.example.com
                →   does not run on example.com

This is useful for sites where every user or page has its own subdomain.

Multiple Domains

You can target multiple domains with a single parser:

// In UI and UserScript methods
site.com, app.site.com
// Only in Build Method
domain: ["site.com", "app.site.com"];

URL Patterns

If you want to restrict your parser to specific pages, you use URL patterns. These are regexes matched against the page path.

.*              →  all pages (default)
track/.*        →  only paths containing /track/
album/.*        →  only paths containing /album/

To use multiple patterns, separate them with a comma:

track/.*, album/.*

If you leave the pattern field blank (or type .*), it matches all pages on the domain.


Supported Selector Capabilities

The selector system supports the following CSS selector strategies:

Show
  • ID selectors
    #id 
  • Class selectors

    .class
    .class1.class2
  • Tag selectors

    div
    button
  • Attribute selectors

    [data-testid="button"]
    [class*="button"]
    [data-testid^="submit"]
    [href$=".pdf"]
  • Partial class matching

    .product-card_
    .title_
  • Hierarchical selectors

    A > B
    A B
    A > B > C
  • Sibling selectors

    A + B
    A ~ B
  • Hybrid selectors

    #main > .item
    [data-testid="card"] .title
    .product-card_ > .title_
  • Context-aware selectors

    header > .search-input
    footer > .search-input
  • Position-based selectors

    A:nth-child(n)
    A:nth-of-type(n)


Option 1 - UI Method (No Code)

The fastest way. You select elements on the page by clicking them; no code is required.

Steps

  1. Open the music site in your browser.
  2. Click the extension icon and press the "Add Music Site" button.
  3. In the panel that opens, click the "+" icon for each field, then click the corresponding element on the page:
  • Title (required) - song title
  • Artist (optional) - artist name
  • Image (optional) - album art
  • Time Passed (optional) - current playback position
  • Duration (optional) - total track duration
  • Source (optional) - the label to be displayed on Discord
  • Play Status (optional) - an element or attribute that is only present when music is playing; used for cast support
  1. Select the selector in the "Most Stable Selector" section.
  2. Check the Domain field:
  • The current page's domain is auto-filled.
  • Change it to *.example.com to cover all subdomains.
  • Separate with commas to target multiple domains: site.com, app.site.com
  1. Click "Save" and refresh the page.

Common Scenarios

Artist and title in the same element: Add the same selector to both fields. The extension splits them automatically (Normalization must be enabled in settings).

Only Duration is available: Add only the Duration field. The extension will estimate the playback time from the moment the track changes.

Time Passed and Duration are combined (e.g., 0:12 / 2:20): Add the same selector to both fields.

Link field: If left blank, the current page URL is automatically used.

Regex field: Leave blank to match all pages, or enter a pattern to restrict the parser to specific pages.


Option 2 - UserScript Method

You can write a custom JavaScript parser with the built-in UserScript Manager to access the page's DOM and make API calls.

How to Add a UserScript

  1. Click "Open Script Manager" in the extension popup.
  2. Click "+ New Script".
  3. Fill in the fields:
  • Script Name - name of the parser (e.g., My Music Parser)
  • Description - A brief note about the target website (optional)
  • Authors - comma-separated author names (optional)
  • Domain(s) - comma-separated list of domains where the script runs (e.g., example.com or example.com,example2.com)
  • URL Pattern(s) - comma-separated regex patterns for matching URLs (e.g., player.* or player.*,.song.*). Use .* for all pages.
  1. Write the script code (see below).
  2. Click "Save Script".

Required Variables

Your script must assign values to the required variables using let. The engine automatically reads these variables as output.

Do not use return at the top-level script scope. return is allowed inside functions.

let title = ""; // Song title
let artist = ""; // Artist name
let image = ""; // Album art URL (or null)
let source = ""; // Label to show on Discord (e.g., "My Radio")
let songUrl = ""; // Direct URL of the current track
let timePassed = null; // Playback position in seconds (or null)
let duration = null; // Total track duration in seconds (or null)

Optional Variables

let isPlaying = false; // Whether music is currently playing (for cast support)
let buttons = [
  // Maximum 2 buttons shown on Discord RPC
  { link: "https://...", text: "Open Track" },
  { link: "https://...", text: "View Artist" },
];

Minimal Example

The example below shows what a basic parser looks like. Each line explains what is being done:

// Get song title from page; returns empty string if not found
let title = document.querySelector(".track-title")?.textContent?.trim() ?? "";

// Get artist name
let artist = document.querySelector(".artist-name")?.textContent?.trim() ?? "";

// Get the URL of the album art image
let image = document.querySelector(".album-art")?.src ?? null;

// Label to display on Discord
let source = "My Music Site";

// URL of the current page
let songUrl = window.location.href;

// Playback position and duration - fill if available on your site, otherwise leave null
let timePassed = null;
let duration = null;

// If the audio element is not paused, music is playing
let isPlaying = !document.querySelector("audio")?.paused;

// Button to display on Discord
let buttons = [{ link: songUrl, text: "Open Track" }];

Fetching Data from an API

Some sites provide song information through an API rather than in the DOM. In this case, you can use fetch:

let title = "";
let artist = "";
let image = null;
let source = "My Music Site";
let songUrl = window.location.href;
let timePassed = null;
let duration = null;

// Call the site's "now playing" endpoint
const response = await fetch("https://api.example.com/now-playing");
const data = await response.json();

// Fill fields from the incoming data
title = data.title ?? "";
artist = data.artist ?? "";
timePassed = data.position ?? null;
duration = data.duration ?? null;

Custom Settings

You can add settings that appear in the extension popup (toggle, text input, dropdown menu):

// Define settings that the user can change from the popup
const showArtist = await useSetting("showArtist", "Show Artist Name", "checkbox", true);
const customLabel = await useSetting("customLabel", "Source Label", "text", "My Station");

let title = document.querySelector(".track-title")?.textContent?.trim() ?? "";

// Show or hide the artist based on the setting
let artist = showArtist ? (document.querySelector(".artist-name")?.textContent?.trim() ?? "") : "";

let source = customLabel;

Setting types: "text", "checkbox", "select"

Use the "Generate Settings" button in the Script Manager to automatically create these lines.

iframeSelectors

Some media players run inside an iframe. Since functions cannot be serialized in UserScripts, iframeSelectors provides a structured DOM-based extraction system that runs inside the iframe context and returns a plain object through getIframeData().

Important

If you use iframeSelectors, the Auto Detect Video Status option will be ignored due to technical limitations and to avoid data conflicts.

If you still want to use the auto-detection engine, you must explicitly include the $video macro key inside your fields object:

{
  "fields": {
    "title": {
      "type": "text",
      "selector": ".custom-track-title"
    },
    "$video": {
      "type": "video"
    }
  }
}

Usage

const iframe = await getIframeData();

const title = getText(".title");
const playing = iframe?.playing;
const time = iframe?.currentTime;

Field Types & Parameters

Show
video

Returns { duration, currentTime, paused } from the video element on the page.

When used with the special $video key, the result is automatically assigned to the corresponding variables.

{
  "fields": {
    "video": {
      "type": "video"
    }
  }
}
exists

Parameters: selector

Returns true if an element matching the selector exists; otherwise false.

{
  "type": "exists",
  "selector": ".playing"
}
not

Parameters: selector

Returns true if no matching element exists; otherwise false.

{
  "type": "not",
  "selector": ".loading"
}
text

Parameters: selector, trim?

Extracts textContent from an element.

  • Default: trim: true
  • Set trim: false to preserve original spacing
{
  "type": "text",
  "selector": "#track-title",
  "trim": false
}
attr

Parameters: selector, attr

Returns the raw attribute value.

{
  "type": "attr",
  "selector": ".player",
  "attr": "data-id"
}
aria

Parameters: selector, attr

Alias of attr. Reads an attribute from the element, typically used for ARIA attributes. Returns the raw string value.

{
  "type": "aria",
  "selector": ".play-btn",
  "attr": "aria-label"
}
src

Parameters: selector

Returns the .src property of media elements.

{
  "type": "src",
  "selector": "video"
}
href

Parameters: selector

Returns the resolved absolute URL from an anchor element.

{
  "type": "href",
  "selector": "a.track-link"
}
number

Parameters: selector

Parses textContent using parseFloat().

Returns:

  • number if valid
  • null otherwise
{
  "type": "number",
  "selector": ".rating"
}
time

Parameters: selector

Parses time strings like MM:SS or HH:MM:SS into seconds.

Invalid values return null, including:

  • --:--
  • -:-
  • empty strings
  • malformed numbers
{
  "type": "time",
  "selector": ".current-time"
}
classContains

Parameters: selector, class

Checks if element contains required CSS classes.

  • string → single class check
  • array → all classes must exist
{
  "type": "classContains",
  "selector": ".volume-btn",
  "class": ["icon-mute", "active"]
}
dataset

Parameters: selector, key

Returns element.dataset[key].

{
  "type": "dataset",
  "selector": ".track",
  "key": "trackId"
}
meta

Parameters: name

Reads a meta tag by name or property and returns its content.

  • <meta name="">
  • <meta property="">

Returns content.

{
  "type": "meta",
  "name": "og:title"
}
count

Returns the number of matching elements:

document.querySelectorAll(selector).length;
{
  "type": "count",
  "selector": ".playlist-item"
}
or

Parameters: rules

Evaluates rules in order and returns the first valid result.

A value is considered valid if it is:

  • not null
  • not undefined
  • not an empty string ("")

⚠️ 0 and false are treated as valid values and will stop evaluation.

If no rule returns a valid value, the result is null.

{
  "type": "or",
  "rules": [
    {
      "type": "prop",
      "path": "player.getTime"
    },
    {
      "type": "time",
      "selector": ".current-time-display"
    }
  ]
}
prop

Parameters: path

Resolves a value from the global window object using dot-notation path traversal.

If any part of the path is missing, the result is null.

If the resolved value is a function, it is executed immediately:

  • without arguments
  • without preserving this context

If function execution throws an error, the result is null.

{
  "type": "prop",
  "path": "jwplayer.getPosition"
}

Option 3 - Build Method (Developers)

You add the parser directly into the extension's source code. This is the most powerful option and provides access to all advanced features.

Step 1 - Create the Parser File

In the project folder, go to the extension/parsers/ directory and create a new .js file named after the site (e.g., myMusicSite.js).

Step 2 - Register the Parser

registerParser({
  // Required
  domain: "example.com", // or "*.example.com" or ["site.com", "app.site.com"]
  title: "Example",
  category: "platform", // "radio" | "platform" | "aggregator"

  // Optional
  homepage: "https://example.com",
  description: "Parser for Example Music.",
  authors: ["yourName"],
  authorsLinks: ["https://github.com/yourName"],
  tags: ["rock", "community"],
  urlPatterns: [/.*/],
  mode: "listen", // "listen" | "watch"

  fn: async function ({ accessWindow, useSetting, iframeData }) {
    return {
      title: getText(".now-playing-title"), // song title
      artist: getText(".now-playing-artist"), // artist name
      image: getImage("img.album_art"), // album art
      timePassed: getText(".time-display-played"),
      duration: getText(".time-display-total"),
      source: "Example",
      songUrl: "https://example.com/song",
      isPlaying: Boolean(document.querySelector("#pauseSongButton")),
      buttons: [{ text: "Listen Along", link: "https://example.com/song" }],
    };
  },
});

Advanced: Custom Settings (useSetting)

fn: async function ({ useSetting }) {
  const showArtist = await useSetting("showArtist", { en: "Show Artist Name" }, "checkbox", true);
  const customLabel = await useSetting("customLabel", { en: "Custom Source Label" }, "text", "My Station");
  const quality = await useSetting("quality", { en: "Stream Quality" }, "select", [
    { value: "high", label: { en: "High" }, selected: true },
    { value: "medium", label: { en: "Medium" } },
  ]);

  return {
    source: showArtist ? customLabel : "",
  };
},

Advanced: Reading Page JavaScript Variables (accessWindow)

Some sites store track data in JavaScript variables instead of the DOM. You can read them using accessWindow:

fn: async function ({ accessWindow }) {
  // Get current audio info from the site's global player object
  const audio = await accessWindow("player.getCurrentAudio");

  return {
    title:  audio?.title  ?? "",  // returns empty string if not found
    artist: audio?.artist ?? "",
  };
},

accessWindow returns null if the property does not exist or throws an error. Always keep this in mind.

Advanced: Reading Data from Iframe (iframeFn)

If the player runs inside an iframe, you can extract data from there using iframeFn:

registerParser({
  domain: "example.com",
  title: "Example",
  category: "platform",
  iframeOrigins: ["player.example.com"],

  fn: async function ({ iframeData }) {
    if (!iframeData) return {};

    return {
      title: iframeData.title,
      isPlaying: iframeData.isPlaying,
      timePassed: String(iframeData.currentTime),
      duration: String(iframeData.duration),
    };
  },

  // This function runs inside the iframe
  iframeFn: function () {
    const video = document.querySelector("video");
    return {
      title: document.querySelector(".track-title")?.textContent ?? "",
      isPlaying: !video?.paused,
      currentTime: video?.currentTime,
      duration: video?.duration,
    };
  },
});

iframeData.lastValidValues holds the latest valid values for each field. Use it as a fallback when data is temporarily unavailable.

Helper Functions

getText(selector, options?)

Reads text content or an attribute from a DOM element.

Parameter Type Description
selector string CSS selector (e.g., ".now-playing-title")
options.attr string Read attribute instead of text (e.g., "href", "title")
options.transform function Function that modifies the result before returning it
options.root Element Search within a specific element instead of the whole page
getText(".now-playing-title");
getText(".song-link", { attr: "href" });
getText(".song-link", { attr: "href", transform: (v) => v.replace(/^\//, "") });

getImage(selector, root?)

Extracts an image URL from a DOM element.

Search priority: direct <img> src → CSS background-image → first child <img> src

getImage(".album-cover img");
getImage(".hero-banner"); // works with background-image too
getImage(".track-card"); // also checks the first child img

getTextAll(selector, options?)

Same as getText, but returns an array for all matching elements.

getTextAll(".track-list .title");
// → ["Song 1", "Song 2", "Song 3"]

getImageAll(selector, root?)

Same as getImage, but returns an array of image URLs for all matching elements.

getImageAll(".playlist-item img");
// → ["cover1.jpg", "cover2.jpg", "cover3.jpg"]

querySelectorDeep(selector, root?, all?)

Searches for elements in the normal DOM and all nested Shadow DOM trees.

Standard querySelector() cannot reach inside Web Components or sites that encapsulate elements within a shadowRoot; this function solves that problem.

Parameter Type Description
selector string CSS selector to search for
root Document - ShadowRoot - Element Root node to start searching. Default: document
all boolean If true, returns all matching elements as an array. Otherwise, returns the first match
querySelectorDeep(".play-button");
querySelectorDeep(".track-title", someShadowRoot);
querySelectorDeep(".menu-item", document, true);

Frequently Asked Questions

My parser only works on one subdomain, I want it to work on all of them.

Change the domain to *.example.com. This matches all subdomains (like app.example.com, player.example.com), but the root domain example.com is not included.

The site uses different layouts based on the URL. How do I handle this?

Use UserScript or Build Method and check the URL path inside the code:

const path = window.location.pathname;
let title = "";

if (path.startsWith("/track/")) {
  // Title element on the track page
  title = document.querySelector(".track-title")?.textContent?.trim() ?? "";
} else if (path.startsWith("/album/")) {
  // Title element on the album page
  title = document.querySelector(".album-title")?.textContent?.trim() ?? "";
}

I added timePassed and duration but the progress bar does not appear.

Make sure the values are numbers in seconds or strings in "m:ss" format (e.g., "1:23"). Sending null or an empty string disables the progress bar.

My parser runs on pages where it shouldn't.

Restrict it using URL patterns. For example, player.* only matches paths starting with /player. You can combine multiple patterns with a comma: track/.*, album/.*

Buttons do not appear on Discord.

The Discord desktop application does not show buttons. To test them, log into Discord via a browser using a separate account.

What does isPlaying do?

It controls cast support. Return true if music is actively playing, and false when paused or stopped.

// From audio element
let isPlaying = !document.querySelector("audio")?.paused;

// From visible pause button (if the pause button is visible, music is playing)
let isPlaying = Boolean(document.querySelector(".pause-button"));

Clone this wiki locally