- Go 94.9%
- Rust 3.1%
- Nix 0.9%
- Shell 0.8%
- Dockerfile 0.2%
- Other 0.1%
mac5 runs Piper CPU-only (CoreML can't run VITS dynamic output length, decision 003) and the script used one daemon on 15 cores -> a 3x smaller book took 3x longer than fedya's GPU. WORKERS=N (default 4) runs N thread-capped daemons; benchmarked 2x ceiling on M5 Pro (5 P-cores). Also fixed a pipefail+ls-empty-glob abort. docs/decisions/003 records the no-GPU verdict. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .github/workflows | ||
| cmd | ||
| docs/decisions | ||
| internal | ||
| sopds-tts-rs | ||
| zipdupes-rs | ||
| .dockerignore | ||
| .gitignore | ||
| .golangci.yml | ||
| .goreleaser.yaml | ||
| CLAUDE.md | ||
| config.yaml.example | ||
| config_test.yaml | ||
| docker-compose.example.yml | ||
| Dockerfile | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| init.sql | ||
| LICENSE | ||
| NOTICE.md | ||
| PROGRESS.md | ||
| README.md | ||
| Taskfile.docker.example.yml | ||
| Taskfile.yml | ||
SOPDS-Go
Go implementation of Simple OPDS Catalog Server — manage and serve large e-book collections via OPDS feeds and a modern web UI. Designed for self-hosted home libraries: scans nested ZIP archives of FB2 books, extracts metadata, generates EPUB/MOBI on the fly, and even produces audiobooks via local TTS.
A modernized Go rewrite of the original Simple OPDS Python project (v0.23) by Dmitry V. Shelepnev (Дмитрий Шелепнёв, © 2014; contact: admin@sopds.ru). The upstream homepage (www.sopds.ru) and any related public source repositories appear to be offline as of 2026. PostgreSQL persistence (vs. MySQL upstream), parallel scanning, GPU-accelerated TTS, and a redesigned responsive UI.
Features
Catalog & Browsing
- OPDS 1.2 catalog server, compatible with Moon+ Reader, FBReader, Aldiko, Calibre
- Modern Web UI with responsive design, Font Awesome icons, full-text search
- Hierarchical navigation — letter-based drill-down (1→2→3 chars) for large author/series lists
- Browse by language, by genre, by series, by new arrivals (last 7 days)
- Personal bookshelf — save books to a reading list
- Duplicate detection + viewer — automatic marking + side-by-side comparison
Library Management
- Parallel scanning with configurable workers + progress tracking
- FB2 metadata extraction — title, authors, genres, series, annotations, covers
- Nested ZIP archive scanning — common format for book corpora
- PostgreSQL persistence with embedded migrations
- Scheduled rescans in cron format
- MySQL migration tool — drop-in import from the original Python SOPDS database
Conversion & TTS
- FB2 → EPUB — pure Go, no external dependencies
- FB2 → MOBI — via Calibre (
ebook-convert) - FB2 → MP3 audiobook — via Piper TTS, with optional CUDA GPU acceleration through the Rust subproject
- Multi-speaker / multi-language voices — Ukrainian, Russian, English, more
Operations
- Basic HTTP authentication support
- Static binaries for Linux + macOS, amd64/arm64 (see Releases)
- Docker image at
ghcr.io/dimgord/sopds-go - Nix flake —
nix run github:dimgord/sopds-go
Quick Start
# Build
go build -o sopds ./cmd/sopds
# Initialize database (as postgres superuser)
psql -U postgres -f init.sql
# Create config file
./sopds init
# Edit config.yaml with your settings
vim config.yaml
# Run migrations
./sopds migrate
# Start server
./sopds start
Access the interfaces:
- Web UI: http://localhost:8081/web/
- OPDS: http://localhost:8081/opds/
Requirements
- Go 1.24+ for building from source
- PostgreSQL 16+ for persistence (PostgreSQL 18 recommended; that's what
brew installwill pull as a dependency) - Calibre (
ebook-convert) — optional, for MOBI conversion - Espeak-ng + Piper voice models — optional, for TTS / audiobooks
- NVIDIA GPU + CUDA 12 — optional, for accelerated TTS via
sopds-tts-rs
Installation
Pre-built binaries (recommended)
Grab the latest release for your platform from GitHub Releases — Linux/macOS × x86_64/arm64.
Archive naming convention: sopds-go_<version>_<os>_<arch>.tar.gz where
<os> is linux or macos and <arch> is x86_64 or arm64.
# Linux x86_64 (replace v1.3.2 with the latest tag from /releases):
VERSION=v1.3.2
curl -LO https://github.com/dimgord/sopds-go/releases/download/$VERSION/sopds-go_${VERSION#v}_linux_x86_64.tar.gz
tar xzf sopds-go_${VERSION#v}_linux_x86_64.tar.gz
sudo install sopds /usr/local/bin/
sudo install zipdupes /usr/local/bin/ # optional: FB2 dedup tool
On NixOS: /usr/local/bin/ doesn't exist (FHS-violating); use the Nix flake below instead, or drop binaries into ~/.local/bin/ (and make sure that's in your $PATH).
Docker
Single container (you already have PostgreSQL running somewhere):
docker run -d \
--name sopds \
-p 8081:8081 \
-v /path/to/your/books:/library:ro \
-v /path/to/config.yaml:/etc/sopds/config.yaml:ro \
ghcr.io/dimgord/sopds-go:latest
Compose stack (sopds-go + sibling PostgreSQL) — copy the template:
mkdir -p ~/dockers/sopds && cd ~/dockers/sopds
curl -O https://raw.githubusercontent.com/dimgord/sopds-go/main/docker-compose.example.yml
curl -O https://raw.githubusercontent.com/dimgord/sopds-go/main/config.yaml.example
curl -O https://raw.githubusercontent.com/dimgord/sopds-go/main/Taskfile.docker.example.yml
mv docker-compose.example.yml docker-compose.yml
mv config.yaml.example config.yaml
mv Taskfile.docker.example.yml Taskfile.yml
# Edit config.yaml: set database.host=postgres, database.password=...
# Edit docker-compose.yml: replace /path/to/your/books with real path
docker compose up -d
docker compose logs -f sopds # watch it boot
# First-time setup (in this order):
docker exec -it sopds sopds migrate -c /etc/sopds/config.yaml # create tables
docker exec -it sopds sopds scan -c /etc/sopds/config.yaml # index books
If you copied Taskfile.docker.example.yml → Taskfile.yml, those last
two are task migrate + task scan. Important: in config.yaml,
library.root must point at the container path (/library per the
example volume mount in docker-compose.yml), not the host path.
The Taskfile.docker.example.yml (requires task,
brew install go-task or any pkg manager) bundles common ops: task up /
task logs / task psql / task scan / task backup / task update /
task nuke. Run task to list everything.
(init.sql is not needed in the docker-compose case — Postgres'
POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB env vars provision
the DB+user with full privileges. init.sql is only for standalone-PG
bootstrap as the postgres superuser.)
Access at http://<host>:8081/web/ (web UI) and http://<host>:8081/opds/ (OPDS feed).
Nix flake
# Run directly without installing
nix run github:dimgord/sopds-go -- start
# Add to a flake.nix
inputs.sopds-go.url = "github:dimgord/sopds-go";
From source
git clone https://github.com/dimgord/sopds-go.git
cd sopds-go
go build -o sopds ./cmd/sopds
Database Setup
# Create database and user (run as postgres superuser)
psql -U postgres -f init.sql
# Run migrations
./sopds migrate
Install Calibre (for MOBI conversion)
# Fedora
sudo dnf install calibre
# Ubuntu/Debian
sudo apt install calibre
Configuration
Create config.yaml (or run ./sopds init):
database:
host: localhost
port: 5432
name: sopds
user: sopds
password: sopds
sslmode: disable
library:
root: /path/to/your/books
formats:
- .fb2
- .epub
- .mobi
- .pdf
- .djvu
scan_zip: true
rescan_zip: false
server:
bind: 0.0.0.0
port: 8081
opds_prefix: /opds
web_prefix: /web
auth:
enabled: false
users:
- username: guest
password: guest
scanner:
workers: 4
schedule: "0 3 * * *" # Cron format: 3 AM daily
on_start: false
duplicates: normal # none, normal, strong, clear
site:
title: "SOPDS Library"
converters:
fb2toepub: "" # Not used (pure Go)
fb2tomobi: "ebook-convert" # Calibre path
temp_dir: /tmp
Commands
| Command | Description |
|---|---|
./sopds start |
Start the server |
./sopds stop |
Stop the server |
./sopds status |
Check server status |
./sopds scan |
Run manual library scan |
./sopds migrate |
Run database migrations |
./sopds init |
Create default config.yaml |
./sopds import-mysql |
Import from MySQL SOPDS database |
./sopds version |
Show version |
Database Backup & Restore
# Backup
pg_dump -U sopds -Fc sopds > sopds_backup.dump
# Restore
pg_restore -U sopds -d sopds -c sopds_backup.dump
# Full restore (drop and recreate)
dropdb -U sopds sopds && createdb -U sopds sopds && pg_restore -U sopds -d sopds sopds_backup.dump
MySQL Migration
Import data from existing MySQL SOPDS database:
./sopds import-mysql \
--mysql-host localhost \
--mysql-user sopds \
--mysql-password sopds \
--mysql-db sopds \
--clear # Optional: clear PostgreSQL tables first
API Endpoints
OPDS Catalog (Atom feeds)
| Endpoint | Description |
|---|---|
GET /opds/ |
Main menu |
GET /opds/catalogs |
Browse by directory |
GET /opds/catalogs/{id} |
Catalog contents |
GET /opds/authors |
Authors list (with letter navigation) |
GET /opds/authors/{id} |
Author's books |
GET /opds/titles |
Books by title |
GET /opds/genres |
Genre sections |
GET /opds/genres/{id} |
Genre books |
GET /opds/series |
Series list (with letter navigation) |
GET /opds/series/{id} |
Series books |
GET /opds/new |
New books (last 7 days) |
GET /opds/search?q={query} |
Search books |
GET /opds/book/{id}/download |
Download book |
GET /opds/book/{id}/cover |
Book cover image |
GET /opds/book/{id}/epub |
Convert FB2 to EPUB |
GET /opds/book/{id}/mobi |
Convert FB2 to MOBI |
Web Interface
| Endpoint | Description |
|---|---|
GET /web/ |
Home page with statistics |
GET /web/search?q={query} |
Search results |
GET /web/authors |
Authors (hierarchical letter navigation) |
GET /web/authors/{id} |
Author's books |
GET /web/genres |
Genre list |
GET /web/genres/{id} |
Genre books |
GET /web/series |
Series (hierarchical letter navigation) |
GET /web/series/{id} |
Series books |
GET /web/languages |
Languages list with book counts |
GET /web/languages/{lang} |
Books in language |
GET /web/new |
New books |
GET /web/catalogs |
Catalog folders |
GET /web/catalogs/{id} |
Catalog contents |
GET /web/duplicates/{id} |
View all duplicates of a book |
GET /web/bookshelf |
User's bookshelf |
POST /web/bookshelf/add/{id} |
Add book to bookshelf |
POST /web/bookshelf/remove/{id} |
Remove book from bookshelf |
Project Structure
sopds-go/
├── cmd/sopds/main.go # CLI entry point (cobra)
├── internal/
│ ├── config/ # YAML configuration loader
│ ├── converter/ # FB2 → EPUB (pure Go), FB2 → MOBI (Calibre)
│ ├── database/ # PostgreSQL: connection, models, queries, migrations
│ ├── scanner/ # Library walker, FB2 parser, ZIP-archive walker
│ ├── server/ # HTTP server (chi), OPDS handlers, Web UI handlers
│ ├── opds/ # Atom-feed generator
│ ├── tts/ # Piper TTS integration (Go path)
│ └── infrastructure/ # Persistence layer abstractions
├── sopds-tts-rs/ # Rust port of TTS — CUDA-accelerated (see below)
├── zipdupes-rs/ # Rust port of FB2-archive deduplicator
├── fb2converters/ # Drop-in directory for additional converter binaries
├── config.yaml.example # Example configuration
├── init.sql # Database initialization
├── Taskfile.yml # task-runner recipes (build / test / docker / etc.)
├── go.mod
└── README.md
Rust subprojects
Two CLI utilities have Rust ports for performance/GPU reasons. Both are CLI-compatible drop-in replacements; the Go originals remain in-tree as fallbacks.
sopds-tts-rs/ — CUDA-accelerated TTS
Drop-in for sopds-tts. Runs Piper ONNX models via ort (Rust ONNX Runtime binding) with the CUDA execution provider for dramatic speedup on systems with NVIDIA GPUs. Pascal-class GPUs (sm_61, e.g. GTX 1070) are explicitly supported via a pinned cuDNN 9.8 in the flake.
cd sopds-tts-rs
nix develop ./ # first time: ~15-30 min (ORT source build with CUDA EP)
cargo build --release
./target/release/sopds-tts-rs <model> <output> < text.txt
sopds-go picks it up automatically: it looks for a binary named sopds-tts next to the server executable (or on PATH) — no config key. Symlink or copy sopds-tts-rs (or a wrapper, below) to that name.
Daemon mode. With no output argument (sopds-tts-rs <model>) the binary loads the model once and then reads NDJSON {"text","output"} requests on stdin, emitting one WAV per line — ~20–90 ms/chunk vs ~340 ms when the model is reloaded per call. sopds-go uses this automatically (one resident process per model, falling back to a one-shot subprocess if the binary doesn't support daemon mode).
Convert a whole book from the CLI. fb2-to-wav.sh wraps the above into one command — it extracts the main body text, splits it into ~600-character sentence chunks (bigger chunks OOM the GPU), runs them across WORKERS parallel daemons with a live ETA, and joins the result:
./fb2-to-wav.sh book.fb2 ~/piper-models/ru_RU-irina-medium.onnx book.mp3
WORKERS=6 ./fb2-to-wav.sh … # more parallelism (default 4)
MAXCHARS=800 ./fb2-to-wav.sh … # bigger chunks (careful: >~700 chars can OOM an 8 GB GPU)
WORKERS defaults to 4 (each daemon capped to ~cores/WORKERS ORT threads). On Apple Silicon — where this model runs CPU-only (CoreML can't run Piper's VITS) — that's roughly a 2× win, bounded by the performance-core count. On a GPU box keep it low (VRAM per resident model).
The output format follows the extension (default .mp3): mp3 / m4b / m4a / opus / ogg are encoded at a speech-grade bitrate; .wav is copied losslessly but is hard-capped at 4 GB by the format, so a longer book auto-switches to .mp3. It self-bootstraps its tools (espeak-ng, ffmpeg, jq, xmllint, GNU sed/awk) via nix shell, so on a nix box nothing needs installing.
Deploy on a GPU host (systemd)
When the service runs as an unprivileged user with a minimal PATH and no nix (e.g. User=sopds, /opt/sopds/sopds):
- Build:
cd sopds-tts-rs && nix develop -c cargo build --release. - Copy the binary somewhere the service user can exec (its home is usually
0700):sudo install -m755 target/release/sopds-tts-rs /opt/sopds/sopds-tts-rs-bin. - Wrapper named
sopds-ttsnext to the server — it only puts espeak-ng onPATHand execs the binary (libonnxruntime+ cuDNN come from the binary's RPATH;libcudafrom/run/opengl-driver/libon NixOS, so noLD_LIBRARY_PATHis needed):#!/bin/sh export PATH="/nix/store/…-espeak-ng-…/bin:$PATH" exec /opt/sopds/sopds-tts-rs-bin "$@"sudo install -m755 wrapper /opt/sopds/sopds-tts. - Pin the runtime store paths against nix garbage collection:
nix develop sopds-tts-rs --profile ~/.local/state/sopds-tts-devshell -c true. - Restart the service. Verify with
nvidia-smi(the process holds GPU memory) and the server log (TTS: Starting N workers).
CUDA teardown note: on some ONNX Runtime + cuDNN combinations the process core-dumps (
corrupted double-linked list) when the session is dropped at exit — after the WAV is already written. The binary sidesteps this by callingexit(0)before the drop, so the exit status stays clean and sopds-go doesn't mistake a good chunk for a failure.
zipdupes-rs/ — Rust port of FB2-archive duplicate finder
Same CLI as Go zipdupes. Significantly faster on cold-cache scans of large book corpora (tested on a 1TB+ FB2 ZIP archive collection). Use whichever you prefer.
cd zipdupes-rs
cargo build --release
./target/release/zipdupes /path/to/archive/dir
Web UI
Modern responsive layout with Font Awesome icons, gradient header, library statistics on the home page, full-text + hierarchical navigation, download buttons for all supported formats, personal bookshelf, duplicates browser, and language filtering. Optimized for both desktop and mobile screens.
Related projects
- fbe-go — FictionBook editor. Same author, same FB2 ecosystem, complementary role: sopds-go serves books; fbe-go edits them. Native macOS/Linux desktop app (Wails + Svelte + ProseMirror) with full FB2 round-trip + XSD validation. Use it to fix/enrich metadata before adding files to your sopds-go library.
License
GNU Affero General Public License v3.0 (AGPL-3.0).
AGPL extends GPL with one important provision: if you run a modified version of sopds-go as a network-accessible service (e.g. a public OPDS catalog or web UI), you must offer the modified source to your users. Self-hosting for personal use is unrestricted.
The original Simple OPDS Python implementation by Dmitry V. Shelepnev (Дмитрий Шелепнёв, © 2014, admin@sopds.ru) is licensed under GPL-3.0-or-later ("any later version" clause). This Go rewrite follows the same copyleft principle, upgraded to AGPL-3.0 specifically because OPDS catalogs are inherently network services. The "or any later version" wording in the original license is what makes the upgrade to AGPL-3.0 cleanly compatible.