No DRM · No subscriptions · 100% Free (and always will be)
AudioVault is a free Android audiobook player that streams from your own server or directly from your phone. You own your books — forever.
Overview
AudioVault works in two independent modes: Local Storage & Home Server. Choose the one that fits your needs the best — or switch between them at any time, with only a few taps.
Copy your audiobooks to a folder on your Android device, point the app at that folder, and — that's it! No server, no setup, no tech knowledge needed.
Run a lightweight server at home (a Raspberry Pi works perfectly). Stream your entire library over Wi-Fi or mobile data, with automatic sync across all your devices.
Local Storage Mode
No server, no terminal. Just files.
Using your file manager, create a dedicated folder for your audiobooks —
for example Audiobooks in your internal storage or SD card.
Each book must be in its own subfolder. The folder name becomes the book title in the app. Recommended format:
⚠ Renaming a folder will reset your listening progress for that book.
Open the app → tap the settings icon → select Local storage → tap Choose books folder → navigate to your audiobooks folder and confirm. The app scans it once and caches the library for instant startup next time.
Tracks inside a folder are played in alphabetical order.
Name your files so they sort correctly — e.g. chapter01.mp3,
chapter02.mp3, and so on.
💡 Keep only audio files and one cover image in each book folder. Other files won't cause problems, but keeping things tidy is recommended.
Server Mode
This guide uses a Raspberry Pi (any model with Wi-Fi), but the same steps work on any Linux machine. You'll need basic comfort with a terminal.
Optional — automated setup
If you prefer not to go through the steps manually, an interactive installer script is available. It asks you a few questions and handles everything automatically — packages, drive mounting, config files, nginx, and services.
The script is fully open source and available on GitHub. You are encouraged to read it before running it — it contains comments explaining every action it takes.
curl -s https://raw.githubusercontent.com/dejnastosic986/audiobook-player/main/install.sh -o install.sh
nano install.sh
bash install.sh
Or run it directly in one command:
bash <(curl -s https://raw.githubusercontent.com/dejnastosic986/audiobook-player/main/install.sh)
💡 The installer does the same things described in the step-by-step guide below — nothing more, nothing hidden. If something goes wrong, the manual steps are there so you understand exactly what the script was trying to do.
Prefer to do it yourself? Follow the steps below.
⚠ Do not store your audiobooks on the SD card. SD cards are slow for large media files and have a limited number of write cycles — constant access will shorten their lifespan significantly. Use an external USB hard drive or SSD instead. The steps below assume this setup.
Use Raspberry Pi Imager to flash Raspberry Pi OS Lite (64-bit) to your SD card. Before writing, click the ⚙ gear icon (or press Ctrl+Shift+X) to open advanced settings and configure the following:
audioserverInsert the SD card into your Pi and power it on. Wait about 60 seconds for it to boot.
Open a terminal on your computer (on Windows use PuTTY or the built-in Windows Terminal, on Mac and Linux use the Terminal app) and connect to your Pi:
ssh [email protected]
If that doesn't work, find your Pi's IP address from your router's admin page and connect directly:
ssh [email protected]
Type yes when asked about the fingerprint, then enter your password.
You won't see any characters while typing — that's normal.
Once connected, update the system:
sudo apt update && sudo apt upgrade -y
This may take a few minutes. Press Y and Enter if prompted.
sudo apt install -y nginx python3 python3-pip ffmpeg inotify-tools
pip3 install flask --break-system-packages
Connect your USB hard drive or SSD to the Pi. First, find its identifier:
lsblk
Look for your drive in the output — it will typically appear as
sda with a partition like sda1.
Then find its UUID (a permanent identifier that survives reboots):
sudo blkid /dev/sda1
You will see output like:
/dev/sda1: UUID="a1b2c3d4-..." TYPE="ext4"
Create the mount point and add it to /etc/fstab
so the drive mounts automatically on every boot:
sudo mkdir -p /mnt/audiodrive
sudo nano /etc/fstab
Add this line at the end of the file.
Replace a1b2c3d4-... with the exact UUID you got
from the blkid command above — copy and paste it
to avoid typos:
UUID=a1b2c3d4-... /mnt/audiodrive ext4 defaults,nofail 0 2
The nofail option is important — it prevents the Pi from
hanging on boot if the drive is not connected.
Apply the change without rebooting:
sudo mount -a
💡 If your drive is formatted as NTFS (common for drives
previously used on Windows), replace ext4 with ntfs-3g
and install the driver first: sudo apt install ntfs-3g
All paths are stored in a single config file so you never have to edit multiple scripts when your folder structure changes.
Throughout this guide, whenever we say "save the following as [filename]",
use this pattern — open the file in the nano text editor,
paste the content, then save and exit:
sudo nano /path/to/filename
Paste the content, then press Ctrl+O to save, Enter to confirm the filename, and Ctrl+X to exit.
Create /srv/audiobook-data/config.env:
sudo nano /srv/audiobook-data/config.env
Paste the following content:
# Path to the folder containing your audiobook subfolders.
# Change this if your drive is mounted elsewhere or your folder has a different name.
BOOKS_DIR=/mnt/audiodrive/audiobooks
# Path where library.json and progress data will be stored.
# This stays on the SD card — only the books live on the external drive.
DATA_DIR=/srv/audiobook-data
Save and exit nano (Ctrl+O → Enter → Ctrl+X), then create the directories:
sudo mkdir -p /srv/audiobook-data
sudo chown -R $USER:$USER /srv/audiobook-data
mkdir -p /mnt/audiodrive/audiobooks
💡 $USER is a variable that automatically expands to your
current username — the same one you set in Raspberry Pi Imager.
You don't need to replace it manually.
Copy your books to the external drive. One subfolder per book, cover image inside:
First, install the required Python packages:
pip3 install flask python-dotenv --break-system-packages
Then create the file:
sudo nano /srv/audiobook-data/audiobook_api.py
Paste the following content:
from flask import Flask, jsonify, request
import json, os
from dotenv import load_dotenv
load_dotenv("/srv/audiobook-data/config.env")
DATA_DIR = os.getenv("DATA_DIR", "/srv/audiobook-data")
app = Flask(__name__)
@app.route("/api/progress/<profile>/<book_id>", methods=["GET"])
def get_progress(profile, book_id):
path = f"{DATA_DIR}/progress/{profile}/{book_id}.json"
if os.path.exists(path):
return jsonify(json.load(open(path)))
return jsonify({"trackIndex": 0, "positionMs": 0, "updatedAt": 0})
@app.route("/api/progress/<profile>/<book_id>", methods=["POST"])
def post_progress(profile, book_id):
data = request.get_json()
folder = f"{DATA_DIR}/progress/{profile}"
os.makedirs(folder, exist_ok=True)
with open(f"{folder}/{book_id}.json", "w") as f:
json.dump(data, f)
return jsonify({"ok": True})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
sudo nano /etc/nginx/sites-available/audiobooks
Paste the following content:
server {
listen 8081;
location /library.json {
alias /srv/audiobook-data/library.json;
}
location / {
root /mnt/audiodrive/audiobooks;
autoindex off;
}
location /api/ {
proxy_pass http://127.0.0.1:5000;
}
}
⚠ nginx cannot read config.env.
The paths above are hardcoded directly in this file. If you ever move
your books to a different drive or folder, you must update
alias and root here manually —
changing config.env alone will not be enough.
sudo ln -s /etc/nginx/sites-available/audiobooks \
/etc/nginx/sites-enabled/audiobooks
sudo nginx -t && sudo systemctl reload nginx
nginx serves files as the www-data user. The library scanner
also needs to set www-data as the owner of
library.json after every regeneration — otherwise nginx
returns a 403 error and the app cannot load the book list.
Grant www-data the ability to run only the exact
chown and chmod commands it needs — nothing else:
sudo visudo -f /etc/sudoers.d/www-data-chown
Paste the following content:
www-data ALL=(root) NOPASSWD: /usr/bin/chown www-data:www-data /srv/audiobook-data/library.json
www-data ALL=(root) NOPASSWD: /usr/bin/chown -R www-data:www-data /mnt/audiodrive/audiobooks
www-data ALL=(root) NOPASSWD: /usr/bin/chmod 644 /srv/audiobook-data/library.json
www-data ALL=(root) NOPASSWD: /usr/bin/chmod 664 /srv/audiobook-data/library.json
www-data ALL=(root) NOPASSWD: /usr/bin/chmod -R 755 /mnt/audiodrive/audiobooks
Verify the syntax before saving:
sudo visudo -c -f /etc/sudoers.d/www-data-chown
You should see parsed OK. If not, re-open the file and check for typos.
💡 visudo uses nano by default —
Ctrl+O to save, Ctrl+X to exit.
The NOPASSWD rule is intentionally narrow: it only covers
the exact commands listed. www-data cannot run anything
else with elevated privileges.
⚠ The paths in this file must match your actual setup.
If you mounted your drive at a different location (e.g. /mnt/wd
instead of /mnt/audiodrive), update the paths here accordingly.
The paths must match exactly — sudoers does not support wildcards
for directory arguments.
This script scans your audiobook folders and generates
library.json — the file the app reads to show
your book list. It uses a duration cache so re-runs after
adding one book are fast.
sudo nano /srv/audiobook-api/regen_library.sh
Paste the following content:
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="/srv/audiobook-api/config.env"
[ -f "$CONFIG_FILE" ] || { echo "ERROR: config.env not found"; exit 1; }
source "$CONFIG_FILE"
[ -n "${AUDIOBOOKS_DIR:-}" ] || { echo "ERROR: AUDIOBOOKS_DIR not set"; exit 1; }
DATA_DIR="/srv/audiobook-data"
OUT="$DATA_DIR/library.json"
mkdir -p "$DATA_DIR"
umask 0002
echo "[regen] Scanning: $AUDIOBOOKS_DIR"
AUDIOBOOKS_DIR="$AUDIOBOOKS_DIR" OUT_FILE="$OUT" python3 - <<'PY'
import os, sys, json, subprocess, tempfile, shutil
AUDIOBOOKS_DIR = os.environ["AUDIOBOOKS_DIR"]
OUT_FILE = os.environ["OUT_FILE"]
AUDIO_EXT = (".mp3", ".m4b", ".m4a", ".wav", ".ogg", ".flac")
IMAGE_EXT = (".jpg", ".jpeg", ".png")
PREFERRED_COVERS = ("cover.jpg", "cover.jpeg", "cover.png", "folder.jpg", "folder.png")
if not shutil.which("ffprobe"):
print("ERROR: ffprobe not found. Install with: sudo apt install ffmpeg")
sys.exit(1)
CACHE_FILE = os.path.join(os.path.dirname(OUT_FILE), "duration_cache.json")
def load_cache():
try:
with open(CACHE_FILE, "r", encoding="utf-8") as f: return json.load(f)
except Exception: return {}
def save_cache(cache):
try:
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(CACHE_FILE))
with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(cache, f)
os.replace(tmp, CACHE_FILE)
except Exception as e: print(f"[warn] Could not save cache: {e}")
cache = load_cache()
def slugify(s):
s = s.lower().strip()
out = []
for ch in s:
if ch.isalnum(): out.append(ch)
elif ch in (" ", "-", "_"): out.append("-")
slug = "".join(out)
while "--" in slug: slug = slug.replace("--", "-")
return slug.strip("-")
def get_duration_ms(path):
try:
mtime = str(os.path.getmtime(path))
key = f"{path}:{mtime}"
if key in cache: return cache[key]
except OSError: pass
try:
r = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=15)
txt = (r.stdout or "").strip()
if not txt or txt.lower() == "n/a": return 0
ms = int(float(txt) * 1000)
try:
mtime = str(os.path.getmtime(path))
cache[f"{path}:{mtime}"] = ms
except OSError: pass
return ms
except Exception: return 0
def find_cover(folder_name):
full = os.path.join(AUDIOBOOKS_DIR, folder_name)
for name in PREFERRED_COVERS:
if os.path.isfile(os.path.join(full, name)):
return f"{folder_name}/{name}"
try:
imgs = sorted(n for n in os.listdir(full) if n.lower().endswith(IMAGE_EXT))
if imgs: return f"{folder_name}/{imgs[0]}"
except OSError: pass
return None
books = []
for folder_name in sorted(os.listdir(AUDIOBOOKS_DIR)):
full = os.path.join(AUDIOBOOKS_DIR, folder_name)
if not os.path.isdir(full) or folder_name.startswith("."): continue
audio_files = sorted(n for n in os.listdir(full) if n.lower().endswith(AUDIO_EXT))
if not audio_files:
print(f" [skip] No audio files in: {folder_name}"); continue
print(f" [book] {folder_name} ({len(audio_files)} track(s))")
tracks = []
for filename in audio_files:
ms = get_duration_ms(os.path.join(full, filename))
tracks.append({"file": filename, "durationMs": ms})
books.append({"id": slugify(folder_name), "title": folder_name,
"coverUrl": find_cover(folder_name), "tracks": tracks})
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(OUT_FILE))
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump({"books": books}, f, ensure_ascii=False, indent=2)
os.replace(tmp, OUT_FILE)
except Exception as e:
try: os.unlink(tmp)
except OSError: pass
print(f"ERROR: {e}"); sys.exit(1)
save_cache(cache)
print(f"\n✅ Done. Wrote {len(books)} book(s) to {OUT_FILE}")
PY
sudo chown www-data:www-data "$OUT"
sudo chmod 664 "$OUT"
sudo chmod 775 "$DATA_DIR"
echo "[regen] Done."
chmod +x /srv/audiobook-api/regen_library.sh
sudo -u www-data /srv/audiobook-api/regen_library.sh
⚠ The first run can take a while for large libraries — it reads every audio file to measure its duration. Subsequent runs are fast because results are cached. Let it finish before starting the services.
This script watches for changes in your books folder and triggers a library rescan — but only after disk activity has settled for 25 seconds. This prevents regen from running while a book upload is still in progress.
sudo nano /srv/audiobook-api/audiobook-watch.sh
Paste the following content:
#!/bin/bash
source /srv/audiobook-api/config.env
echo "[watch] Watching $AUDIOBOOKS_DIR for changes..."
while true; do
inotifywait -r -e modify,create,delete,moved_to,moved_from \
"$AUDIOBOOKS_DIR" 2>/dev/null
echo "[watch] Change detected. Waiting for activity to settle..."
while inotifywait -r -e modify,create,delete,moved_to,moved_from \
-t 25 "$AUDIOBOOKS_DIR" 2>/dev/null; do
echo "[watch] Still active, resetting timer..."
done
echo "[watch] Regenerating library..."
/srv/audiobook-api/regen_library.sh
echo "[watch] Done."
done
chmod +x /srv/audiobook-api/audiobook-watch.sh
💡 The 25-second timer resets every time new activity is detected. If you are copying a large book that takes several minutes, regen will only run once — 25 seconds after the last file write completes.
Two services — one for the Flask API, one that runs the watcher script.
Both read the books path from config.env.
/etc/systemd/system/audiobook-api.service
[Unit]
Description=Audiobook API
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/audiobook-api
ExecStart=/srv/audiobook-api/venv/bin/gunicorn \
--workers 2 \
--bind 127.0.0.1:5000 \
--access-logfile - \
--error-logfile - \
audiobook_api:app
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
💡 The API runs as www-data so it can write
progress.json with the correct ownership. This is why
we set up the sudoers rule in step 9.
/etc/systemd/system/audiobook-watch.service
[Unit]
Description=Audiobook Library Watcher
After=network.target
[Service]
User=www-data
Group=www-data
ExecStart=/bin/bash /srv/audiobook-api/audiobook-watch.sh
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable audiobook-api audiobook-watch
sudo systemctl start audiobook-api audiobook-watch
💡 Make sure /srv/audiobook-api/audiobook-watch.sh uses
the variable AUDIOBOOKS_DIR (from config.env),
not BOOKS_DIR — the two config files use different variable names.
sudo systemctl status audiobook-api
sudo systemctl status audiobook-watch
Both services should show Active: active (running) in green.
If either shows failed, check the logs for details:
sudo journalctl -u audiobook-api -n 30
sudo journalctl -u audiobook-watch -n 30
Find your Raspberry Pi's local IP address:
hostname -I
Before opening the app, verify the server is responding correctly. Open a browser on any device connected to the same Wi-Fi network and visit:
http://192.168.x.x:8081/library.json
You should see a JSON response with your book list. If you see an error,
double-check that both services are running (systemctl status audiobook-api)
and that nginx is active (sudo systemctl status nginx).
Once the browser test works, open the app, go to Settings → Server and enter:
http://192.168.x.x:8081
Replace 192.168.x.x with your Pi's actual IP address.
💡 For remote access outside your home network, consider setting up a VPN (WireGuard is lightweight and works well on Raspberry Pi).
Design Philosophy
AudioVault has no user accounts, no passwords, and no authentication. Rest assured, this is NOT an oversight — it is a deliberate design decision, stemming from a specific philosophy.
This Android audiobook app was built for one reason: the frustration of paying for an audiobook and having it taken away. Audible removes titles. Services shut down. Terms change — and items in your library disappear. Our standpoint is: if you bought it, you should own it — permanently, unconditionally, and without anxiety over losing your precious books.
A public pirate audiobook repository needs to be discoverable, scalable, and easy to use for strangers. This audiobook app is none of those things:
The server runs on a private IP address inside your home network. There is no domain, no DNS, no way to find it from the outside unless you deliberately expose it.
A Raspberry Pi serving files over a home connection cannot handle hundreds of concurrent users. The architecture is explicitly designed for single-family use.
Setting up the server requires SSH access, terminal commands, and manual file management. This is not a service anyone stumbles into — it requires deliberate technical effort.
There is no public-facing interface, no browse page, no way to discover what books are on the server without already having the app and knowing the address.
The conscious choice to omit authentication is what keeps this app honest about what it is — a personal tool for people who believe in owning their audiobooks, not a platform with pretensions of scaling. If you want to share audiobooks with the world, this app is the wrong choice. If you want to share them with your family — without asking anyone's permission or jumping through hoops — and i