> ## Documentation Index
> Fetch the complete documentation index at: https://www.towbar.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Audiobookshelf

> Audiobookshelf organizes and streams audiobooks and podcasts.

export const UseCaseNavigation = () => {
  useLayoutEffect(() => {
    const storageKey = "towbar-use-case-open-groups";
    const groupSelector = "#sidebar li[data-title] > button[aria-expanded]";
    let restoring = false;
    let frame = 0;
    function storedGroups() {
      try {
        return new Set(JSON.parse(sessionStorage.getItem(storageKey) || "[]"));
      } catch {
        return new Set();
      }
    }
    function save(groups) {
      try {
        sessionStorage.setItem(storageKey, JSON.stringify([...groups]));
      } catch {}
    }
    function groupName(button) {
      return button.parentElement?.dataset.title;
    }
    function rememberOpenGroups() {
      const groups = new Set();
      for (const button of document.querySelectorAll(groupSelector)) {
        if (button.getAttribute("aria-expanded") === "true") {
          const name = groupName(button);
          if (name) groups.add(name);
        }
      }
      save(groups);
    }
    function restoreOpenGroups() {
      frame = 0;
      const groups = storedGroups();
      if (!groups.size) return;
      restoring = true;
      for (const button of document.querySelectorAll(groupSelector)) {
        if (groups.has(groupName(button)) && button.getAttribute("aria-expanded") === "false") {
          button.click();
        }
      }
      restoring = false;
    }
    function scheduleRestore() {
      if (!frame) frame = requestAnimationFrame(restoreOpenGroups);
    }
    function onClick(event) {
      if (restoring || !(event.target instanceof Element)) return;
      const button = event.target.closest(groupSelector);
      if (button) {
        const name = groupName(button);
        if (!name) return;
        const groups = storedGroups();
        if (button.getAttribute("aria-expanded") === "true") groups.delete(name); else groups.add(name);
        save(groups);
        return;
      }
      if (event.target.closest('a[href^="/docs/use-cases/"]')) {
        rememberOpenGroups();
      }
    }
    const observer = new MutationObserver(scheduleRestore);
    document.addEventListener("click", onClick, true);
    observer.observe(document.getElementById("sidebar") || document.body, {
      childList: true,
      subtree: true
    });
    scheduleRestore();
    return () => {
      document.removeEventListener("click", onClick, true);
      observer.disconnect();
      cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<UseCaseNavigation />

<img className="towbar-doc-brand-logo" src="https://mintcdn.com/avgeek/EGkEWLTQnOwecqNx/assets/use-case-logos/audiobookshelf.svg?fit=max&auto=format&n=EGkEWLTQnOwecqNx&q=85&s=722aaf7f9c342300cc2817ca17cacda9" alt="Audiobookshelf logo" aria-hidden="true" width="24" height="24" data-path="assets/use-case-logos/audiobookshelf.svg" />

**Upstream source:** [Current installation guide](https://audiobookshelf.org/docs/documentation/install/docker/).

Audiobookshelf keeps its SQLite database in `/config`, artwork and backups in `/metadata`, and media in the other volumes. No separate SQL Datastore is needed. Keep `/config` on local storage rather than a network filesystem.

## Towbar manifest

```yaml title=".towbar/services/audiobookshelf.service.yml" theme={"system"}
id: audiobookshelf
name: Audiobookshelf
buildServer: null
deployment:
  type: image
  image: ghcr.io/advplyr/audiobookshelf@sha256:3528a93b6442ffe54bd46771bbbab7c97084e1101071586d9dc2254f30bb4358
container:
  port: 80
  volumes:
    - name: config
      mountPath: /config
      initialData: image
    - name: metadata
      mountPath: /metadata
      initialData: image
    - name: audiobooks
      mountPath: /audiobooks
      initialData: image
    - name: podcasts
      mountPath: /podcasts
      initialData: image
environments:
  production:
    server: 192.0.2.10
rollout:
  type: recreate
  maintenanceMode: true
  reason: Persistent application data uses a single writer
domains:
  primary: audiobookshelf.example.com
tls:
  mode: direct
```

## Configure

1. Prepare the example server, connect the repository, and map `production` to the branch containing these manifests. Replace the example server IP and domain. Commit the manifests, then sync the repository and inspect the resolved configuration.
2. Keep the Service's named volumes attached across deployments. Towbar's Datastore backup policy does not back up Service volumes, so include them in your own recovery plan.
3. Deploy the Service, then perform the checks below before enabling auto-deploy.
4. Transfer a test audiobook to the server and use `docker cp` to place it under `/audiobooks` in the running container. The named volume retains the file when the container is replaced.

## Verify

Create a library for `/audiobooks`, scan and play the test file, then verify the library and listening progress survive redeployment.

For field constraints, see [Service manifest](/docs/services/manifest).
