macOS has no D-Bus/MPRIS: the audio engine is what the OS media UI talks to. mpv's Cocoa backend registers with MPRemoteCommandCenter and MPNowPlayingInfoCenter, so it already publishes the live ICY title and receives the media keys. Add a platform-neutral media session (mediasession.py) so the app keeps a single interface: MprisService on D-Bus platforms, NowPlayingSession on macOS. Map mpv's NEXT/PREV/PLAY/PLAYONLY/PAUSEONLY/STOP keys onto script-messages and read them back as client-message IPC events, so the hardware media keys and Control Center drive the TUI (station changes, play/pause/stop) instead of fighting it. --no-mpris (now also --no-media) disables the media keys too. macOS-only mpv options are gated on sys.platform so the Linux command line is unchanged; mpv aborts on unknown flags.
127 lines
3.7 KiB
Python
127 lines
3.7 KiB
Python
"""Console entry point."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
from collections.abc import Sequence
|
|
|
|
from . import __version__, api
|
|
from .fallback import fallback_channels
|
|
from .spectrum import SOURCE_MODES
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="somafm-tui",
|
|
description="A flashy terminal radio for somafm.com — live spectrum, "
|
|
"song history and desktop media integration.",
|
|
)
|
|
parser.add_argument("--station", metavar="ID", help="station id to select")
|
|
parser.add_argument(
|
|
"--play",
|
|
dest="autoplay",
|
|
action="store_true",
|
|
default=None,
|
|
help="start playing immediately",
|
|
)
|
|
parser.add_argument(
|
|
"--no-autoplay",
|
|
dest="autoplay",
|
|
action="store_false",
|
|
help="never start playing on launch",
|
|
)
|
|
parser.add_argument("--volume", type=int, metavar="0-100", help="initial volume")
|
|
parser.add_argument(
|
|
"--no-mpris",
|
|
"--no-media",
|
|
dest="no_mpris",
|
|
action="store_true",
|
|
help="do not publish to the desktop media session (MPRIS on Linux, "
|
|
"Now Playing on macOS) and ignore the media keys",
|
|
)
|
|
parser.add_argument(
|
|
"--offline",
|
|
action="store_true",
|
|
help="do not touch the network (cached/bundled stations only)",
|
|
)
|
|
parser.add_argument(
|
|
"--no-audio",
|
|
action="store_true",
|
|
help="UI only; skip mpv/ffmpeg/MPRIS (useful for demos)",
|
|
)
|
|
parser.add_argument(
|
|
"--spectrum-source",
|
|
choices=SOURCE_MODES,
|
|
default="auto",
|
|
help="spectrum audio source: auto (monitor→stream→sim), monitor "
|
|
"(parec on Linux, an avfoundation loopback device on macOS), "
|
|
"stream (muted second ffmpeg decode), or sim",
|
|
)
|
|
parser.add_argument(
|
|
"--diagnose-images",
|
|
action="store_true",
|
|
help="print terminal/image-protocol diagnostics and exit",
|
|
)
|
|
parser.add_argument(
|
|
"--list",
|
|
action="store_true",
|
|
help="print known station ids and exit",
|
|
)
|
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
return parser
|
|
|
|
|
|
def _list_stations(offline: bool) -> int:
|
|
stations = api.load_cached_channels()
|
|
if not stations and not offline:
|
|
async def load():
|
|
client = api.make_client()
|
|
try:
|
|
return await api.fetch_channels(client)
|
|
finally:
|
|
await client.aclose()
|
|
|
|
try:
|
|
stations = asyncio.run(load())
|
|
except Exception:
|
|
stations = []
|
|
if not stations:
|
|
stations = api.parse_channels({"channels": fallback_channels()})
|
|
for station in sorted(stations, key=lambda item: item.title.casefold()):
|
|
print(f"{station.id:<20} {station.title}")
|
|
return 0
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
args = _build_parser().parse_args(argv)
|
|
if args.diagnose_images:
|
|
from . import artwork
|
|
|
|
for key, value in artwork.diagnostics().items():
|
|
print(f"{key}: {value}")
|
|
return 0
|
|
|
|
if args.list:
|
|
return _list_stations(args.offline)
|
|
|
|
# Import the app lazily so --list/--version do not probe the terminal for
|
|
# image protocols.
|
|
from .app import SomaFMApp
|
|
|
|
app = SomaFMApp(
|
|
autoplay=args.autoplay,
|
|
station_id=args.station,
|
|
volume=args.volume,
|
|
media=not args.no_mpris,
|
|
offline=args.offline,
|
|
audio=not args.no_audio,
|
|
spectrum_source=args.spectrum_source,
|
|
)
|
|
app.run()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|