Skip to content

nemb111/streambert

Repository files navigation

Streambert

This project is for you if you have at least one Hörbert in your household, you're somewhat tech‑savvy, and you want to greatly reduce the parental interaction required to create new playlists for the kids (or anyone else). 😉

The idea is to use a server (for example a Raspberry Pi or mini PC) to stream local MP3 files to a Hörbert over Wi‑Fi. To achieve this goal, it makes use of Hörbert's streaming capabilities normally used for listening to internet radio stations.

The MP3 files are organized in folders on the server into playlists, albums, and tracks. Using Hörbert's buttons, the user can navigate the content and play the desired audio files.


If you find this project helpful and want to support me:

Buy Me a Coffee


Table of Contents


Features

  • Stream a continuous MP3 audio feed over HTTP
  • Per-client playback state (so different clients can be on different tracks)
  • Skip to next track / album / playlist via dedicated endpoints
  • Configurable playlist root and playback behavior via environment variables

Requirements

  • uv package manager.

  • Note: This project uses the FFmpeg binary from the pyffmpeg PyPI package. That means you don't have to install FFmpeg manually, but you also need to trust the FFmpeg binary bundled with pyffmpeg.

Python dependencies are declared in pyproject.toml.

Installation

The only requirement is to install the uv package manager.

You can do it manually or follow the next steps.

If you are on Windows

Open PowerShell and run:

winget install astral-sh.uv --interactive

Note: the Windows Package Manager (winget) must be installed (it is included by default on Windows 11).

If you are on Linux

Debian/Ubuntu-based

sudo apt install uv

Fedora-based

sudo dnf install uv

Virtual environment

This step is independent of the operating system you are using. Change directory to the project's root folder (streambert by default) and run:

uv venv -p 3.13

Now uv downloads the correct Python version and creates a folder called .venv in the current working directory that contains the virtual environment.

Double-check that the .venv folder exists in the root folder of this project.

Quickstart

Ideally you are running a server that is available 24/7, such as a Raspberry Pi, another single-board computer, a mini PC, or a full-fledged server.

Any computer will work, but you must ensure it is running when the Hörbert is used — the goal is to reduce the required parental interaction.

Now let's see it in action:

Place at least one MP3 file into the playlists folder. Otherwise the server will raise an error indicating that no playlist could be found.

From the root directory of the project run:

uv run python main.py

You should see output similar to the following:

Installed 15 packages in 5ms
2025-10-13 16:17:38,947 - pyffmpeg.FFmpeg - INFO - FFmpeg Initialising
2025-10-13 16:17:38,947 - pyffmpeg.FFmpeg - INFO - Save directory: .
2025-10-13 16:17:38,947 - pyffmpeg.FFmpeg - INFO - Checking GitHub Activeness: True
2025-10-13 16:17:38,947 - pyffmpeg.misc.Paths - INFO - bin folder: /home/pc/.pyffmpeg/bin
2025-10-13 16:17:38,947 - pyffmpeg.misc.Paths - INFO - Inside load_ffmpeg_bin
2025-10-13 16:17:38,947 - pyffmpeg.FFmpeg - INFO - FFmpeg file: /home/pc/.pyffmpeg/bin/ffmpeg
2025-10-13 16:17:38,947 - pyffmpeg.FFmpeg - INFO - Inside get_ffmpeg_bin
 * Serving Flask app 'main'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8999
 * Running on http://192.168.1.37:8999
Press CTRL+C to quit

The Flask development server prints a warning, but that is acceptable for local/home LAN use.

