station-server 0.4.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 (44) hide show
  1. station_server-0.4.0/.gitignore +61 -0
  2. station_server-0.4.0/LICENSE +21 -0
  3. station_server-0.4.0/PKG-INFO +275 -0
  4. station_server-0.4.0/README.md +241 -0
  5. station_server-0.4.0/packaging/launcher.py +6 -0
  6. station_server-0.4.0/packaging/station-server.spec +78 -0
  7. station_server-0.4.0/packaging/windows/installer.iss +69 -0
  8. station_server-0.4.0/pyproject.toml +33 -0
  9. station_server-0.4.0/station_server/__init__.py +9 -0
  10. station_server-0.4.0/station_server/__main__.py +299 -0
  11. station_server-0.4.0/station_server/audio.py +171 -0
  12. station_server-0.4.0/station_server/channel.py +241 -0
  13. station_server-0.4.0/station_server/config.py +188 -0
  14. station_server-0.4.0/station_server/desktop.py +122 -0
  15. station_server-0.4.0/station_server/onair.py +359 -0
  16. station_server-0.4.0/station_server/playlist.py +72 -0
  17. station_server-0.4.0/station_server/schedule.py +123 -0
  18. station_server-0.4.0/station_server/selfupdate.py +277 -0
  19. station_server-0.4.0/station_server/senders.py +314 -0
  20. station_server-0.4.0/station_server/static/app.js +793 -0
  21. station_server-0.4.0/station_server/static/icon-192.png +0 -0
  22. station_server-0.4.0/station_server/static/icon-512.png +0 -0
  23. station_server-0.4.0/station_server/static/icon.icns +0 -0
  24. station_server-0.4.0/station_server/static/icon.ico +0 -0
  25. station_server-0.4.0/station_server/static/icon.png +0 -0
  26. station_server-0.4.0/station_server/static/index.html +41 -0
  27. station_server-0.4.0/station_server/static/manifest.webmanifest +31 -0
  28. station_server-0.4.0/station_server/static/offline.html +19 -0
  29. station_server-0.4.0/station_server/static/offline.js +29 -0
  30. station_server-0.4.0/station_server/static/style.css +333 -0
  31. station_server-0.4.0/station_server/static/sw.js +29 -0
  32. station_server-0.4.0/station_server/updates.py +91 -0
  33. station_server-0.4.0/station_server/web.py +236 -0
  34. station_server-0.4.0/station_server/window.py +69 -0
  35. station_server-0.4.0/tests/conftest.py +230 -0
  36. station_server-0.4.0/tests/test_app.py +189 -0
  37. station_server-0.4.0/tests/test_channel.py +246 -0
  38. station_server-0.4.0/tests/test_config.py +59 -0
  39. station_server-0.4.0/tests/test_desktop.py +75 -0
  40. station_server-0.4.0/tests/test_playlist.py +76 -0
  41. station_server-0.4.0/tests/test_schedule.py +52 -0
  42. station_server-0.4.0/tests/test_senders.py +123 -0
  43. station_server-0.4.0/tests/test_updates.py +182 -0
  44. station_server-0.4.0/tools/make_test_station.py +109 -0
