No DRM · No subscriptions · 100% Free (and always will be)

Your library,
your rules.

AudioVault is a free Android audiobook player that streams from your own server or directly from your phone. You own your books — forever.

No account required
No cloud lock-in
Family sharing via server
Works offline

Overview

Android audiobook player
with two modes

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.

📱

Local Storage

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.

🏠

Home Server

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

Getting started
on your phone

No server, no terminal. Just files.

1

Create a folder on your phone

Using your file manager, create a dedicated folder for your audiobooks — for example Audiobooks in your internal storage or SD card.

2

Organize your books

Each book must be in its own subfolder. The folder name becomes the book title in the app. Recommended format:

📁 Audiobooks/
  📁 Terry Pratchett - Discworld 01 - The Colour of Magic/
    🎵 chapter01.mp3
    🎵 chapter02.mp3
    🖼 cover.jpg
  📁 Frank Herbert - Dune/
    🎵 dune.m4b
    🖼 cover.png

⚠ Renaming a folder will reset your listening progress for that book.

3

Select the folder in the app

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.

Supported formats

mp3 m4b m4a flac ogg wav

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

Running your
own server

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.

Run the installer script

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.

terminal — download and inspect first (recommended)
curl -s https://raw.githubusercontent.com/dejnastosic986/audiobook-player/main/install.sh -o install.sh
nano install.sh
terminal — run when ready
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.

1

Install Raspberry Pi OS

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:

  • Enable SSH — Use password authentication
  • Set a username and password — remember these, you will need them
  • Configure your Wi-Fi network name and password
  • Set the hostname to something memorable, e.g. audioserver

Insert the SD card into your Pi and power it on. Wait about 60 seconds for it to boot.

2

Connect to your Pi via SSH

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:

terminal — replace "pi" with your username if you chose a different one
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:

terminal
sudo apt update && sudo apt upgrade -y

This may take a few minutes. Press Y and Enter if prompted.

3

Install required packages

terminal
sudo apt install -y nginx python3 python3-pip ffmpeg inotify-tools
pip3 install flask --break-system-packages
4

Mount your external drive automatically

Connect your USB hard drive or SSD to the Pi. First, find its identifier:

terminal
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):

terminal
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:

terminal
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:

/etc/fstab
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:

terminal
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

5

Create the config file

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:

terminal — open file for editing
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:

terminal
sudo nano /srv/audiobook-data/config.env

Paste the following content:

/srv/audiobook-data/config.env
# 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:

terminal
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.

6

Organize your audiobooks

Copy your books to the external drive. One subfolder per book, cover image inside:

📁 /mnt/audiodrive/audiobooks/
  📁 Terry Pratchett - Discworld 01 - The Colour of Magic/
    🎵 chapter01.mp3
    🎵 chapter02.mp3
    🖼 cover.jpg
  📁 Frank Herbert - Dune/
    🎵 dune.m4b
7

Create the Flask API

First, install the required Python packages:

terminal
pip3 install flask python-dotenv --break-system-packages

Then create the file:

terminal
sudo nano /srv/audiobook-data/audiobook_api.py

Paste the following content:

audiobook_api.py
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)
8

Configure nginx

terminal
sudo nano /etc/nginx/sites-available/audiobooks

Paste the following content:

/etc/nginx/sites-available/audiobooks
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.

terminal
sudo ln -s /etc/nginx/sites-available/audiobooks \
          /etc/nginx/sites-enabled/audiobooks
sudo nginx -t && sudo systemctl reload nginx
9

Fix file permissions for 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:

terminal
sudo visudo -f /etc/sudoers.d/www-data-chown

Paste the following content:

/etc/sudoers.d/www-data-chown
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:

terminal
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.

10

Create the library scanner

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.

terminal
sudo nano /srv/audiobook-api/regen_library.sh

Paste the following content:

regen_library.sh
#!/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."
terminal
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.

11

Create the folder watcher script

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.

terminal
sudo nano /srv/audiobook-api/audiobook-watch.sh

Paste the following content:

audiobook-watch.sh
#!/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
terminal
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.

12

Set up systemd services

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
terminal — enable and start both services
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.

terminal — verify both services are running
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
13

Connect the app

Find your Raspberry Pi's local IP address:

terminal
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

Why is there
no login.

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.

Why our free audiobook player is NOT piracy software

A public pirate audiobook repository needs to be discoverable, scalable, and easy to use for strangers. This audiobook app is none of those things:

🔒

Not discoverable

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.

📦

Not scalable

A Raspberry Pi serving files over a home connection cannot handle hundreds of concurrent users. The architecture is explicitly designed for single-family use.

🛠

Not plug-and-play

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.

🚫

No search, no catalog

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