radiotui-tw 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. radiotui_tw-0.1.0/.gitignore +14 -0
  2. radiotui_tw-0.1.0/LICENSE +21 -0
  3. radiotui_tw-0.1.0/PKG-INFO +245 -0
  4. radiotui_tw-0.1.0/README.md +213 -0
  5. radiotui_tw-0.1.0/pyproject.toml +68 -0
  6. radiotui_tw-0.1.0/terminal_radio/__init__.py +1 -0
  7. radiotui_tw-0.1.0/terminal_radio/__main__.py +6 -0
  8. radiotui_tw-0.1.0/terminal_radio/cli.py +96 -0
  9. radiotui_tw-0.1.0/terminal_radio/constants/__init__.py +1 -0
  10. radiotui_tw-0.1.0/terminal_radio/constants/about.py +16 -0
  11. radiotui_tw-0.1.0/terminal_radio/constants/analytics.py +31 -0
  12. radiotui_tw-0.1.0/terminal_radio/constants/audio.py +5 -0
  13. radiotui_tw-0.1.0/terminal_radio/constants/config.py +63 -0
  14. radiotui_tw-0.1.0/terminal_radio/constants/logo.py +54 -0
  15. radiotui_tw-0.1.0/terminal_radio/constants/playback.py +6 -0
  16. radiotui_tw-0.1.0/terminal_radio/constants/player.py +20 -0
  17. radiotui_tw-0.1.0/terminal_radio/constants/station.py +6 -0
  18. radiotui_tw-0.1.0/terminal_radio/constants/transfer.py +6 -0
  19. radiotui_tw-0.1.0/terminal_radio/constants/tui.py +93 -0
  20. radiotui_tw-0.1.0/terminal_radio/core/__init__.py +1 -0
  21. radiotui_tw-0.1.0/terminal_radio/core/about.py +41 -0
  22. radiotui_tw-0.1.0/terminal_radio/core/config.py +116 -0
  23. radiotui_tw-0.1.0/terminal_radio/core/exceptions.py +33 -0
  24. radiotui_tw-0.1.0/terminal_radio/core/i18n.py +123 -0
  25. radiotui_tw-0.1.0/terminal_radio/data/locales/en.yml +205 -0
  26. radiotui_tw-0.1.0/terminal_radio/data/locales/zh-Hant.yml +205 -0
  27. radiotui_tw-0.1.0/terminal_radio/data/stations.toml +383 -0
  28. radiotui_tw-0.1.0/terminal_radio/data/themes.yml +187 -0
  29. radiotui_tw-0.1.0/terminal_radio/dependencies.py +30 -0
  30. radiotui_tw-0.1.0/terminal_radio/enums/__init__.py +14 -0
  31. radiotui_tw-0.1.0/terminal_radio/enums/analytics.py +12 -0
  32. radiotui_tw-0.1.0/terminal_radio/enums/history.py +14 -0
  33. radiotui_tw-0.1.0/terminal_radio/enums/playback.py +12 -0
  34. radiotui_tw-0.1.0/terminal_radio/enums/station.py +20 -0
  35. radiotui_tw-0.1.0/terminal_radio/main.py +64 -0
  36. radiotui_tw-0.1.0/terminal_radio/models/__init__.py +20 -0
  37. radiotui_tw-0.1.0/terminal_radio/models/history.py +38 -0
  38. radiotui_tw-0.1.0/terminal_radio/models/station.py +77 -0
  39. radiotui_tw-0.1.0/terminal_radio/models/theme.py +29 -0
  40. radiotui_tw-0.1.0/terminal_radio/py.typed +0 -0
  41. radiotui_tw-0.1.0/terminal_radio/routers/__init__.py +13 -0
  42. radiotui_tw-0.1.0/terminal_radio/routers/history.py +28 -0
  43. radiotui_tw-0.1.0/terminal_radio/routers/player.py +46 -0
  44. radiotui_tw-0.1.0/terminal_radio/routers/stations.py +30 -0
  45. radiotui_tw-0.1.0/terminal_radio/routers/themes.py +16 -0
  46. radiotui_tw-0.1.0/terminal_radio/schemas/__init__.py +24 -0
  47. radiotui_tw-0.1.0/terminal_radio/schemas/history.py +72 -0
  48. radiotui_tw-0.1.0/terminal_radio/schemas/player.py +47 -0
  49. radiotui_tw-0.1.0/terminal_radio/schemas/station.py +39 -0
  50. radiotui_tw-0.1.0/terminal_radio/schemas/theme.py +33 -0
  51. radiotui_tw-0.1.0/terminal_radio/services/__init__.py +57 -0
  52. radiotui_tw-0.1.0/terminal_radio/services/analytics.py +153 -0
  53. radiotui_tw-0.1.0/terminal_radio/services/audio.py +45 -0
  54. radiotui_tw-0.1.0/terminal_radio/services/catalog.py +77 -0
  55. radiotui_tw-0.1.0/terminal_radio/services/custom_stations.py +102 -0
  56. radiotui_tw-0.1.0/terminal_radio/services/history.py +139 -0
  57. radiotui_tw-0.1.0/terminal_radio/services/history_csv.py +82 -0
  58. radiotui_tw-0.1.0/terminal_radio/services/migration.py +39 -0
  59. radiotui_tw-0.1.0/terminal_radio/services/player.py +349 -0
  60. radiotui_tw-0.1.0/terminal_radio/services/radio.py +709 -0
  61. radiotui_tw-0.1.0/terminal_radio/services/reconnect.py +95 -0
  62. radiotui_tw-0.1.0/terminal_radio/services/sleep_timer.py +41 -0
  63. radiotui_tw-0.1.0/terminal_radio/services/state.py +52 -0
  64. radiotui_tw-0.1.0/terminal_radio/services/station_health.py +155 -0
  65. radiotui_tw-0.1.0/terminal_radio/services/station_library.py +157 -0
  66. radiotui_tw-0.1.0/terminal_radio/services/themes.py +72 -0
  67. radiotui_tw-0.1.0/terminal_radio/services/transfer.py +127 -0
  68. radiotui_tw-0.1.0/terminal_radio/tui/__init__.py +5 -0
  69. radiotui_tw-0.1.0/terminal_radio/tui/__main__.py +6 -0
  70. radiotui_tw-0.1.0/terminal_radio/tui/app.py +1072 -0
  71. radiotui_tw-0.1.0/terminal_radio/tui/formatting.py +55 -0
  72. radiotui_tw-0.1.0/terminal_radio/tui/radio.tcss +605 -0
  73. radiotui_tw-0.1.0/terminal_radio/tui/screens.py +553 -0
  74. radiotui_tw-0.1.0/terminal_radio/tui/statistics.py +360 -0
  75. radiotui_tw-0.1.0/terminal_radio/tui/theming.py +21 -0
  76. radiotui_tw-0.1.0/terminal_radio/tui/widgets.py +676 -0
  77. radiotui_tw-0.1.0/tests/test_analytics.py +349 -0
  78. radiotui_tw-0.1.0/tests/test_goodbye.py +42 -0
  79. radiotui_tw-0.1.0/tests/test_history_csv.py +54 -0
  80. radiotui_tw-0.1.0/tests/test_playback_resilience.py +355 -0
  81. radiotui_tw-0.1.0/tests/test_station_discovery.py +423 -0
  82. radiotui_tw-0.1.0/tests/test_station_transfer_api.py +176 -0
  83. radiotui_tw-0.1.0/tests/test_tui_app.py +908 -0
  84. radiotui_tw-0.1.0/tests/test_tui_widgets.py +172 -0
