musicsocket
v0.1.2
Published
Universal client SDK for musicsocket — realtime now-playing for any framework, Node, Bun, or a plain <script> tag. SSE/WS/REST with auto-fallback, ready-made widgets, and OAuth2 helpers.
Maintainers
Readme
musicsocket
Universal client SDK for a musicsocket server. One install gives you
realtime "now playing" in any frontend framework, Node, Bun, or a plain
<script> tag — with automatic transport negotiation (SSE → WebSocket → REST
polling), ready-made themeable widgets, and OAuth2 ("Log in with musicsocket")
helpers.
npm install musicsocketWhy
- Write the realtime engine once. All the transport, reconnect, fallback and parsing logic lives in the framework-agnostic core. Every framework adapter is a thin shim that maps one reactive store onto the platform's native reactivity.
- Degrades gracefully. Asks for the best channel the runtime supports and silently falls back / reconnects. Worst case: 5s REST polling, everywhere.
- Pick your import.
musicsocket/react,/vue3,/svelte5,/solid, … or a global<script>build.
Quick start (core)
import { MusicSocket } from "musicsocket";
const ms = new MusicSocket("https://msk.armagan.rest");
const sub = ms.watch(["acc_1", "acc_2:privatekey"], { transport: "auto", live: true });
const unsubscribe = sub.subscribe((state) => {
console.log(state.status, state.transport, state.entries);
});
// later
unsubscribe();
sub.close();Framework adapters
| Import | Primitive |
| --- | --- |
| musicsocket/react · /preact · /reactnative | useNowPlaying(source, refs, opts) |
| musicsocket/vue3 · /vue2 | useNowPlaying(...) → ref |
| musicsocket/svelte5 · /svelte4 | nowPlaying(...) store ($np) |
| musicsocket/svelte5/runes | useNowPlaying(...) runes ($state, no $) |
| musicsocket/solid | createNowPlaying(...) accessor |
| musicsocket/lit | NowPlayingController |
| musicsocket/angular | injectNowPlaying(...) signal · nowPlaying$(...) |
| musicsocket/astro · /native · /widgets | custom elements + global build |
// React
import { useNowPlaying, useProgress } from "musicsocket/react";
function Card() {
const { entries } = useNowPlaying("https://msk.armagan.rest", "acc_1", { live: true });
const np = entries["acc_1"]?.nowPlaying;
const { fraction } = useProgress(np ?? null);
return np ? <p>{np.title} — {np.artist} ({Math.round(fraction * 100)}%)</p> : <p>Idle</p>;
}<!-- Svelte (store API, works in 4 and 5) -->
<script>
import { nowPlaying } from "musicsocket/svelte5";
const np = nowPlaying("https://msk.armagan.rest", "acc_1", { live: true });
</script>
{#if $np.entries.acc_1?.nowPlaying}
{$np.entries.acc_1.nowPlaying.title}
{/if}<!-- Svelte 5 runes ($state — no $ prefix) -->
<script>
import { useNowPlaying } from "musicsocket/svelte5/runes";
const np = useNowPlaying("https://msk.armagan.rest", "acc_1", { live: true });
</script>
{#if np.entries.acc_1?.nowPlaying}
{np.entries.acc_1.nowPlaying.title} ({np.status})
{/if}// Angular — signals via DI (auto-cleans on destroy)
import { Component } from "@angular/core";
import { injectNowPlaying, MUSICSOCKET_BASE_URL } from "musicsocket/angular";
// app config: provide { provide: MUSICSOCKET_BASE_URL, useValue: "https://msk.armagan.rest" }
@Component({
template: `{{ np().entries['acc_1']?.nowPlaying?.title ?? 'Idle' }}`,
})
export class NowPlayingCard {
np = injectNowPlaying("acc_1", { live: true }); // Signal<PresenceState>
}
// Or as an Observable for the async pipe: nowPlaying$(baseUrl, "acc_1")Watching multiple accounts
One subscription can watch many accounts at once; pull each out by id.
import { useNowPlaying, useNowPlayingEntries, pickEntry } from "musicsocket/react";
// whole state (entries is a map keyed by account id)
const { entries } = useNowPlaying("https://msk.armagan.rest", ["acc_1", "acc_2", "acc_3:key"]);
// or aligned to your refs
const [a, b, c] = useNowPlayingEntries("https://msk.armagan.rest", ["acc_1", "acc_2", "acc_3:key"]);pickEntry / pickEntries work on any adapter's state (and on the core
PresenceState), so Vue/Solid/Svelte/Lit/Angular get the same selectors.
One shared connection
Every watch() / hook against the same server shares a single connection, no
matter how many components subscribe. Internally a connection hub keeps one
transport subscribed to the union of all watched accounts and fans updates out
to each watcher; it adds/removes accounts live (WS subscribe/unsubscribe ops, SSE/
poll re-query) and closes the connection automatically once nothing is watching.
So 50 <music-socket-card>s on a page = 1 socket, not 50.
Custom labels / language
Every widget's texts are overridable — pick a built-in language (en, tr, or
register your own with addLang) and/or override individual labels. This works on
server-rendered widgets (forwarded as query params) and the client-rendered
<music-socket-card>.
// URL builders / framework widgets
ms.svgUrl("acc_1", { lang: "tr", labels: { idle: "Müzik yok", nowPlaying: "Şu an" } });
import { resolveLabels, addLang } from "musicsocket";
addLang("de", { nowPlaying: "LÄUFT", paused: "PAUSIERT", idle: "Nichts läuft", loading: "Lädt…" });
const labels = resolveLabels("de", { idle: "Stille" });<!-- custom texts on any element -->
<music-socket-card base-url="https://msk.armagan.rest" account="acc_1"
lang="tr" label-idle="Şu an sessizlik"></music-socket-card>
<music-socket-svg base-url="https://msk.armagan.rest" account="acc_1"
label-playing="ŞİMDİ" label-idle="Boşta"></music-socket-svg>Plain <script> / no build step
<script src="https://unpkg.com/musicsocket/dist/native.global.js"></script>
<music-socket-card base-url="https://msk.armagan.rest" account="acc_1"></music-socket-card>
<script>
const ms = new musicsocket.MusicSocket("https://msk.armagan.rest");
ms.watch("acc_1").subscribe((s) => console.log(s.entries));
</script>Widgets
URL builders for the server's visual endpoints, plus custom elements:
ms.svgUrl("acc_1", { theme: "midnight", layout: "compact" }); // streaming SVG
ms.pngUrl("acc_1", { theme: "spotify" }); // static PNG
ms.iframeUrl("acc_1"); // hosted embed<music-socket-svg base-url="https://msk.armagan.rest" account="acc_1" theme="nord"></music-socket-svg>OAuth2 ("Log in with musicsocket")
const oauth = ms.oauth({ clientId: "app_123", redirectUri: location.origin + "/cb", scope: ["profile", "accounts.read"] });
// 1) kick off (PKCE handled for you)
await oauth.redirectToAuthorization();
// 2) on your redirect page
const tokens = await oauth.handleRedirect();
const info = await oauth.getUserInfo(tokens.access_token);Transport negotiation
auto tries SSE, then WebSocket, then REST polling (pollIntervalMs,
default 5000). Unsupported transports are skipped by capability detection; dropped
connections retry with capped backoff and downgrade after repeated failure.
License
MIT