@@ -0,0 +1,61 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ .venv/
6
+ venv/
7
+ *.db
8
+ *.db-journal
9
+ *.db-wal
10
+ *.db-shm
11
+ .DS_Store
12
+ node_modules/
13
+ .svelte-kit/
14
+ web/build/
15
+ dist/
16
+ build/
17
+ *.egg-info/
18
+ .env
19
+ .env.local
20
+
21
+ # User-dropped iTunes XML exports (large, not repo content)
22
+ from_user/*
23
+ !from_user/README.md
24
+
25
+ # Generated iTunes XML exports (default disk-export destination)
26
+ out/
27
+ output/
28
+
29
+ # Playwright
30
+ web/test-results/
31
+ web/playwright-report/
32
+ test-results/
33
+ playwright-report/
34
+ playwright-test.*.db
35
+ playwright-test.*.db-wal
36
+ playwright-test.*.db-shm
37
+ playwright-stations.*/
38
+
39
+ # Coverage output
40
+ .coverage
41
+ web/coverage/
42
+
43
+ # Database backups (DESIGN.md §14.18). User-data; never repo content.
44
+ backups/
45
+ .claude/
46
+
47
+ # §16.15 c1 — MkDocs build output. Source lives in docs/; the
48
+ # rendered HTML/CSS/JS lands in site/ and is published to GH Pages
49
+ # via the docs workflow, never committed to main.
50
+ site/
51
+ # §16.15 c4 — OpenAPI dump consumed by mkdocs-render-swagger-plugin.
52
+ # Regenerated by `make docs-openapi` (chained from docs-build +
53
+ # docs-serve); committing would risk drift between the JSON and
54
+ # the FastAPI app's actual schema.
55
+ docs/reference/openapi.json
56
+ # §16.15 c5 — CLI --help dump consumed by the docs build.
57
+ # Regenerated by `make docs-cli`; same drift logic as openapi.json.
58
+ docs/reference/cli.md
59
+
60
+ # ffmpeg fetched for the Station Server downloads (server-installers.yml)
61
+ server/packaging/ffmpeg/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Raby Whyte
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,275 @@
1
+ Metadata-Version: 2.5
2
+ Name: station-server
3
+ Version: 0.4.0
4
+ Summary: Broadcast station-studio playlists to SHOUTcast or Icecast
5
+ Author-email: Raby Whyte <rabywhyte@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025-2026 Raby Whyte
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: icecast,itunes,playlist,playout,radio,shoutcast
29
+ Requires-Python: >=3.11
30
+ Requires-Dist: station-player>=0.17
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Station Server
36
+
37
+ Puts a station-studio playlist on air. It plays the week's tracks in order,
38
+ in real time, and sends them straight to your SHOUTcast or Icecast server
39
+ with each song's title. That replaces iTunes playing out of the headphone
40
+ jack, the loopback cable, and Winamp with the SHOUTcast DSP plugin.
41
+
42
+ No sound card is involved: the music files are decoded, joined and encoded
43
+ inside Station Server. Nothing else playing on the computer (a YouTube
44
+ video, system sounds) can reach the broadcast, and one computer can run
45
+ both stations.
46
+
47
+ **Status: 0.4: an app you download and install, running as many stations
48
+ as you like on one computer, that updates itself.** Each station has its own playlist, server
49
+ and play log. See the
50
+ [plan](https://claude.ai/artifact/RMcRHeFUDnw2BTvhGskcmd).
51
+
52
+ ## Install
53
+
54
+ **Download** (no Python or terminal needed): a Mac `.dmg` (Apple Silicon
55
+ or Intel) or a Windows setup `.exe`, each with its own ffmpeg. They're on
56
+ each `server-v…` release of this repository. They're also built for
57
+ every pull request that changes Station Server: open the pull request's
58
+ *Checks* › **Station Server installers** run, and download from its
59
+ *Artifacts*.
60
+
61
+ - **Mac:** open the `.dmg` and drag Station Server to Applications. The
62
+ first time, macOS says it can't check the app: open System Settings ›
63
+ Privacy & Security and click **Open Anyway** (the app isn't signed with
64
+ a paid Apple certificate).
65
+ - **Windows:** run the setup. If SmartScreen stops it, click **More info**
66
+ › **Run anyway**. It installs for you only, with no administrator
67
+ password, and adds Station Server to the Start menu.
68
+
69
+ Then, on the station's computer, turn on **Settings › Start Station Server
70
+ when I log in**, so that after a restart, a power cut or Windows updates it
71
+ starts in the background and puts the stations that were on air back on
72
+ air. On a Mac it's also started again if it ever stops unexpectedly.
73
+
74
+ **From a checkout** (for development, or Linux): you need Python 3.11 or
75
+ newer and **ffmpeg** with its MP3 encoder (macOS: `brew install ffmpeg`;
76
+ Windows: `winget install ffmpeg`; or point Station Server at one with
77
+ `--ffmpeg PATH` or `$STATION_SERVER_FFMPEG`).
78
+
79
+ ```bash
80
+ python -m venv .venv
81
+ .venv/bin/pip install ./player ./server # Windows: .venv\Scripts\pip
82
+ ```
83
+
84
+ Station Server reads playlists with station-player's code, so both are
85
+ installed. Then run `.venv/bin/station-server` (Windows:
86
+ `.venv\Scripts\station-server`).
87
+
88
+ You'll also need the music on the computer, and the playlists exported from
89
+ station-studio (or any iTunes / Music XML).
90
+
91
+ ## The app
92
+
93
+ ```bash
94
+ .venv/bin/station-server
95
+ ```
96
+
97
+ opens Station Server in a window of its own (with Chrome, Edge or Brave;
98
+ otherwise a browser tab). It keeps running in the background: closing the
99
+ window doesn't take a station off air, and running the command again just
100
+ opens the window. **Quit** in the sidebar stops it.
101
+
102
+ 1. **Add a station.** Give it a name and its playlist: a station-studio
103
+ export, or better its exports folder
104
+ (`~/.station-studio/exports/rebirth-classics`), so the newest week is
105
+ played and next week's is picked up by itself at the end of a track.
106
+ 2. **Server.** SHOUTcast or Icecast, the address and port, and the source
107
+ password. **Test connection** checks them without going on air.
108
+ 3. **Go on air.** The station page shows the track playing, the running
109
+ order with the time each track goes out, and any tracks whose files
110
+ can't be found.
111
+
112
+ **Joining the week by the clock.** The playlist's first track is pinned to
113
+ the start of the week (Monday 00:00 unless you change it), and the
114
+ tracks' lengths say when each of the others airs. Go on air on Wednesday
115
+ afternoon, or restart after a power cut, and the station joins where the
116
+ clock says, part way into a track. Choose *From the top* to start at
117
+ track 1 instead. A playlist shorter than a week loops.
118
+
119
+ A station that was on air when the app quit goes back on air when it
120
+ starts. If no track in the playlist can be read, it sends silence and
121
+ keeps trying rather than dropping off the server.
122
+
123
+ **Play log.** Every track played is logged; download a date range as CSV
124
+ for PRS and PPL returns (the same format as station-player's).
125
+
126
+ **Dock and taskbar icon.** In the app window, Settings → *Install as app*
127
+ gives Station Server its own icon in the Dock or taskbar, with Add a
128
+ station and Play log on its menu, and it opens that way from then on.
129
+ Opened from there while Station Server isn't running, the window starts it
130
+ (through the `station-server://` link the app registers) and waits.
131
+
132
+ **New versions.** As in Station Player and Station Studio, a note at the
133
+ foot of the sidebar says when a new version is out, and Settings ›
134
+ Updates has *Check for updates* and *Update now*: the downloaded app
135
+ fetches the new version from its GitHub release, checks it's the genuine
136
+ file, and restarts; stations that were on air go back on air. It asks
137
+ GitHub once a day; turn that off in Settings.
138
+
139
+ **Two stations on one computer.** Add a station for each; they run side
140
+ by side, each with its own playlist, server, clock and play log, and go on
141
+ and off air separately. Rebirth Classics to `shoutcast://…:8000` and
142
+ Rebirth Radio to `shoutcast://…:8002`, say, from one PC.
143
+
144
+ Settings, passwords, play logs and the log file `station-server.log` are kept in `~/.station-server`
145
+ (`$STATION_SERVER_HOME` to change it); the settings file is readable by
146
+ you only. `station-server app --port N` uses another port than 8770.
147
+
148
+ ## From the command line
149
+
150
+ Check that ffmpeg is found and the playlist's music is all there:
151
+
152
+ ```bash
153
+ station-server check ~/.station-studio/exports/rebirth-classics-week-12.xml
154
+ ```
155
+
156
+ Record what listeners would hear to an MP3 (and the titles to a .txt beside it):
157
+
158
+ ```bash
159
+ station-server play week-12.xml --to test.mp3 --once
160
+ ```
161
+
162
+ ## Go on air
163
+
164
+ The password is the **source** password (the one Winamp's DSP plugin uses).
165
+ Give it with `--password`, or set `STATION_SERVER_PASSWORD` so it isn't
166
+ typed on the command line.
167
+
168
+ **SHOUTcast** (DNAS 1.9.x or 2.x). Use the listener port, the one in the
169
+ station's link; Station Server connects to the port above it, as Winamp does.
170
+
171
+ ```bash
172
+ station-server play week-12.xml --to shoutcast://81.150.147.206:8000 \
173
+ --name "Rebirth Classics" --genre "Vintage Oldies / Club Classics" \
174
+ --url https://www.rebirthclassics.uk
175
+ ```
176
+
177
+ For stream 2 on a DNAS 2.x add `?sid=2`: `shoutcast://host:8002?sid=2`.
178
+
179
+ **Icecast** (2.4 or later), to a mount:
180
+
181
+ ```bash
182
+ station-server play week-12.xml --to icecast://source@my.server:8000/classics
183
+ ```
184
+
185
+ While it plays, the window shows each track as it starts. Type `s` and
186
+ Enter to skip a track; Ctrl+C stops.
187
+
188
+ ### Trying it on a Mac with your own Icecast
189
+
190
+ No station needed: run Icecast on the same Mac and listen in station-player.
191
+
192
+ ```bash
193
+ brew install ffmpeg icecast
194
+ mkdir -p "$(brew --prefix)/var/log/icecast" # Homebrew doesn't make it
195
+ icecast -c "$(brew --prefix)/etc/icecast.xml" # leave running; Ctrl+C stops
196
+ ```
197
+
198
+ http://localhost:8000 shows Icecast's status page. Its source password is
199
+ `hackme` until you change `<source-password>` (and `<admin-password>`) in
200
+ that `icecast.xml`. In a second terminal run `.venv/bin/station-server`,
201
+ add a station with the playlist folder
202
+ `~/.station-studio/exports/rebirth-classics`, server **Icecast**,
203
+ `localhost`, port `8000`, mount `/classics`, password `hackme`, and go on
204
+ air. Or from the command line:
205
+
206
+ ```bash
207
+ .venv/bin/station-server play ~/.station-studio/exports/rebirth-classics/012.xml \
208
+ --to icecast://localhost:8000/classics --password hackme --name "Rebirth Classics"
209
+ ```
210
+
211
+ Then add the station `http://localhost:8000/classics` in station-player.
212
+ The title changes with each track, and the status page shows the stream.
213
+
214
+ No music to hand? `python server/tools/make_test_station.py` makes a
215
+ folder of short test songs and a week's station-studio export in
216
+ `~/station-server-test`, and prints the exports folder to use as the
217
+ playlist. `--week 13` adds next week's, to watch the switch-over;
218
+ `--missing` adds a song whose file isn't there.
219
+
220
+ ### Trying it next to Winamp
221
+
222
+ Only one source can feed a stream at a time. To compare without taking the
223
+ station off air, send Station Server to a spare stream: a second DNAS on
224
+ another port, stream 2 on a DNAS 2.x (`?sid=2`, set up in its config), or a
225
+ test Icecast mount. Listen to both in station-player.
226
+
227
+ ## Command-line options
228
+
229
+ | Option | What it does |
230
+ | --- | --- |
231
+ | `--playlist NAME` | Which playlist, when the XML has several (a full library export) |
232
+ | `--music-folder DIR` | Where to look for tracks that have moved since the XML was made (repeatable) |
233
+ | `--remap FROM=TO` | Rewrite a path prefix, e.g. a Mac library played on a PC: `--remap "/Users/rob/Music=D:/Music"` |
234
+ | `--start N` | Start at track N |
235
+ | `--once` | Stop at the end of the playlist instead of starting again |
236
+ | `--bitrate KBPS` | MP3 bitrate, 128 by default |
237
+ | `--no-level` | Turn off volume levelling |
238
+ | `--name`, `--genre`, `--url` | What the server shows about the station |
239
+ | `--public` | Ask the server to list the station in its directory |
240
+
241
+ ## How it works
242
+
243
+ ```
244
+ playlist XML ─► each track decoded by ffmpeg ─► mixer (gapless, levelled)
245
+ ─► held to real time ─► one MP3 encoder ─► SHOUTcast / Icecast
246
+ ```
247
+
248
+ - **Gapless.** Tracks are joined as raw audio before one encoder, so there's
249
+ no gap and no reconnect between songs.
250
+ - **Volume levelling** uses the ReplayGain or iTunes Sound Check values in
251
+ each file (read by station-player), never boosting more than 10 dB or past
252
+ the track's peak. Files without them play as they are for now.
253
+ - **Titles** reach the server when the track is actually heard, not when its
254
+ audio is first sent.
255
+ - **Dropped connection.** If the server goes away, Station Server keeps
256
+ playing and reconnects every 5 seconds. A track it can't read is skipped.
257
+
258
+ ## Releasing
259
+
260
+ Bump the version in `server/pyproject.toml` and
261
+ `server/station_server/__init__.py`, merge, then run Actions ›
262
+ **Publish Station Server** on main. It tests, makes a draft
263
+ `server-v<version>` release, builds the Mac and Windows downloads onto
264
+ it, and publishes it once they're all there.
265
+
266
+ ## Development
267
+
268
+ ```bash
269
+ cd server
270
+ python -m pytest -q
271
+ ```
272
+
273
+ The tests run without ffmpeg, skipping the end-to-end ones; with ffmpeg
274
+ installed they also play real audio through fake SHOUTcast and Icecast
275
+ servers.
@@ -0,0 +1,241 @@
1
+ # Station Server
2
+
3
+ Puts a station-studio playlist on air. It plays the week's tracks in order,
4
+ in real time, and sends them straight to your SHOUTcast or Icecast server
5
+ with each song's title. That replaces iTunes playing out of the headphone
6
+ jack, the loopback cable, and Winamp with the SHOUTcast DSP plugin.
7
+
8
+ No sound card is involved: the music files are decoded, joined and encoded
9
+ inside Station Server. Nothing else playing on the computer (a YouTube
10
+ video, system sounds) can reach the broadcast, and one computer can run
11
+ both stations.
12
+
13
+ **Status: 0.4: an app you download and install, running as many stations
14
+ as you like on one computer, that updates itself.** Each station has its own playlist, server
15
+ and play log. See the
16
+ [plan](https://claude.ai/artifact/RMcRHeFUDnw2BTvhGskcmd).
17
+
18
+ ## Install
19
+
20
+ **Download** (no Python or terminal needed): a Mac `.dmg` (Apple Silicon
21
+ or Intel) or a Windows setup `.exe`, each with its own ffmpeg. They're on
22
+ each `server-v…` release of this repository. They're also built for
23
+ every pull request that changes Station Server: open the pull request's
24
+ *Checks* › **Station Server installers** run, and download from its
25
+ *Artifacts*.
26
+
27
+ - **Mac:** open the `.dmg` and drag Station Server to Applications. The
28
+ first time, macOS says it can't check the app: open System Settings ›
29
+ Privacy & Security and click **Open Anyway** (the app isn't signed with
30
+ a paid Apple certificate).
31
+ - **Windows:** run the setup. If SmartScreen stops it, click **More info**
32
+ › **Run anyway**. It installs for you only, with no administrator
33
+ password, and adds Station Server to the Start menu.
34
+
35
+ Then, on the station's computer, turn on **Settings › Start Station Server
36
+ when I log in**, so that after a restart, a power cut or Windows updates it
37
+ starts in the background and puts the stations that were on air back on
38
+ air. On a Mac it's also started again if it ever stops unexpectedly.
39
+
40
+ **From a checkout** (for development, or Linux): you need Python 3.11 or
41
+ newer and **ffmpeg** with its MP3 encoder (macOS: `brew install ffmpeg`;
42
+ Windows: `winget install ffmpeg`; or point Station Server at one with
43
+ `--ffmpeg PATH` or `$STATION_SERVER_FFMPEG`).
44
+
45
+ ```bash
46
+ python -m venv .venv
47
+ .venv/bin/pip install ./player ./server # Windows: .venv\Scripts\pip
48
+ ```
49
+
50
+ Station Server reads playlists with station-player's code, so both are
51
+ installed. Then run `.venv/bin/station-server` (Windows:
52
+ `.venv\Scripts\station-server`).
53
+
54
+ You'll also need the music on the computer, and the playlists exported from
55
+ station-studio (or any iTunes / Music XML).
56
+
57
+ ## The app
58
+
59
+ ```bash
60
+ .venv/bin/station-server
61
+ ```
62
+
63
+ opens Station Server in a window of its own (with Chrome, Edge or Brave;
64
+ otherwise a browser tab). It keeps running in the background: closing the
65
+ window doesn't take a station off air, and running the command again just
66
+ opens the window. **Quit** in the sidebar stops it.
67
+
68
+ 1. **Add a station.** Give it a name and its playlist: a station-studio
69
+ export, or better its exports folder
70
+ (`~/.station-studio/exports/rebirth-classics`), so the newest week is
71
+ played and next week's is picked up by itself at the end of a track.
72
+ 2. **Server.** SHOUTcast or Icecast, the address and port, and the source
73
+ password. **Test connection** checks them without going on air.
74
+ 3. **Go on air.** The station page shows the track playing, the running
75
+ order with the time each track goes out, and any tracks whose files
76
+ can't be found.
77
+
78
+ **Joining the week by the clock.** The playlist's first track is pinned to
79
+ the start of the week (Monday 00:00 unless you change it), and the
80
+ tracks' lengths say when each of the others airs. Go on air on Wednesday
81
+ afternoon, or restart after a power cut, and the station joins where the
82
+ clock says, part way into a track. Choose *From the top* to start at
83
+ track 1 instead. A playlist shorter than a week loops.
84
+
85
+ A station that was on air when the app quit goes back on air when it
86
+ starts. If no track in the playlist can be read, it sends silence and
87
+ keeps trying rather than dropping off the server.
88
+
89
+ **Play log.** Every track played is logged; download a date range as CSV
90
+ for PRS and PPL returns (the same format as station-player's).
91
+
92
+ **Dock and taskbar icon.** In the app window, Settings → *Install as app*
93
+ gives Station Server its own icon in the Dock or taskbar, with Add a
94
+ station and Play log on its menu, and it opens that way from then on.
95
+ Opened from there while Station Server isn't running, the window starts it
96
+ (through the `station-server://` link the app registers) and waits.
97
+
98
+ **New versions.** As in Station Player and Station Studio, a note at the
99
+ foot of the sidebar says when a new version is out, and Settings ›
100
+ Updates has *Check for updates* and *Update now*: the downloaded app
101
+ fetches the new version from its GitHub release, checks it's the genuine
102
+ file, and restarts; stations that were on air go back on air. It asks
103
+ GitHub once a day; turn that off in Settings.
104
+
105
+ **Two stations on one computer.** Add a station for each; they run side
106
+ by side, each with its own playlist, server, clock and play log, and go on
107
+ and off air separately. Rebirth Classics to `shoutcast://…:8000` and
108
+ Rebirth Radio to `shoutcast://…:8002`, say, from one PC.
109
+
110
+ Settings, passwords, play logs and the log file `station-server.log` are kept in `~/.station-server`
111
+ (`$STATION_SERVER_HOME` to change it); the settings file is readable by
112
+ you only. `station-server app --port N` uses another port than 8770.
113
+
114
+ ## From the command line
115
+
116
+ Check that ffmpeg is found and the playlist's music is all there:
117
+
118
+ ```bash
119
+ station-server check ~/.station-studio/exports/rebirth-classics-week-12.xml
120
+ ```
121
+
122
+ Record what listeners would hear to an MP3 (and the titles to a .txt beside it):
123
+
124
+ ```bash
125
+ station-server play week-12.xml --to test.mp3 --once
126
+ ```
127
+
128
+ ## Go on air
129
+
130
+ The password is the **source** password (the one Winamp's DSP plugin uses).
131
+ Give it with `--password`, or set `STATION_SERVER_PASSWORD` so it isn't
132
+ typed on the command line.
133
+
134
+ **SHOUTcast** (DNAS 1.9.x or 2.x). Use the listener port, the one in the
135
+ station's link; Station Server connects to the port above it, as Winamp does.
136
+
137
+ ```bash
138
+ station-server play week-12.xml --to shoutcast://81.150.147.206:8000 \
139
+ --name "Rebirth Classics" --genre "Vintage Oldies / Club Classics" \
140
+ --url https://www.rebirthclassics.uk
141
+ ```
142
+
143
+ For stream 2 on a DNAS 2.x add `?sid=2`: `shoutcast://host:8002?sid=2`.
144
+
145
+ **Icecast** (2.4 or later), to a mount:
146
+
147
+ ```bash
148
+ station-server play week-12.xml --to icecast://source@my.server:8000/classics
149
+ ```
150
+
151
+ While it plays, the window shows each track as it starts. Type `s` and
152
+ Enter to skip a track; Ctrl+C stops.
153
+
154
+ ### Trying it on a Mac with your own Icecast
155
+
156
+ No station needed: run Icecast on the same Mac and listen in station-player.
157
+
158
+ ```bash
159
+ brew install ffmpeg icecast
160
+ mkdir -p "$(brew --prefix)/var/log/icecast" # Homebrew doesn't make it
161
+ icecast -c "$(brew --prefix)/etc/icecast.xml" # leave running; Ctrl+C stops
162
+ ```
163
+
164
+ http://localhost:8000 shows Icecast's status page. Its source password is
165
+ `hackme` until you change `<source-password>` (and `<admin-password>`) in
166
+ that `icecast.xml`. In a second terminal run `.venv/bin/station-server`,
167
+ add a station with the playlist folder
168
+ `~/.station-studio/exports/rebirth-classics`, server **Icecast**,
169
+ `localhost`, port `8000`, mount `/classics`, password `hackme`, and go on
170
+ air. Or from the command line:
171
+
172
+ ```bash
173
+ .venv/bin/station-server play ~/.station-studio/exports/rebirth-classics/012.xml \
174
+ --to icecast://localhost:8000/classics --password hackme --name "Rebirth Classics"
175
+ ```
176
+
177
+ Then add the station `http://localhost:8000/classics` in station-player.
178
+ The title changes with each track, and the status page shows the stream.
179
+
180
+ No music to hand? `python server/tools/make_test_station.py` makes a
181
+ folder of short test songs and a week's station-studio export in
182
+ `~/station-server-test`, and prints the exports folder to use as the
183
+ playlist. `--week 13` adds next week's, to watch the switch-over;
184
+ `--missing` adds a song whose file isn't there.
185
+
186
+ ### Trying it next to Winamp
187
+
188
+ Only one source can feed a stream at a time. To compare without taking the
189
+ station off air, send Station Server to a spare stream: a second DNAS on
190
+ another port, stream 2 on a DNAS 2.x (`?sid=2`, set up in its config), or a
191
+ test Icecast mount. Listen to both in station-player.
192
+
193
+ ## Command-line options
194
+
195
+ | Option | What it does |
196
+ | --- | --- |
197
+ | `--playlist NAME` | Which playlist, when the XML has several (a full library export) |
198
+ | `--music-folder DIR` | Where to look for tracks that have moved since the XML was made (repeatable) |
199
+ | `--remap FROM=TO` | Rewrite a path prefix, e.g. a Mac library played on a PC: `--remap "/Users/rob/Music=D:/Music"` |
200
+ | `--start N` | Start at track N |
201
+ | `--once` | Stop at the end of the playlist instead of starting again |
202
+ | `--bitrate KBPS` | MP3 bitrate, 128 by default |
203
+ | `--no-level` | Turn off volume levelling |
204
+ | `--name`, `--genre`, `--url` | What the server shows about the station |
205
+ | `--public` | Ask the server to list the station in its directory |
206
+
207
+ ## How it works
208
+
209
+ ```
210
+ playlist XML ─► each track decoded by ffmpeg ─► mixer (gapless, levelled)
211
+ ─► held to real time ─► one MP3 encoder ─► SHOUTcast / Icecast
212
+ ```
213
+
214
+ - **Gapless.** Tracks are joined as raw audio before one encoder, so there's
215
+ no gap and no reconnect between songs.
216
+ - **Volume levelling** uses the ReplayGain or iTunes Sound Check values in
217
+ each file (read by station-player), never boosting more than 10 dB or past
218
+ the track's peak. Files without them play as they are for now.
219
+ - **Titles** reach the server when the track is actually heard, not when its
220
+ audio is first sent.
221
+ - **Dropped connection.** If the server goes away, Station Server keeps
222
+ playing and reconnects every 5 seconds. A track it can't read is skipped.
223
+
224
+ ## Releasing
225
+
226
+ Bump the version in `server/pyproject.toml` and
227
+ `server/station_server/__init__.py`, merge, then run Actions ›
228
+ **Publish Station Server** on main. It tests, makes a draft
229
+ `server-v<version>` release, builds the Mac and Windows downloads onto
230
+ it, and publishes it once they're all there.
231
+
232
+ ## Development
233
+
234
+ ```bash
235
+ cd server
236
+ python -m pytest -q
237
+ ```
238
+
239
+ The tests run without ffmpeg, skipping the end-to-end ones; with ffmpeg
240
+ installed they also play real audio through fake SHOUTcast and Icecast
241
+ servers.
@@ -0,0 +1,6 @@
1
+ """What the downloaded Mac and Windows apps run (see station-server.spec)."""
2
+ import sys
3
+
4
+ from station_server.__main__ import main
5
+
6
+ sys.exit(main())
@@ -0,0 +1,78 @@
1
+ # PyInstaller recipe for the Mac and Windows downloads: Station Server with
2
+ # its own Python and its own ffmpeg, so it installs without a terminal.
3
+ #
4
+ # pip install pyinstaller ../player .
5
+ # (put an ffmpeg build at packaging/ffmpeg/ffmpeg or ffmpeg.exe)
6
+ # pyinstaller packaging/station-server.spec
7
+ #
8
+ # Makes dist/Station Server.app (Mac) or dist/Station Server/ (Windows),
9
+ # which .github/workflows/server-installers.yml turns into a .dmg or a
10
+ # setup .exe.
11
+ import sys
12
+ import tomllib
13
+ from pathlib import Path
14
+
15
+ HERE = Path(SPECPATH)
16
+ ROOT = HERE.parent
17
+ VERSION = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["version"]
18
+ STATIC = ROOT / "station_server" / "static"
19
+ NAME = "Station Server"
20
+ HELPER = "station-server-helper" # keep in step with desktop.MAC_HELPER
21
+ MAC = sys.platform == "darwin"
22
+ FFMPEG = HERE / "ffmpeg" / ("ffmpeg" if sys.platform != "win32" else "ffmpeg.exe")
23
+ FFMPEG_LICENSE = HERE / "ffmpeg" / "LICENSE.txt"
24
+
25
+ a = Analysis(
26
+ [str(HERE / "launcher.py")],
27
+ pathex=[str(ROOT)],
28
+ binaries=[(str(FFMPEG), "ffmpeg")] if FFMPEG.is_file() else [],
29
+ datas=[(str(STATIC), "station_server/static"), (str(ROOT / "LICENSE"), ".")]
30
+ + ([(str(FFMPEG_LICENSE), "ffmpeg")] if FFMPEG_LICENSE.is_file() else []),
31
+ excludes=["tkinter", "unittest", "pydoc", "test"],
32
+ )
33
+ pyz = PYZ(a.pure)
34
+ exe = EXE(
35
+ pyz,
36
+ a.scripts,
37
+ [],
38
+ exclude_binaries=True,
39
+ name=NAME,
40
+ console=False, # no terminal window: Station Server's own window is the UI
41
+ argv_emulation=MAC, # station-server: links arrive as arguments
42
+ icon=str(STATIC / ("icon.icns" if MAC else "icon.ico")),
43
+ )
44
+ # On the Mac, the app the Finder opens starts this helper and exits: while
45
+ # a process macOS counts as the running app lives, later clicks on the app
46
+ # go to it and do nothing. The helper runs Station Server; start at login
47
+ # runs it too.
48
+ helper = EXE(
49
+ pyz,
50
+ a.scripts,
51
+ [],
52
+ exclude_binaries=True,
53
+ name=HELPER,
54
+ console=False,
55
+ ) if MAC else None
56
+ coll = COLLECT(exe, *([helper] if helper else []), a.binaries, a.datas, name=NAME)
57
+
58
+ if MAC:
59
+ app = BUNDLE(
60
+ coll,
61
+ name=f"{NAME}.app",
62
+ icon=str(STATIC / "icon.icns"),
63
+ bundle_identifier="io.github.raby.StationServer",
64
+ version=VERSION,
65
+ info_plist={
66
+ "CFBundleDisplayName": NAME,
67
+ "CFBundleShortVersionString": VERSION,
68
+ "CFBundleVersion": VERSION,
69
+ "LSMinimumSystemVersion": "11.0",
70
+ # The UI is a browser window, so the app itself needs no Dock icon.
71
+ "LSUIElement": True,
72
+ "NSHighResolutionCapable": True,
73
+ # station-server://start, from the installed app's window when
74
+ # Station Server isn't running (see __main__.URL_SCHEME).
75
+ "CFBundleURLTypes": [{"CFBundleURLName": "io.github.raby.StationServer",
76
+ "CFBundleURLSchemes": ["station-server"]}],
77
+ },
78
+ )