@@ -0,0 +1,14 @@
1
+ .DS_Store
2
+ .radio/
3
+ .venv/
4
+ docs/
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
11
+ htmlcov/
12
+ dist/
13
+ build/
14
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Sung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.5
2
+ Name: radiotui-tw
3
+ Version: 0.1.0
4
+ Summary: Terminal radio player for Taiwanese FM and AM stations, with a Textual UI and a FastAPI control API
5
+ Project-URL: Homepage, https://github.com/yueswater/terminal-radio
6
+ Project-URL: Repository, https://github.com/yueswater/terminal-radio
7
+ Project-URL: Issues, https://github.com/yueswater/terminal-radio/issues
8
+ Project-URL: Changelog, https://github.com/yueswater/terminal-radio/releases
9
+ Author: Anthony Sung
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: am,fm,mpv,radio,streaming,taiwan,terminal,textual,tui
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Natural Language :: Chinese (Traditional)
17
+ Classifier: Natural Language :: English
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Players
24
+ Classifier: Topic :: Terminals
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: fastapi[standard]>=0.141.1
28
+ Requires-Dist: platformdirs>=4.11.5
29
+ Requires-Dist: pyyaml>=6.0.3
30
+ Requires-Dist: textual>=8.2.8
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Terminal Radio
34
+
35
+ <p align="right">
36
+ <strong>English</strong> · <a href="https://github.com/yueswater/terminal-radio/blob/main/README.zh-Hant.md">繁體中文</a>
37
+ </p>
38
+
39
+ <p align="center">
40
+ <img src="https://raw.githubusercontent.com/yueswater/terminal-radio/main/assets/terminal-radio-logo.svg" width="560"
41
+ alt="A terminal screen beside the RADIO word mark, in gradient ASCII art">
42
+ </p>
43
+
44
+ ![python](https://img.shields.io/badge/python-3.12%2B-3fb950?style=flat-square&logo=python&logoColor=white) ![Textual](https://img.shields.io/badge/Textual-8.2-3fb950?style=flat-square) ![FastAPI](https://img.shields.io/badge/FastAPI-0.141-3fb950?style=flat-square&logo=fastapi&logoColor=white) ![player](https://img.shields.io/badge/player-mpv-3fb950?style=flat-square&logo=mpv&logoColor=white) ![stations](https://img.shields.io/badge/stations-44-3fb950?style=flat-square) ![themes](https://img.shields.io/badge/themes-14-3fb950?style=flat-square) ![i18n](https://img.shields.io/badge/i18n-zh--Hant%20%7C%20en-3fb950?style=flat-square) ![platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-3fb950?style=flat-square&logo=apple&logoColor=white) ![license](https://img.shields.io/badge/license-MIT-3fb950?style=flat-square)
45
+
46
+ A terminal player for Taiwanese radio stations. The Textual interface and FastAPI control API share the same service layer.
47
+
48
+ ## Prerequisites
49
+
50
+ Radio requires Python 3.12 or later, [uv](https://docs.astral.sh/uv/) and [mpv](https://mpv.io/). Install mpv for your system first:
51
+
52
+ ```sh
53
+ # macOS (Homebrew)
54
+ brew install mpv
55
+
56
+ # Ubuntu / Debian
57
+ sudo apt update
58
+ sudo apt install mpv
59
+
60
+ # Arch Linux
61
+ sudo pacman -S mpv
62
+ ```
63
+
64
+ For other systems, see the [mpv installation guide](https://mpv.io/installation/). After installation, run `mpv --version` to confirm that the command is available in your terminal.
65
+
66
+ ## Install
67
+
68
+ ```sh
69
+ uv tool install git+https://github.com/yueswater/terminal-radio
70
+ ```
71
+
72
+ This puts the `radio` command on PATH, so it runs from any directory. No clone
73
+ needed. `pipx install git+https://github.com/yueswater/terminal-radio` works the
74
+ same way.
75
+
76
+ To remove it, run `uv tool uninstall radiotui-tw`.
77
+
78
+ ### Working on the project
79
+
80
+ Clone the repository and install it in editable form, so changes take effect
81
+ without reinstalling:
82
+
83
+ ```sh
84
+ make link # uv tool install --editable . --force
85
+ make unlink # remove it again
86
+ ```
87
+
88
+ ## Run
89
+
90
+ ```sh
91
+ radio # terminal interface
92
+ radio ui --no-autoplay # do not resume the last station at startup
93
+ radio api # HTTP API; docs at http://127.0.0.1:8000/docs
94
+ radio stations --band AM
95
+ radio --help
96
+ ```
97
+
98
+ Without installing, use `make run`, `make api` or `uv run radio ...`.
99
+
100
+ ## Terminal interface
101
+
102
+ The tabs include **Home**, FM, AM, **Favorites**, **History**, **Statistics**, **Themes**, **Settings** and **About**. Every launch starts on Home, even when the app resumes the last station. The bottom bar shows the playback state, frequency, station, program title, elapsed time, audio output, sleep timer and volume. Click the playback state at the bottom left to pause or resume. The output device name is limited to fifteen characters. By default, the last station resumes at startup.
103
+
104
+ | Key | Action |
105
+ | --- | --- |
106
+ | `←` `→` | Move to the previous or next tab |
107
+ | `↑` `↓` `j` `k` | Move the cursor |
108
+ | `enter` | Use the selected item: play or resume a station, apply a theme or change a setting |
109
+ | `space` | Pause or resume |
110
+ | `s` | Stop playback |
111
+ | `f` | Add or remove a favorite |
112
+ | `+` `=` | Raise the volume |
113
+ | `-` `_` | Lower the volume |
114
+ | `m` | Mute or unmute |
115
+ | `t` | Switch to the next theme |
116
+ | `e` | Export settings |
117
+ | `i` | Import settings |
118
+ | `w` | Switch between English and Traditional Chinese |
119
+ | `/` | Search all built-in and custom stations |
120
+ | `?` | Open the keyboard shortcut guide |
121
+ | `q` | Quit |
122
+
123
+ ## Scrolling
124
+
125
+ When columns are wider than the window, use a mouse or trackpad to scroll horizontally. The horizontal scrollbar is hidden so it does not look like a volume bar. The left and right arrow keys still only switch tabs.
126
+
127
+ If all rows fit on screen but the columns are too wide, scrolling down moves right and scrolling up moves left. When more rows are available below, the wheel keeps its normal vertical movement. The FM, AM, Favorites, History and Settings tables stay centred with the same space above and below. Their pages remain fixed while only the table rows scroll.
128
+
129
+ Favorites, volume, mute, autoplay, reconnect, station checks, language, the last station and the active theme are stored in `<state>/state.json`.
130
+
131
+ ## Configuration files
132
+
133
+ | File | Contents |
134
+ | --- | --- |
135
+ | `app/data/stations.toml` | Station slug, name, band, frequency and stream URL |
136
+ | `app/data/themes.yml` | All color palettes and the default theme |
137
+ | `app/data/locales/*.yml` | English and Traditional Chinese interface text |
138
+ | `app/tui/radio.tcss` | Terminal interface layout |
139
+ | `<state>/history.jsonl` | Listening history, with one JSON event per line |
140
+ | `<state>/state.json` | Favorites, volume, mute, autoplay, animations, language, station and theme |
141
+ | `<state>/custom-stations.toml` | Stations added from the Settings page |
142
+
143
+ `<state>` is the per-user directory the program writes to, outside the
144
+ installation, so upgrading or reinstalling never loses a history:
145
+ `~/Library/Application Support/terminal-radio` on macOS and
146
+ `~/.local/state/terminal-radio` on Linux. `RADIO_DATA_DIR` overrides it.
147
+
148
+ The bundled catalogue, themes and locales are read-only. To use your own without
149
+ touching the installation, drop a `stations.toml`, `themes.yml` or `locales/`
150
+ into the config directory, `~/Library/Application Support/terminal-radio` on
151
+ macOS and `~/.config/terminal-radio` on Linux, and it is read in preference.
152
+
153
+ Built-in stations live in `app/data/stations.toml`. You can also open **Custom stations** from **Settings** to add, edit or delete a local station without changing the project file. Custom stream URLs must use HTTP or HTTPS. To add a built-in station, append a block to `app/data/stations.toml`:
154
+
155
+ ```toml
156
+ [[stations]]
157
+ slug = "example"
158
+ name = "Example FM"
159
+ band = "FM"
160
+ frequency = "99.9"
161
+ description = "Optional description"
162
+ url = "https://example.com/live/playlist.m3u8"
163
+ ```
164
+
165
+ ## Audio output
166
+
167
+ The bottom bar shows where the sound is being sent. `mpv` only reports `auto`, so macOS runs `system_profiler SPAudioDataType` in the background every fifteen seconds and caches the result. On other platforms, or when detection fails, the app shows the name of the mpv output driver instead.
168
+
169
+ ## Playback tools
170
+
171
+ Press `/` to search by frequency, station name, description or band. Results update while you type, and `enter` plays the highlighted station.
172
+
173
+ Automatic reconnect is enabled by default. When a stream drops, Radio retries after 1, 2, 4, 8 and 15 seconds. It stops retrying after the fifth failure. You can turn this off in **Settings**.
174
+
175
+ The sleep timer can be turned off or set to 15, 30, 60 or a custom number of minutes from 1 to 1440. Its countdown appears in the bottom bar and only lasts for the current run.
176
+
177
+ Radio can check whether station streams are online, slow or offline. Automatic checks are cached for five minutes, and **Check all stations now** runs a fresh check. At most four streams are checked at once.
178
+
179
+ ## Languages
180
+
181
+ Radio currently includes only English and Traditional Chinese. Their messages are stored in `app/data/locales/en.yml` and `app/data/locales/zh-Hant.yml`, and Traditional Chinese is the default. Press `w` to switch between them. The **Settings** page also shows the current language.
182
+
183
+ All text written by the app is translated. Station names, descriptions and program titles come from the catalog or stream data, so they remain in their original language. When interface text changes, update both locale files. If a translation key is missing, the app first falls back to Traditional Chinese and then displays the key itself.
184
+
185
+ ## Themes, settings and about
186
+
187
+ The **Themes** page previews every palette in `app/data/themes.yml`. Each card uses its own background, foreground and color swatches. Press `enter` to apply the selected theme. When you return to this page, the cursor stays on the active theme.
188
+
189
+ The **Settings** page includes autoplay, reconnect, sleep timer, station checks, custom stations, keyboard shortcuts, animations, language, theme and volume. Press `enter` to change an editable item. Read-only items show their value and the environment variable that can override it. Select **Restore defaults** and confirm to reset preferences while keeping favorites, custom stations, the last station and listening history.
190
+
191
+ Animations are off by default.
192
+
193
+ The **About** page shows the version, copyright and packages used by the app. The author, year and project URL are defined in `app/core/about.py`.
194
+
195
+ ## Exporting and importing settings
196
+
197
+ Press `e`, or select **Export settings**, to list the available Desktop, Documents, Downloads, home, project and data folders. Press `enter` to write the file or `escape` to cancel.
198
+
199
+ The file name follows the format `settings_<timestamp>.radio.config`, with time recorded to the millisecond.
200
+
201
+ ```json
202
+ {
203
+ "version": "0.1.0",
204
+ "exported_at": "2026-08-30T13:44:24.355+08:00",
205
+ "settings": { "...": "..." },
206
+ "preferences": { "favorites": [], "volume": 100, "...": "..." },
207
+ "custom_stations": []
208
+ }
209
+ ```
210
+
211
+ Press `i` to search the same folders for `.radio.config` files, listed from newest to oldest. Importing restores custom stations, favorites, volume, mute, theme, language, autoplay, reconnect, station checks and animations. Every page is updated at once. Older exports without `custom_stations` remain supported.
212
+
213
+ The app only applies the `preferences` section. The `settings` section records the environment at the time of export, so its paths and commands belong to the original device and are not transferred during import. Files with an invalid format or the wrong value types are rejected. Stations that no longer exist are also removed from favorites and the last-played record.
214
+
215
+ ## Listening history
216
+
217
+ Each session start, session end, play, pause and resume is written to `<state>/history.jsonl` with timing data. A `play_ended` event records the total elapsed, paused and interrupted time. Listening time excludes both pauses and reconnect interruptions. The table always uses `HH:MM:SS`.
218
+
219
+ Select **Export CSV** to save the complete station summary with a UTF-8 BOM. Column names follow the current interface language. Select **Clear listening history** and confirm to remove all saved events.
220
+
221
+ The **Statistics** page reads the complete valid history and draws terminal charts for total listening time, play count, active days, the ten most-listened stations, a 14-day trend, weekdays, time of day and FM/AM share. Only completed plays are counted.
222
+
223
+ ## API endpoints
224
+
225
+ | Method | Path | Purpose |
226
+ | --- | --- | --- |
227
+ | GET | `/stations?band=FM&q=police` | List stations, optionally filtered by band or search text |
228
+ | GET | `/stations/{slug}` | Get one station |
229
+ | GET | `/player` | Get playback state, program title and timers |
230
+ | POST | `/player/play` | Play a station |
231
+ | POST | `/player/toggle` | Toggle playback for a station |
232
+ | POST | `/player/pause` | Pause playback |
233
+ | POST | `/player/resume` | Resume playback |
234
+ | POST | `/player/stop` | Stop playback |
235
+ | GET | `/history` | Get recent listening events |
236
+ | GET | `/history/summary` | Get listening totals for each station |
237
+ | GET | `/themes` | List available themes |
238
+
239
+ ## Contributing and security
240
+
241
+ Read [CONTRIBUTING.md](https://github.com/yueswater/terminal-radio/blob/main/CONTRIBUTING.md) before submitting a pull request. Report security issues privately by following [SECURITY.md](https://github.com/yueswater/terminal-radio/blob/main/SECURITY.md), and do not open a public issue. All participants must follow the [Code of Conduct](https://github.com/yueswater/terminal-radio/blob/main/CODE_OF_CONDUCT.md).
242
+
243
+ ## License
244
+
245
+ Radio uses the [MIT License](https://github.com/yueswater/terminal-radio/blob/main/LICENSE).
@@ -0,0 +1,213 @@
1
+ # Terminal Radio
2
+
3
+ <p align="right">
4
+ <strong>English</strong> · <a href="https://github.com/yueswater/terminal-radio/blob/main/README.zh-Hant.md">繁體中文</a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://raw.githubusercontent.com/yueswater/terminal-radio/main/assets/terminal-radio-logo.svg" width="560"
9
+ alt="A terminal screen beside the RADIO word mark, in gradient ASCII art">
10
+ </p>
11
+
12
+ ![python](https://img.shields.io/badge/python-3.12%2B-3fb950?style=flat-square&logo=python&logoColor=white) ![Textual](https://img.shields.io/badge/Textual-8.2-3fb950?style=flat-square) ![FastAPI](https://img.shields.io/badge/FastAPI-0.141-3fb950?style=flat-square&logo=fastapi&logoColor=white) ![player](https://img.shields.io/badge/player-mpv-3fb950?style=flat-square&logo=mpv&logoColor=white) ![stations](https://img.shields.io/badge/stations-44-3fb950?style=flat-square) ![themes](https://img.shields.io/badge/themes-14-3fb950?style=flat-square) ![i18n](https://img.shields.io/badge/i18n-zh--Hant%20%7C%20en-3fb950?style=flat-square) ![platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-3fb950?style=flat-square&logo=apple&logoColor=white) ![license](https://img.shields.io/badge/license-MIT-3fb950?style=flat-square)
13
+
14
+ A terminal player for Taiwanese radio stations. The Textual interface and FastAPI control API share the same service layer.
15
+
16
+ ## Prerequisites
17
+
18
+ Radio requires Python 3.12 or later, [uv](https://docs.astral.sh/uv/) and [mpv](https://mpv.io/). Install mpv for your system first:
19
+
20
+ ```sh
21
+ # macOS (Homebrew)
22
+ brew install mpv
23
+
24
+ # Ubuntu / Debian
25
+ sudo apt update
26
+ sudo apt install mpv
27
+
28
+ # Arch Linux
29
+ sudo pacman -S mpv
30
+ ```
31
+
32
+ For other systems, see the [mpv installation guide](https://mpv.io/installation/). After installation, run `mpv --version` to confirm that the command is available in your terminal.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ uv tool install git+https://github.com/yueswater/terminal-radio
38
+ ```
39
+
40
+ This puts the `radio` command on PATH, so it runs from any directory. No clone
41
+ needed. `pipx install git+https://github.com/yueswater/terminal-radio` works the
42
+ same way.
43
+
44
+ To remove it, run `uv tool uninstall radiotui-tw`.
45
+
46
+ ### Working on the project
47
+
48
+ Clone the repository and install it in editable form, so changes take effect
49
+ without reinstalling:
50
+
51
+ ```sh
52
+ make link # uv tool install --editable . --force
53
+ make unlink # remove it again
54
+ ```
55
+
56
+ ## Run
57
+
58
+ ```sh
59
+ radio # terminal interface
60
+ radio ui --no-autoplay # do not resume the last station at startup
61
+ radio api # HTTP API; docs at http://127.0.0.1:8000/docs
62
+ radio stations --band AM
63
+ radio --help
64
+ ```
65
+
66
+ Without installing, use `make run`, `make api` or `uv run radio ...`.
67
+
68
+ ## Terminal interface
69
+
70
+ The tabs include **Home**, FM, AM, **Favorites**, **History**, **Statistics**, **Themes**, **Settings** and **About**. Every launch starts on Home, even when the app resumes the last station. The bottom bar shows the playback state, frequency, station, program title, elapsed time, audio output, sleep timer and volume. Click the playback state at the bottom left to pause or resume. The output device name is limited to fifteen characters. By default, the last station resumes at startup.
71
+
72
+ | Key | Action |
73
+ | --- | --- |
74
+ | `←` `→` | Move to the previous or next tab |
75
+ | `↑` `↓` `j` `k` | Move the cursor |
76
+ | `enter` | Use the selected item: play or resume a station, apply a theme or change a setting |
77
+ | `space` | Pause or resume |
78
+ | `s` | Stop playback |
79
+ | `f` | Add or remove a favorite |
80
+ | `+` `=` | Raise the volume |
81
+ | `-` `_` | Lower the volume |
82
+ | `m` | Mute or unmute |
83
+ | `t` | Switch to the next theme |
84
+ | `e` | Export settings |
85
+ | `i` | Import settings |
86
+ | `w` | Switch between English and Traditional Chinese |
87
+ | `/` | Search all built-in and custom stations |
88
+ | `?` | Open the keyboard shortcut guide |
89
+ | `q` | Quit |
90
+
91
+ ## Scrolling
92
+
93
+ When columns are wider than the window, use a mouse or trackpad to scroll horizontally. The horizontal scrollbar is hidden so it does not look like a volume bar. The left and right arrow keys still only switch tabs.
94
+
95
+ If all rows fit on screen but the columns are too wide, scrolling down moves right and scrolling up moves left. When more rows are available below, the wheel keeps its normal vertical movement. The FM, AM, Favorites, History and Settings tables stay centred with the same space above and below. Their pages remain fixed while only the table rows scroll.
96
+
97
+ Favorites, volume, mute, autoplay, reconnect, station checks, language, the last station and the active theme are stored in `<state>/state.json`.
98
+
99
+ ## Configuration files
100
+
101
+ | File | Contents |
102
+ | --- | --- |
103
+ | `app/data/stations.toml` | Station slug, name, band, frequency and stream URL |
104
+ | `app/data/themes.yml` | All color palettes and the default theme |
105
+ | `app/data/locales/*.yml` | English and Traditional Chinese interface text |
106
+ | `app/tui/radio.tcss` | Terminal interface layout |
107
+ | `<state>/history.jsonl` | Listening history, with one JSON event per line |
108
+ | `<state>/state.json` | Favorites, volume, mute, autoplay, animations, language, station and theme |
109
+ | `<state>/custom-stations.toml` | Stations added from the Settings page |
110
+
111
+ `<state>` is the per-user directory the program writes to, outside the
112
+ installation, so upgrading or reinstalling never loses a history:
113
+ `~/Library/Application Support/terminal-radio` on macOS and
114
+ `~/.local/state/terminal-radio` on Linux. `RADIO_DATA_DIR` overrides it.
115
+
116
+ The bundled catalogue, themes and locales are read-only. To use your own without
117
+ touching the installation, drop a `stations.toml`, `themes.yml` or `locales/`
118
+ into the config directory, `~/Library/Application Support/terminal-radio` on
119
+ macOS and `~/.config/terminal-radio` on Linux, and it is read in preference.
120
+
121
+ Built-in stations live in `app/data/stations.toml`. You can also open **Custom stations** from **Settings** to add, edit or delete a local station without changing the project file. Custom stream URLs must use HTTP or HTTPS. To add a built-in station, append a block to `app/data/stations.toml`:
122
+
123
+ ```toml
124
+ [[stations]]
125
+ slug = "example"
126
+ name = "Example FM"
127
+ band = "FM"
128
+ frequency = "99.9"
129
+ description = "Optional description"
130
+ url = "https://example.com/live/playlist.m3u8"
131
+ ```
132
+
133
+ ## Audio output
134
+
135
+ The bottom bar shows where the sound is being sent. `mpv` only reports `auto`, so macOS runs `system_profiler SPAudioDataType` in the background every fifteen seconds and caches the result. On other platforms, or when detection fails, the app shows the name of the mpv output driver instead.
136
+
137
+ ## Playback tools
138
+
139
+ Press `/` to search by frequency, station name, description or band. Results update while you type, and `enter` plays the highlighted station.
140
+
141
+ Automatic reconnect is enabled by default. When a stream drops, Radio retries after 1, 2, 4, 8 and 15 seconds. It stops retrying after the fifth failure. You can turn this off in **Settings**.
142
+
143
+ The sleep timer can be turned off or set to 15, 30, 60 or a custom number of minutes from 1 to 1440. Its countdown appears in the bottom bar and only lasts for the current run.
144
+
145
+ Radio can check whether station streams are online, slow or offline. Automatic checks are cached for five minutes, and **Check all stations now** runs a fresh check. At most four streams are checked at once.
146
+
147
+ ## Languages
148
+
149
+ Radio currently includes only English and Traditional Chinese. Their messages are stored in `app/data/locales/en.yml` and `app/data/locales/zh-Hant.yml`, and Traditional Chinese is the default. Press `w` to switch between them. The **Settings** page also shows the current language.
150
+
151
+ All text written by the app is translated. Station names, descriptions and program titles come from the catalog or stream data, so they remain in their original language. When interface text changes, update both locale files. If a translation key is missing, the app first falls back to Traditional Chinese and then displays the key itself.
152
+
153
+ ## Themes, settings and about
154
+
155
+ The **Themes** page previews every palette in `app/data/themes.yml`. Each card uses its own background, foreground and color swatches. Press `enter` to apply the selected theme. When you return to this page, the cursor stays on the active theme.
156
+
157
+ The **Settings** page includes autoplay, reconnect, sleep timer, station checks, custom stations, keyboard shortcuts, animations, language, theme and volume. Press `enter` to change an editable item. Read-only items show their value and the environment variable that can override it. Select **Restore defaults** and confirm to reset preferences while keeping favorites, custom stations, the last station and listening history.
158
+
159
+ Animations are off by default.
160
+
161
+ The **About** page shows the version, copyright and packages used by the app. The author, year and project URL are defined in `app/core/about.py`.
162
+
163
+ ## Exporting and importing settings
164
+
165
+ Press `e`, or select **Export settings**, to list the available Desktop, Documents, Downloads, home, project and data folders. Press `enter` to write the file or `escape` to cancel.
166
+
167
+ The file name follows the format `settings_<timestamp>.radio.config`, with time recorded to the millisecond.
168
+
169
+ ```json
170
+ {
171
+ "version": "0.1.0",
172
+ "exported_at": "2026-08-30T13:44:24.355+08:00",
173
+ "settings": { "...": "..." },
174
+ "preferences": { "favorites": [], "volume": 100, "...": "..." },
175
+ "custom_stations": []
176
+ }
177
+ ```
178
+
179
+ Press `i` to search the same folders for `.radio.config` files, listed from newest to oldest. Importing restores custom stations, favorites, volume, mute, theme, language, autoplay, reconnect, station checks and animations. Every page is updated at once. Older exports without `custom_stations` remain supported.
180
+
181
+ The app only applies the `preferences` section. The `settings` section records the environment at the time of export, so its paths and commands belong to the original device and are not transferred during import. Files with an invalid format or the wrong value types are rejected. Stations that no longer exist are also removed from favorites and the last-played record.
182
+
183
+ ## Listening history
184
+
185
+ Each session start, session end, play, pause and resume is written to `<state>/history.jsonl` with timing data. A `play_ended` event records the total elapsed, paused and interrupted time. Listening time excludes both pauses and reconnect interruptions. The table always uses `HH:MM:SS`.
186
+
187
+ Select **Export CSV** to save the complete station summary with a UTF-8 BOM. Column names follow the current interface language. Select **Clear listening history** and confirm to remove all saved events.
188
+
189
+ The **Statistics** page reads the complete valid history and draws terminal charts for total listening time, play count, active days, the ten most-listened stations, a 14-day trend, weekdays, time of day and FM/AM share. Only completed plays are counted.
190
+
191
+ ## API endpoints
192
+
193
+ | Method | Path | Purpose |
194
+ | --- | --- | --- |
195
+ | GET | `/stations?band=FM&q=police` | List stations, optionally filtered by band or search text |
196
+ | GET | `/stations/{slug}` | Get one station |
197
+ | GET | `/player` | Get playback state, program title and timers |
198
+ | POST | `/player/play` | Play a station |
199
+ | POST | `/player/toggle` | Toggle playback for a station |
200
+ | POST | `/player/pause` | Pause playback |
201
+ | POST | `/player/resume` | Resume playback |
202
+ | POST | `/player/stop` | Stop playback |
203
+ | GET | `/history` | Get recent listening events |
204
+ | GET | `/history/summary` | Get listening totals for each station |
205
+ | GET | `/themes` | List available themes |
206
+
207
+ ## Contributing and security
208
+
209
+ Read [CONTRIBUTING.md](https://github.com/yueswater/terminal-radio/blob/main/CONTRIBUTING.md) before submitting a pull request. Report security issues privately by following [SECURITY.md](https://github.com/yueswater/terminal-radio/blob/main/SECURITY.md), and do not open a public issue. All participants must follow the [Code of Conduct](https://github.com/yueswater/terminal-radio/blob/main/CODE_OF_CONDUCT.md).
210
+
211
+ ## License
212
+
213
+ Radio uses the [MIT License](https://github.com/yueswater/terminal-radio/blob/main/LICENSE).
@@ -0,0 +1,68 @@
1
+ [project]
2
+ name = "radiotui-tw"
3
+ version = "0.1.0"
4
+ description = "Terminal radio player for Taiwanese FM and AM stations, with a Textual UI and a FastAPI control API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Anthony Sung" }]
10
+ keywords = ["radio", "terminal", "tui", "textual", "streaming", "taiwan", "fm", "am", "mpv"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Console",
14
+ "Intended Audience :: End Users/Desktop",
15
+ "Natural Language :: Chinese (Traditional)",
16
+ "Natural Language :: English",
17
+ "Operating System :: MacOS",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Multimedia :: Sound/Audio :: Players",
23
+ "Topic :: Terminals",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "fastapi[standard]>=0.141.1",
28
+ "platformdirs>=4.11.5",
29
+ "pyyaml>=6.0.3",
30
+ "textual>=8.2.8",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/yueswater/terminal-radio"
35
+ Repository = "https://github.com/yueswater/terminal-radio"
36
+ Issues = "https://github.com/yueswater/terminal-radio/issues"
37
+ Changelog = "https://github.com/yueswater/terminal-radio/releases"
38
+
39
+ [project.scripts]
40
+ radio = "terminal_radio.cli:main"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.3.0",
45
+ "pytest-asyncio>=0.24.0",
46
+ ]
47
+
48
+ [build-system]
49
+ requires = ["hatchling"]
50
+ build-backend = "hatchling.build"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["terminal_radio"]
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ # Enough to rebuild the wheel and to verify it, and nothing else. The logo
57
+ # generator and the rendered artwork belong to the repository, not to a
58
+ # distribution someone installs.
59
+ include = [
60
+ "/terminal_radio",
61
+ "/tests",
62
+ "/README.md",
63
+ "/LICENSE",
64
+ ]
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
68
+ asyncio_mode = "auto"
@@ -0,0 +1 @@
1
+ """Terminal radio player with a Textual UI and a FastAPI control API."""
@@ -0,0 +1,6 @@
1
+ """Allow starting the terminal UI with python -m app."""
2
+
3
+ from terminal_radio.tui.app import run
4
+
5
+ if __name__ == "__main__":
6
+ run()