// SomaFM source adapter — implements the RadioSource contract from // base-source.js. SomaFM is a listener-supported indie radio network // that publicly exposes a station catalogue (channels.json) and per- // station playlist files (.pls) — no API key required. // // Reference: https://somafm.com/ import { parsePls } from '../lib/playlist-parser.js'; const CHANNELS_URL = 'https://somafm.com/channels.json'; /** @type {import('./base-source.js').RadioSource} */ export const somaFM = { id: 'somafm', displayName: 'SomaFM', async listStations() { const res = await fetch(CHANNELS_URL, { cache: 'no-store' }); if (!res.ok) throw new Error(`SomaFM channels.json HTTP ${res.status}`); const data = await res.json(); const channels = Array.isArray(data?.channels) ? data.channels : []; return channels.map(ch => ({ id: `somafm:${ch.id}`, sourceId: 'somafm', name: ch.title || ch.id, description: ch.description || '', genre: ch.genre || '', artworkUrl: ch.image || '', // We don't resolve the stream here — that happens lazily on play // (one extra HTTP fetch per station change, beats ~150 upfront). streamUrls: [], // Keep the raw playlists list around so resolveStreamUrl can pick // the best one without re-fetching the catalogue. _playlists: Array.isArray(ch.playlists) ? ch.playlists : [], })); }, async resolveStreamUrl(station) { // Prefer mp3 + highest available quality. SomaFM offers aac too, // but mp3 has the broadest browser support. const playlists = station._playlists || []; const preferred = playlists.find(p => p.format === 'mp3' && p.quality === 'highest') || playlists.find(p => p.format === 'mp3') || playlists[0]; if (!preferred?.url) { throw new Error(`No playable playlist for ${station.name}`); } const res = await fetch(preferred.url, { cache: 'no-store' }); if (!res.ok) throw new Error(`SomaFM .pls HTTP ${res.status}`); const text = await res.text(); const streams = parsePls(text); if (!streams.length) { throw new Error(`Empty playlist for ${station.name}`); } return streams[0]; }, async getNowPlaying(station) { // SomaFM publishes per-channel song history at // https://somafm.com/songs/{shortId}.json // The first entry is the current track. const shortId = station.id.split(':')[1]; if (!shortId) return null; try { const res = await fetch(`https://somafm.com/songs/${shortId}.json`, { cache: 'no-store' }); if (!res.ok) return null; const data = await res.json(); const songs = Array.isArray(data?.songs) ? data.songs : []; const cur = songs[0]; if (!cur) return null; return { title: cur.title || '', artist: cur.artist || '', album: cur.album || '', }; } catch { return null; } }, };