Browser and streaming interface for local media files
Project description
Home Stream
A web-based browser and streaming interface for local media files. Supports in-browser playback, HTTP Basic Auth streaming (e.g. VLC), and user-based login with permanent stream URLs.
Features
- Browse local folders and media files
- Show titles and metadata in the browse view (toggleable): Kodi-style
.nfofiles for video, embedded tags for audio - Secure user login
- In-browser audio/video player (if supported by your browser)
- HTTP Basic Auth fallback for external players (VLC, mpv, etc.) using authenticated links
- Lightweight and dependency-minimal Python Flask app
Use-Cases for home-stream
The application is mainly designed with the use-case of running it on a NAS or home server with direct media access to allow accessing your media files remotely.
For this, you may put this application behind a reverse proxy or make it accessible via a VPN connection.
Of course, you can also use it purely locally, although there are probably better tools to administrate your media files.
Installation
Minimum Python version: 3.10
Install and run via pipx (Recommended)
pipx makes installing and running Python programs easier and avoids conflicts with other packages. Install it with
pip3 install pipx
The following one-liner both installs and runs this program from PyPI:
pipx run home-stream
If you want to be able to use the application without prepending it with pipx run every time, install it globally like so:
pipx install home-stream
The application will then be available in ~/.local/bin, which must be added to your $PATH. So make sure that ~/.local/bin is in your $PATH. On Windows, the required path for your environment may look like %USERPROFILE%\AppData\Roaming\Python\Python310\Scripts, depending on the Python version you have installed. Recent pipx versions allow you to do this via pipx ensurepath.
To upgrade the application to the newest available version, run this command:
pipx upgrade home-stream
Other installation methods
You may also use pure pip or poetry to install this package.
Configuration
Create a config.yaml file in the project root. You can derive your configuration from config.sample.yaml.
Passwords must be bcrypt-hashed. You can generate them using Python:
import bcrypt
print(bcrypt.hashpw(b"your-password", bcrypt.gensalt()).decode())
or by using online tools like bcrypt-generator.com, although not recommended for production passwords.
Usage
Start the app (session login + streaming):
home-stream -c config.yaml # uses Flask development server
Log in via browser, by default on localhost:8000. Browse and play media, or copy permanent stream URLs.
Production Webserver
For productive use, you should use a proper WSGI server. uWSGI is officially supported and ships in the Docker image. With the default download_method: direct, large file transfers run through the WSGI worker, which can strain servers like gunicorn (worker blocking, timeouts) — uWSGI tolerates this better. If you enable download offloading (see below), large files are served by your webserver instead, so the WSGI server only handles short requests and this distinction no longer matters.
For a quick start, run uv run uwsgi --ini uwsgi.ini. Using the docker image ghcr.io/mxmehl/home-stream you would have everything contained into one container, ready to be use locally or behind a reverse proxy. Note that depending on your use-cases, you may want to reconfigure some uwsgi settings. This currently would need to be done manually.
Large file downloads (offloading)
By default (download_method: direct), the application serves file bytes itself through the Python/uWSGI worker. This works, but for large files (movies) it ties up a worker for the whole transfer, interacts poorly with proxy buffering, and can hit timeouts (e.g. uWSGI's harakiri). This is the classic reason large downloads drop while a plain nginx/sftp serve never does.
The robust solution is to keep authentication and discovery in the app, but let your webserver push the bytes using a zero-copy sendfile() path. The app authorizes the request exactly as before (your permanent per-user tokens are unchanged), then hands the file to the webserver via a response header. Choose the mechanism that matches your front-end webserver via download_method in config.yaml:
The one rule that matters: whatever webserver serves the bytes must have direct read access to the media files at the same paths the app sees. This is the part that trips people up — see the note below.
Easiest robust option: an nginx sidecar container (recommended)
You do not need to bundle nginx into the app image, and you do not need your existing internet-facing reverse proxy to touch the media. The simplest robust setup is a dedicated nginx container that sits next to the app in the same docker-compose.yaml, shares the media volume, and does the offload. Your outer proxy (Caddy, Traefik, an existing nginx, …) just forwards HTTP to this sidecar and handles TLS as before.
Why a sidecar rather than your existing host/edge webserver? Because of the file-access rule above. The app container can already read the media (it runs as its own user). A different webserver — for example a host nginx running as www-data, or an edge proxy on another machine — usually cannot read your media (especially with read-only NFS mounts and restrictive ownership), so its offload attempts fail with 403/404 even though the app authorized them. A sidecar container avoids this entirely by mounting the same volume and running as the same user as the app.
Ready-to-use samples are provided: docker-compose.sample.yaml (app + nginx sidecar + valkey) and nginx.sample.conf (the matching nginx config). Set download_method: xaccel in config.yaml, adjust the media path and port, and point your TLS-terminating reverse proxy at the sidecar.
The remaining subsections describe the underlying mechanisms if you prefer to use an existing nginx/Apache that does have media access instead of a sidecar.
nginx (download_method: xaccel)
The app emits an X-Accel-Redirect header pointing into an internal location that nginx maps to your media root. See nginx.sample.conf for a ready-to-use example. The key points:
Apache / Lighttpd (download_method: xsendfile)
Requires mod_xsendfile. The app emits an X-Sendfile header with the absolute file path, which Apache serves directly:
XSendFile On
# Scope strictly to the media root so only intended files can be served.
XSendFilePath /media/data
Notes
- The offloading webserver must see the same media files at the configured path. With the sidecar above, this is handled by mounting the same volume into the nginx container. If you instead reuse an existing host/edge webserver, it must have read access to the media itself — a separate user like
www-dataoften does not, especially with read-only NFS mounts and restrictive ownership, which is exactly why the sidecar is recommended. Read-only access is sufficient. - Symlinks are resolved. The app hands the webserver the symlink-resolved (real) path. If
media_rootis itself a symlink (or sits under one), point nginx'saliasand Apache'sXSendFilePathat the real target directory, not the symlink. - Security: keep the nginx location
internal, and scopeXSendFilePathto exactly the media root. Authorization stays entirely in the app — the webserver only serves paths the app explicitly hands it. - Folder downloads (generated M3U8 playlists) are always served directly by the app, since they are tiny.
- With offloading enabled, uWSGI's
harakiri/keepalive settings no longer affect downloads.
License
This project is licensed under the terms of the GPL-3.0-only license, but contains elements under different licenses.
The project is REUSE compliant, so licensing and copyright information is available for every single file.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file home_stream-1.7.0.tar.gz.
File metadata
- Download URL: home_stream-1.7.0.tar.gz
- Upload date:
- Size: 38.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb15b5604e0d612f0cb003c28e293116d191375d399a936fa0513d562b5815ad
|
|
| MD5 |
62481979dfd11f63101bf9fa950a6746
|
|
| BLAKE2b-256 |
ea23774dfaa6af8a279b87eeae26e706466f39b52ed844db2853f793e6a2126f
|
Provenance
The following attestation bundles were made for home_stream-1.7.0.tar.gz:
Publisher:
publish.yaml on mxmehl/home-stream
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
home_stream-1.7.0.tar.gz -
Subject digest:
fb15b5604e0d612f0cb003c28e293116d191375d399a936fa0513d562b5815ad - Sigstore transparency entry: 1953413369
- Sigstore integration time:
-
Permalink:
mxmehl/home-stream@fef994a7730ee25ea85de209e09515cd1e2d74e7 -
Branch / Tag:
refs/tags/v1.7.0 - Owner: https://github.com/mxmehl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@fef994a7730ee25ea85de209e09515cd1e2d74e7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file home_stream-1.7.0-py3-none-any.whl.
File metadata
- Download URL: home_stream-1.7.0-py3-none-any.whl
- Upload date:
- Size: 42.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09107f7bee1e1d1840a0f1b18840ae86fc5bb30eb2b1fa04d125a76e2c40167f
|
|
| MD5 |
94646a246a68b4cb0f117120e189dbd4
|
|
| BLAKE2b-256 |
ee640305cc9cb0b534b67e0705d5ccc47a8dc9ca4fd6dda9561d83c35ecb51e8
|
Provenance
The following attestation bundles were made for home_stream-1.7.0-py3-none-any.whl:
Publisher:
publish.yaml on mxmehl/home-stream
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
home_stream-1.7.0-py3-none-any.whl -
Subject digest:
09107f7bee1e1d1840a0f1b18840ae86fc5bb30eb2b1fa04d125a76e2c40167f - Sigstore transparency entry: 1953413789
- Sigstore integration time:
-
Permalink:
mxmehl/home-stream@fef994a7730ee25ea85de209e09515cd1e2d74e7 -
Branch / Tag:
refs/tags/v1.7.0 - Owner: https://github.com/mxmehl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@fef994a7730ee25ea85de209e09515cd1e2d74e7 -
Trigger Event:
release
-
Statement type: