feat(stage): show Bandcamp link in ON AIR

This commit is contained in:
beefcarver committed 2026-10-30 15:12:37 +00:00
1 parent 8e1c0a64eb
commit 66729c7542
4 files changed
+81 -3

No files matched your search

+1
View File
@@ -56,6 +56,7 @@ class Song:
artist: str = ""
album: str = ""
art_url: str = ""
bandcamp_url: str = ""
played_at: datetime | None = None
@property
+46 -1
View File
@@ -17,7 +17,7 @@ from textual.css.query import NoMatches
from textual.theme import Theme
from textual.widgets import Input
from . import api, artwork, config
from . import api, artwork, bandcamp, config
from .api import Station
from .fallback import fallback_channels
from .mpris import MprisService
@@ -133,6 +133,7 @@ class SomaFMApp(App[None]):
self.artwork_url = ""
self.artwork_bytes: bytes | None = None
self._art_track_key = ""
self._bandcamp_key = ""
self._http: httpx.AsyncClient | None = None
self._closed = False
self._tick_timer = None
@@ -415,8 +416,51 @@ class SomaFMApp(App[None]):
)
self.mpris.set_status(self._mpris_status())
self.mpris.set_volume(self.volume / 100.0)
self._update_bandcamp()
self._update_artwork()
def _update_bandcamp(self) -> None:
"""Find a Bandcamp track link once per now-playing track."""
station = self.current
if station is None or self.offline or self._http is None:
return
song = station.now_playing
if song is None or not (song.artist.strip() or song.title.strip()):
return
track_key = f"{station.id}|{song.key()}"
if track_key == self._bandcamp_key:
return
self._bandcamp_key = track_key
self._fetch_bandcamp(
station.id, song.key(), song.artist, song.album, song.title
)
@work(exclusive=True, group="bandcamp", exit_on_error=False)
async def _fetch_bandcamp(
self,
station_id: str,
song_key: str,
artist: str,
album: str,
title: str,
) -> None:
if self._http is None or self.offline:
return
url = await bandcamp.find_track(self._http, artist, album, title)
if not url:
return
station = next((s for s in self.stations if s.id == station_id), None)
if station is None or self.current is None or self.current.id != station_id:
return
song = station.now_playing
if song is None or song.key() != song_key:
return
song.bandcamp_url = url
with contextlib.suppress(NoMatches):
self.query_one(StagePanel).refresh_stage()
def _refresh_art(self) -> None:
with contextlib.suppress(NoMatches):
self.query_one(StagePanel).refresh_art()
@@ -758,6 +802,7 @@ class SomaFMApp(App[None]):
def action_refresh(self) -> None:
self._last_history_fetch = 0.0
self._bandcamp_key = ""
self._set_notice("refreshing…", 2)
if self.offline:
self._set_notice("offline mode — nothing to refresh", 3)
+10 -2
View File
@@ -161,7 +161,7 @@ class StageInfo(Widget):
notice = Text(truncate(app.notice, width), style="italic yellow")
return [
details = [
heading,
description,
Text(""),
@@ -170,8 +170,16 @@ class StageInfo(Widget):
title,
album,
meta,
notice,
]
if song is not None and song.bandcamp_url:
link = Text(no_wrap=True)
link.append(
"Bandcamp ↗",
style=f"bold cyan link {song.bandcamp_url}",
)
details.append(link)
details.append(notice)
return details
class HistoryPane(Widget):
+24
View File
@@ -133,3 +133,27 @@ async def test_narrow_layout() -> None:
text = rendered_text(app)
assert "STATIONS" in text
assert "ON AIR" in text
@pytest.mark.asyncio
async def test_stage_shows_bandcamp_link_only_when_present() -> None:
app = SomaFMApp(autoplay=False, offline=True, audio=False)
async with app.run_test(size=(110, 40)) as pilot:
await pilot.pause()
station = app.current
assert station is not None
song = Song(
title="Rauschebart",
artist="Jens Buchert",
album="Gravity",
bandcamp_url="https://jensbuchert.bandcamp.com/track/rauschebart",
)
station.now_playing = song
app.query_one(StagePanel).refresh_stage()
await pilot.pause()
assert "Bandcamp" in rendered_text(app)
song.bandcamp_url = ""
app.query_one(StagePanel).refresh_stage()
await pilot.pause()
assert "Bandcamp" not in rendered_text(app)