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.
- station_server-0.4.0/.gitignore +61 -0
- station_server-0.4.0/LICENSE +21 -0
- station_server-0.4.0/PKG-INFO +275 -0
- station_server-0.4.0/README.md +241 -0
- station_server-0.4.0/packaging/launcher.py +6 -0
- station_server-0.4.0/packaging/station-server.spec +78 -0
- station_server-0.4.0/packaging/windows/installer.iss +69 -0
- station_server-0.4.0/pyproject.toml +33 -0
- station_server-0.4.0/station_server/__init__.py +9 -0
- station_server-0.4.0/station_server/__main__.py +299 -0
- station_server-0.4.0/station_server/audio.py +171 -0
- station_server-0.4.0/station_server/channel.py +241 -0
- station_server-0.4.0/station_server/config.py +188 -0
- station_server-0.4.0/station_server/desktop.py +122 -0
- station_server-0.4.0/station_server/onair.py +359 -0
- station_server-0.4.0/station_server/playlist.py +72 -0
- station_server-0.4.0/station_server/schedule.py +123 -0
- station_server-0.4.0/station_server/selfupdate.py +277 -0
- station_server-0.4.0/station_server/senders.py +314 -0
- station_server-0.4.0/station_server/static/app.js +793 -0
- station_server-0.4.0/station_server/static/icon-192.png +0 -0
- station_server-0.4.0/station_server/static/icon-512.png +0 -0
- station_server-0.4.0/station_server/static/icon.icns +0 -0
- station_server-0.4.0/station_server/static/icon.ico +0 -0
- station_server-0.4.0/station_server/static/icon.png +0 -0
- station_server-0.4.0/station_server/static/index.html +41 -0
- station_server-0.4.0/station_server/static/manifest.webmanifest +31 -0
- station_server-0.4.0/station_server/static/offline.html +19 -0
- station_server-0.4.0/station_server/static/offline.js +29 -0
- station_server-0.4.0/station_server/static/style.css +333 -0
- station_server-0.4.0/station_server/static/sw.js +29 -0
- station_server-0.4.0/station_server/updates.py +91 -0
- station_server-0.4.0/station_server/web.py +236 -0
- station_server-0.4.0/station_server/window.py +69 -0
- station_server-0.4.0/tests/conftest.py +230 -0
- station_server-0.4.0/tests/test_app.py +189 -0
- station_server-0.4.0/tests/test_channel.py +246 -0
- station_server-0.4.0/tests/test_config.py +59 -0
- station_server-0.4.0/tests/test_desktop.py +75 -0
- station_server-0.4.0/tests/test_playlist.py +76 -0
- station_server-0.4.0/tests/test_schedule.py +52 -0
- station_server-0.4.0/tests/test_senders.py +123 -0
- station_server-0.4.0/tests/test_updates.py +182 -0
- 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,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
|
+
)
|