Step-by-step guide to add a new streaming music platform to StreamThenOwn
Use this skill when the user asks to add support for a new streaming service (e.g. Tidal, SoundCloud, Pandora…).
Each platform is a pluggable adapter in src/platforms/<platform>/ with 3 files:
| File | Implements | Purpose |
|---|---|---|
metadata.ts | MetadataExtractor | Scrapes artist/album from the service's DOM |
ui.ts | UIInjector | Injects the STO button & dropdown, styled to match the service |
index.ts | PlatformAdapter | Combines metadata + UI + SPA navigation observer |
Interfaces are defined in src/platforms/types.ts.
Before writing code, inspect the target platform's DOM in DevTools:
extractSong()pushState, popstate, or a custom router?src/platforms/<platform>/metadata.tsImplement MetadataExtractor. Key rules:
source: "album" for album pages, source: "song" for track/player baralbum is optional — some pages only show artist + trackextractSong() for persistent player barsel?.textContent?.trim())/intl-fr/album/…), extract and include locale in the metadataReference: src/platforms/deezer/metadata.ts is a clean, mid-complexity example.
src/platforms/<platform>/ui.tsImplement UIInjector. Key rules:
document.createElement() exclusively — never innerHTMLsrc/stores/icons.ts (createStoreIcon(), createButtonIcon())injectButton() method must be idempotent (no-op if button already exists)waitForElement() from src/utils/dom.ts to wait for the target containercloseMenu() to dismiss the dropdown when clicking outsidesto-<platform>- (e.g. sto-dz-btn, sto-sp-menu)Reference: src/platforms/deezer/ui.ts or src/platforms/spotify/ui.ts.
src/platforms/<platform>/index.tsImplement PlatformAdapter. Standard pattern:
import type { PlatformAdapter } from "../types";
import { <Platform>MetadataExtractor } from "./metadata";
import { <Platform>UIInjector } from "./ui";
export class <Platform>Adapter implements PlatformAdapter {
readonly name = "<Platform Name>";
readonly metadata = new <Platform>MetadataExtractor();
readonly ui = new <Platform>UIInjector();
observeNavigation(onNavigate: () => void): void {
let lastUrl = location.href;
window.addEventListener("popstate", () => {
if (location.href !== lastUrl) {
lastUrl = location.href;
onNavigate();
}
});
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
onNavigate();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
In src/platforms/index.ts:
PLATFORM_MAPpublic/manifest.jsonAdd a new entry to the content_scripts array:
{
"matches": ["*://<hostname>/*"],
"js": ["content/index.js"],
"css": ["styles/<platform>.css"],
"run_at": "document_idle"
}
public/styles/<platform>.csssto-<abbrev>- (e.g. sto-td- for Tidal)!important, max specificity = 1 class + 1 elementpublic/styles/deezer.css as referenceCreate both:
src/platforms/<platform>/metadata.test.ts — test all extraction strategies, edge cases, null returnssrc/platforms/<platform>/ui.test.ts — test button injection, menu creation, cleanup, idempotencyTest DOM pattern: use document.createElement + DocumentFragment — never innerHTML. See src/platforms/deezer/metadata.test.ts for the standard pattern.
README.mdREADME.mdPRIVACY_POLICY.mdextensionDescription in all 15 locale files (public/_locales/*/messages.json) if the description lists platforms by nameRun make validate — everything must pass (typecheck, lint, stylelint, format, knip, tests, build).
MutationObserver + popstate.waitForElement().element.shadowRoot?.querySelector() if needed./intl-fr/, Apple Music /us/album/).