SOURCE / PINNED RELEASE
Made of little things.
Funclet
- Release
- 0b497cfc2f66…
- Author-recorded commit
- 1e531b24f490…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6b7a37cac1d
Archive hash verified: 71d97a89ed8c40c2…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/** Wavefunc interchange formats. Keep parsing independent from host authority. */
export const STATION_KIND = 31237;
export const FAVOURITES_KIND = 30078;
export const FAVOURITES_LABEL = 'wavefunc_user_favourite_list';
export const WAVEFUNC_RELAY = 'wss://relay.wavefunc.live';
export const WAVEFUNC_SERVER = {
pubkey: 'bb0707242a17a4be881919b3dcfea63f42aacedc3ff898a66be30af195ff32b2',
relays: [WAVEFUNC_RELAY, 'wss://relay2.contextvm.org/'],
};
export interface EventRecord {
id: string;
pubkey: string;
kind: number;
created_at: number;
tags: string[][];
content: string;
}
export interface Stream { url: string; format: string; bitrate?: number; primary: boolean }
export interface Station {
id: string;
address: string;
name: string;
description: string;
location: string;
genres: string[];
thumbnail: string;
streams: Stream[];
createdAt: number;
}
export interface FavouriteList {
id: string;
identifier: string;
name: string;
description: string;
addresses: string[];
createdAt: number;
}
export interface MusicResult { id: string; title: string; detail: string; kind: string }
export interface NowPlaying { title: string; artist: string; source: string }
export type SearchMode = 'stations' | 'recordings' | 'artists' | 'releases';
export function record(value: unknown): Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown> : {};
}
export function text(value: unknown, limit = 500): string {
return typeof value === 'string' ? value.trim().slice(0, limit) : '';
}
export function json(value: string): Record<string, unknown> {
try { return record(JSON.parse(value)); } catch { return {}; }
}
function tag(event: EventRecord, name: string): string {
return text(event.tags.find(t => t[0] === name)?.[1]);
}
export function httpsUrl(value: unknown): string {
try {
const url = new URL(text(value, 4096).replace(/^`|`$/g, ''));
return url.protocol === 'https:' && !url.username && !url.password ? url.href : '';
} catch { return ''; }
}
export function stationReference(address: string): { kind: number; author: string; identifier: string } | null {
const match = /^31237:([a-f0-9]{64}):(.+)$/i.exec(address);
return match ? { kind: STATION_KIND, author: match[1].toLowerCase(), identifier: match[2] } : null;
}
function stream(value: unknown): Stream | null {
const item = record(value);
const url = text(item.url, 4096).replace(/^`|`$/g, '');
if (!url) return null;
const bitrate = record(item.quality).bitrate;
return { url, format: (text(item.format) || text(record(item.quality).codec)).toLowerCase(), primary: item.primary === true,
bitrate: typeof bitrate === 'number' && Number.isFinite(bitrate) ? bitrate : undefined };
}
export function playableStreams(station: Station): Stream[] {
return station.streams.filter(s => httpsUrl(s.url) &&
/^(?:mp3|mpeg|ogg|opus|vorbis|wav|wave|audio\/(?:mpeg|mp3|ogg|opus|vorbis|wav|wave|x-wav))$/i.test(s.format.split(';')[0].trim()) &&
!/\.(m3u8?|pls|mpd)(?:\?|$)/i.test(s.url))
.sort((a, b) => Number(b.primary === true) - Number(a.primary === true));
}
/** HTTP is an external-link fallback only, never a MEDIA source. */
export function httpStreams(station: Station): Stream[] {
return station.streams.flatMap(stream => {
try {
const url = new URL(stream.url);
return url.protocol === 'http:' && !url.username && !url.password ? [{ ...stream, url: url.href }] : [];
} catch { return []; }
}).sort((a, b) => Number(b.primary === true) - Number(a.primary === true));
}
/** Wavefunc's station route accepts a raw event ID as well as naddr. */
export function wavefuncStationUrl(station: Station): string {
return /^[a-f0-9]{64}$/i.test(station.id) ? `https://wavefunc.live/station/${station.id.toLowerCase()}` : '';
}
export function parseStation(event: EventRecord): Station | null {
const identifier = tag(event, 'd');
const name = tag(event, 'name');
if (event.kind !== STATION_KIND || !identifier || !name || !/^[a-f0-9]{64}$/i.test(event.pubkey)) return null;
const content = json(event.content);
const tagged = event.tags.filter(t => t[0] === 'stream').map(t =>
stream({ url: t[1], format: t[2], quality: json(t[3] ?? ''), primary: t.includes('primary') }))
.filter((s): s is Stream => s !== null);
const streams = tagged.length ? tagged : (Array.isArray(content.streams) ? content.streams : [])
.map(stream).filter((s): s is Stream => s !== null);
// Other applications also use kind 31237 for album catalogues. Validate the
// Wavefunc schema instead of turning every addressable record into a station.
if (!streams.length) return null;
return {
id: event.id, address: `${STATION_KIND}:${event.pubkey}:${identifier}`,
name,
description: tag(event, 'description') || text(content.description, 2000),
location: [tag(event, 'location'), tag(event, 'country')].filter(Boolean).join(' · '),
genres: event.tags.filter(t => t[0] === 'c' && t[2] === 'genre').map(t => text(t[1])).filter(Boolean).slice(0, 5),
thumbnail: httpsUrl(event.tags.find(t => t[0] === 'thumbnail')?.[1]), streams, createdAt: event.created_at,
};
}
export function parseFavouriteList(event: EventRecord): FavouriteList | null {
const identifier = tag(event, 'd');
if (event.kind !== FAVOURITES_KIND || tag(event, 'l') !== FAVOURITES_LABEL || !identifier) return null;
const content = json(event.content);
return { id: event.id, identifier, name: tag(event, 'name') || text(content.name) || 'Favourite stations',
description: tag(event, 'description') || text(content.description), createdAt: event.created_at,
addresses: [...new Set(event.tags.filter(t => t[0] === 'a').map(t => t[1]).filter(a => stationReference(a)))],
};
}
export function latestBy<T extends { createdAt: number; id: string }>(items: T[], key: (item: T) => string): T[] {
const latest = new Map<string, T>();
for (const item of items) {
const previous = latest.get(key(item));
if (!previous || item.createdAt > previous.createdAt ||
(item.createdAt === previous.createdAt && item.id < previous.id)) latest.set(key(item), item);
}
return [...latest.values()];
}
/** MCP servers may return structured content or JSON text. Never display raw HTML. */
export function toolPayload(result: unknown): unknown {
const response = record(result);
if (response.isError) throw new Error('Wavefunc could not complete this request. Try again.');
let payload = response.structuredContent;
if (!payload && Array.isArray(response.content)) {
const block = response.content.find(b => record(b).type === 'text');
try { payload = JSON.parse(text(record(block).text, 500_000)); }
catch { throw new Error('Wavefunc returned an unreadable response.'); }
}
const data = record(payload);
if (data.error) throw new Error(text(data.error));
if (!('result' in data)) throw new Error('Wavefunc returned an unexpected response.');
return data.result;
}
export function parseMusic(payload: unknown, mode: SearchMode): MusicResult[] {
if (!Array.isArray(payload)) throw new Error('Wavefunc returned an unexpected search response.');
return payload.slice(0, 30).flatMap(item => {
const row = record(item);
const title = text(row.title) || text(row.name);
if (!title) return [];
return [{ id: text(row.id), title, kind: mode,
detail: [text(row.artist), text(row.release), text(row.country), text(row.disambiguation), text(row.date)].filter(Boolean).join(' · ') }];
});
}
export function parseNowPlaying(payload: unknown): NowPlaying | null {
const data = record(payload);
if (data.source === 'UNKNOWN' || data.method === 'none') return null;
const enriched = record(data.enriched);
const useEnriched = enriched.confidence === 'high' || enriched.confidence === 'medium';
const title = (useEnriched ? text(enriched.title) : '') || text(data.song) || text(data.title);
if (!title) return null;
return { title, artist: (useEnriched ? text(enriched.artist) : '') || text(data.artist), source: text(data.source) };
}
