diff --git a/.gitignore b/.gitignore index 629f70c..8038817 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,6 @@ docs/superpowers/ # claude code per-project state .claude/ -# design handoff bundle from claude design — local reference only -.design-prototype/ - # generated configs and secrets config/*.env !config/*.env.example diff --git a/config/frontend.env.example b/config/frontend.env.example deleted file mode 100644 index fb71aa0..0000000 --- a/config/frontend.env.example +++ /dev/null @@ -1,5 +0,0 @@ -NODE_ENV=production -HOST=0.0.0.0 -PORT=3000 -LIBRARY_ROOT=/library -DATA_ROOT=/now-playing diff --git a/config/liquidsoap.liq b/config/liquidsoap.liq index 52e7d88..fab96fa 100644 --- a/config/liquidsoap.liq +++ b/config/liquidsoap.liq @@ -9,24 +9,13 @@ source_pw = environment.get(default="", "SOURCE_PW") def emit_now_playing(station, m) = filename = m["filename"] if filename != "" then - # decoder-reported metadata may be empty; fall back to request.duration which - # reads it directly from the file. -1.0 means unknown. - meta_dur = m["duration"] - dur_str = - if meta_dur != "" then - meta_dur - else - # request.duration returns float? in liquidsoap 2.3 — unwrap with default -1. - d = null.get(default=-1., request.duration(filename)) - if d > 0. then string(int_of_float(d)) else "" end - end payload = json() payload.add("station", station) payload.add("artist", m["artist"]) payload.add("title", m["title"]) payload.add("album", m["album"]) payload.add("filename", filename) - payload.add("duration", dur_str) + payload.add("duration", m["duration"]) payload.add("started_at", string(int_of_float(time()))) file.write(data=payload.stringify(), atomic=true, temp_dir="/now-playing", "/now-playing/#{station}.json") end diff --git a/docker-compose.yml b/docker-compose.yml index 55cf2b2..1bbd3a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,26 +45,3 @@ services: timeout: 5s retries: 3 start_period: 30s - - frontend: - build: ./frontend - image: denpa-radio-frontend:latest - restart: always - env_file: config/frontend.env - ports: - - '172.17.0.1:12001:3000' - volumes: - - type: bind - source: /mnt/trashbox/denpa-radio/library - target: /library - read_only: true - - type: bind - source: ./data/now-playing - target: /now-playing - read_only: true - healthcheck: - test: ['CMD', 'wget', '-q', '-O', '-', 'http://127.0.0.1:3000/api/stations.json'] - interval: 30s - timeout: 5s - retries: 3 - start_period: 15s diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index de131f2..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:22-alpine AS deps -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci - -FROM node:22-alpine AS build -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . -RUN npm run build - -FROM node:22-alpine AS runtime -WORKDIR /app -ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000 -COPY --from=build /app/dist ./dist -COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/package.json ./ -USER node -EXPOSE 3000 -CMD ["node", "./dist/server/entry.mjs"] diff --git a/frontend/src/components/FooterStrip.astro b/frontend/src/components/FooterStrip.astro deleted file mode 100644 index dc851fe..0000000 --- a/frontend/src/components/FooterStrip.astro +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - diff --git a/frontend/src/components/HeroPlayer.tsx b/frontend/src/components/HeroPlayer.tsx deleted file mode 100644 index 3fc68cb..0000000 --- a/frontend/src/components/HeroPlayer.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; -import type { Station, NowPlaying, IcecastStatusJson } from '@lib/types'; -import { Tape } from './Tape'; -import { Spectrum } from './Spectrum'; -import { playerStore } from '@lib/store'; -import { fmtMs, fmtJpDate, fmtJpTime } from '@lib/format'; - -const PUBLIC_ORIGIN = 'https://denpa.femboy.page'; -const POLL_NOW_MS = 5_000; -const POLL_LISTENERS_MS = 30_000; - -interface Props { - initialStations: Station[]; -} - -type Format = 'mp3' | 'opus'; - -const lsGet = (k: string): string | null => { - try { return localStorage.getItem(k); } catch { return null; } -}; -const lsSet = (k: string, v: string) => { - try { localStorage.setItem(k, v); } catch { /* ignore */ } -}; - -export function HeroPlayer({ initialStations }: Props) { - // ssr-safe defaults; localStorage + Date reads happen post-mount to avoid - // hydration mismatches (#418, #423, #425). - const [stations, setStations] = useState(initialStations); - const [selectedId, setSelectedId] = useState(initialStations[0]?.id ?? ''); - const [format, setFormat] = useState('mp3'); - const [vol, setVol] = useState(0.72); - const [muted, setMuted] = useState(false); - const [playing, setPlaying] = useState(false); - const [now, setNow] = useState(null); - const [listeners, setListeners] = useState(null); - const [elapsed, setElapsed] = useState(0); - const [copyState, setCopyState] = useState<'idle' | 'ok'>('idle'); - const [clockText, setClockText] = useState<{ date: string; time: string }>({ date: '', time: '' }); - - // restore persisted UI state once on the client - useEffect(() => { - const savedStation = lsGet('denpa:station'); - if (savedStation && initialStations.some((s) => s.id === savedStation)) { - setSelectedId(savedStation); - } - if (lsGet('denpa:format') === 'opus') setFormat('opus'); - const savedVol = parseFloat(lsGet('denpa:vol') ?? ''); - if (!isNaN(savedVol)) setVol(Math.max(0, Math.min(1, savedVol))); - }, [initialStations]); - - const audioRef = useRef(null); - const station = stations.find((s) => s.id === selectedId); - const streamUrl = station ? `${PUBLIC_ORIGIN}${station.mounts[format]}` : ''; - - // refresh stations periodically (cheap; backend caches) - useEffect(() => { - let aborted = false; - const refresh = async () => { - try { - const r = await fetch('/api/stations.json'); - if (!r.ok) return; - const ss = await r.json() as Station[]; - if (!aborted && ss.length) { - setStations(ss); - if (!ss.some((s) => s.id === selectedId)) { - const first = ss[0]; - if (first) setSelectedId(first.id); - } - } - } catch { /* ignore */ } - }; - const id = setInterval(refresh, 60_000); - return () => { aborted = true; clearInterval(id); }; - }, [selectedId]); - - // share audio element with Spectrum - useEffect(() => { - playerStore.setAudio(audioRef.current); - return () => playerStore.setAudio(null); - }, []); - - // wire