Find the IP address of the machine running Streambert (for example: http://192.168.1.37:8999).

Visit:

http://SERVER_IP:PORT/playlist

in your favorite browser.

Replace SERVER_IP with your server's IP address. Your MP3 files should play in the browser. 👏

Setting up Hörbert

After confirming playback in a browser, assign these URLs to Hörbert's buttons:

http://SERVER_IP:PORT/playlist
http://SERVER_IP:PORT/album
http://SERVER_IP:PORT/track

Using the earlier example IP, the URLs would be:

http://192.168.1.37:8999/playlist
http://192.168.1.37:8999/album
http://192.168.1.37:8999/track

See Hörbert's official documentation for instructions on assigning URLs to buttons: https://www.hoerbert.com/service/anleitungen-und-videos/hoerbert/

Playlists, albums and tracks

🟩 Playlist
🟦 Album
🟧 Track


playlists/
├──🟩normal_playlist/
│   ├──🟦happy_music/
│   │   ├──🟧01-song.mp3
│   │   └──🟧02-song.mp3
│   └──🟦audio_book/
│       ├──🟧01-chapter1.mp3
│       └──🟧02-chapter2.mp3
├──🟩deeply_nested_playlist/
│   ├──🟦deep_album/
│   │   └── nested_folder1/
│   │       └── nested_folder2/
│   │           └──🟧deep_nested_song.mp3
│   └──🟦normal_album/
│       ├──🟧song1.mp3
│       └──🟧song2.mp3
├──🟩playlist_with_unnested_mp3/
│   ├──🟦normal_album/
│   │   ├──🟧song1.mp3
│   │   └──🟧song2.mp3
│   └──🟦🟧this_mp3_is_also_an_album.mp3
└──🟩🟦🟧this_mp3_is_also_a_playlist_and_album.mp3

Playlists, albums, and tracks are determined by the directory layout under the playlists/ root.

A playlist is either a directory directly under playlists/ or an MP3 file directly inside playlists/.

An album is a direct child (directory or MP3 file) inside a playlist directory.

Deeper nested folders have no special semantics and are treated as ordinary folders. An MP3 file is always a track, but it may also act as a playlist or album depending on where it appears.

Hörbert button logic

The section Setting up Hörbert described the mapping of Hörbert's buttons to different URLs. We call the button mapped to http://SERVER_IP:PORT/playlist the playlist button; the other two buttons are the album button and the track button.

  • playlist button 🟩 Every time this button is pressed you jump from one playlist to the next, and the first track found is played. This works circularly — when the last playlist is reached and the button is pressed again, playback wraps to the first playlist. Example — playlist button behavior

    1. Starting state:

      • Current playlist: 🟩 playlist_with_unnested_mp3
      • Current track: first track in that playlist
    2. Press playlist button once:

      • Jumps to the next playlist which is a single MP3 file treated as both playlist and album:
      • Current playlist: 🟩🟦🟧 this_mp3_is_also_a_playlist_and_album.mp3
      • Current track: this_mp3_is_also_a_playlist_and_album.mp3
    3. Press playlist button again:

      • Wraps to the next playlist directory:
      • Current playlist: 🟩 normal_playlist/
      • Current track: 🟧 01-song.mp3 (the first track inside normal_playlist)

    This demonstrates how the playlist button cycles through playlists (folders or top-level MP3 files). When a top-level MP3 file is encountered, it is treated as a playlist, an album, and a single track simultaneously.

  • album button 🟦 The album button moves only between albums inside the current playlist; it does not switch playlists. Use the playlist button to change playlists. Pressing the album button always starts playback at the first track found in the target album.

    Example — album button behavior

    1. Starting state:

      • Current playlist: 🟩 normal_playlist/
      • Current album: 🟦 happy_music/
      • Current track: 🟧 01-song.mp3 (the first track in the current album)
    2. Press the album button once:

      • Jumps to the next album inside the same playlist.
      • Current album: 🟦 audio_book/
      • Current track: 🟧 01-chapter1.mp3 (the first track in audio_book)
    3. Press the album button again:

      • Wraps to the first album of the playlist (behavior is circular within the playlist).
      • Current album: 🟦 happy_music/
      • Current track: 🟧 01-song.mp3
  • track button 🟧 moves between all tracks of the current playlist, crossing album boundaries when needed. The behavior is cyclic: when the last track of the playlist is reached and the track button is pressed again, playback wraps to the first track of the same playlist. Example — track button behavior

    1. Starting state:

      • Current playlist: 🟩 normal_playlist/
      • Current album: 🟦 happy_music/
      • Current track: 🟧 01-song.mp3 (first track in the playlist)
    2. Press the track button once:

      • Moves to the next track within the same album.
      • Current album: 🟦 happy_music/
      • Current track: 🟧 02-song.mp3
    3. Press the track button again:

      • Moves to the next track, crossing the album boundary into the next album.
      • Current album: 🟦 audio_book/
      • Current track: 🟧 01-chapter1.mp3
    4. Continue pressing until the end of the playlist:

      • When the last track of the playlist is reached and the track button is pressed again, playback wraps to the first track of the playlist.
      • Current playlist: 🟩 normal_playlist/
      • Current album: 🟦 happy_music/
      • Current track: 🟧 01-song.mp3

If no button is pressed, playback advances to the next track automatically (equivalent to repeatedly pressing the track button, but allowing each track to finish).

Configuration

Streambert is configured via environment variables. Defaults live in src/config.py. Important variables:

  • PLAYLISTS_ROOT_DIR (string) — Path to the playlists root directory. Default: ./playlists. Must point to an existing directory; the server exits with an error if the path is invalid.
  • HEART_BEAT_ALIVE_SEC (integer) — Seconds a client heartbeat is considered alive. Default: 3.
  • REWIND_SEC (integer >= 0) — Seconds to rewind when Hörbert switches from Off to On. Default: 10.
  • FLASK_HOST (string) — Host/IP to bind the Flask server. Default: 0.0.0.0 (binds to all interfaces).
  • FLASK_PORT (integer) — Port to bind the Flask server. Default: 8999.

Note: PLAYLISTS_ROOT_DIR may be a relative path — the code resolves it relative to the project root.

Example .env (shell-style) or command-line override

You can set configuration via environment variables. Example:

PLAYLISTS_ROOT_DIR=/home/pc/my_playlists
FLASK_HOST=0.0.0.0
FLASK_PORT=8999

Or for a one-off run:

PLAYLISTS_ROOT_DIR=/home/pc/my_playlists FLASK_PORT=80 uv run python main.py

API Endpoints

All endpoints return an MP3 audio stream with MIME type audio/mpeg.

  • GET /track — skip to the next track, then return the audio stream
  • GET /album — skip to the next album, then return the audio stream
  • GET /playlist — switch to the next playlist, then return the audio stream

Clients are tracked by remote address; each client has an independent playback state.

Notes and caveats

  • The server identifies clients by remote address; behind NAT or proxies this may not uniquely identify clients.
  • PLAYLISTS_ROOT_DIR must point to an existing directory. The server will exit with an error if the path is invalid.
  • The playlists root folder must contain at least one MP3 file.

Contributing

Contributions welcome. Open an issue or submit a pull request (include tests when applicable).

Troubleshooting

  • "No playlists found" error: ensure PLAYLISTS_ROOT_DIR points to an existing directory containing MP3 files. From the project root run ls -R ./playlists to verify.
  • Port in use: change FLASK_PORT or stop the process occupying that port.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages