ampachedata
Client library for the Ampache JSON API with a SQLite write-through data layer.
Every API response is persisted to SQLite first, then read back - the database is
the single source of truth, and callers never receive raw server payloads.
Zero runtime dependencies, stdlib only (pure Python + sqlite3).
- Live-proven against real Ampache 8.x, 7.x servers
- Session persistence across process restarts, with silent re-auth + one retry on expiry
- Two layers:
AmpacheClientfor network + write-through, repositories for offline reads (no network) - Cleartext passwords are accepted once, hashed in memory, and never stored or logged
Requirements
- Python 3.11+
- A SQLite database with the schema - create one with the opt-in
createDatabase()helper (see Quick start; the library never creates or alters schema on its own, by design)
Installation
pip install ampachedata
Optional dev extras (pytest):
pip install ampachedata[dev]
From source:
git clone https://github.com/icefields/Ampache-Data-Library
cd Ampache-Data-Library
pip install -e .
Quick start
1. Create the database (one time). The library deliberately never creates or alters schema on its own - you own the file. The opt-in helper builds it from the schema bundled inside the package:
from ampachedata import createDatabase
createDatabase("musicdb.db")
createDatabase refuses to touch an existing file - it raises DatabaseError
rather than altering your database.
2. Store credentials (one time per user/server). The cleartext password is accepted once, hashed with SHA256 in memory, and only the digest is persisted:
from ampachedata import storeCredentialsFromPassword
storeCredentialsFromPassword(
dbPath="musicdb.db",
username="alice",
serverUrl="https://ampache.example.com",
cleartextPassword="secret",
)
Or use the CLI (hidden prompt, stdin, or env var - no --password flag, so
cleartext never lands in argv or shell history):
python -m ampachedata init-credentials --db-path musicdb.db
# or fully scripted:
python -m ampachedata init-credentials --db-path musicdb.db \
--username alice --server-url https://ampache.example.com --password-stdin
If you already hold a pre-hashed Ampache API key (64 lowercase hex chars), use
storeCredentialsFromKey or the --key-stdin / --key flags instead.
3. Use the client. The first authenticated call triggers the handshake from the stored credentials automatically - no explicit login step:
from ampachedata import AmpacheClient
client = AmpacheClient(dbPath="musicdb.db")
artists = client.getArtists(limit=50)
print(artists[0].name)
# Write-through: everything fetched is already in the DB
songs = client.getSongs(filter="Supernaut", exact=1)
# Build a playable URL (no network call, nothing persisted)
url = client.getStreamUrl(songs[0].id, stats=0)
# Done for this session - tears down the server session cleanly
client.goodbye()
Architecture: write-through, both directions
┌───────────────────────────────┐
Ampache ────▶│ AmpacheClient (network layer)│────▶ SQLite (musicdb.db)
JSON API │ persist first, read back │ │
└───────────────────────────────┘ ▼
┌───────────────────────────────┐
Your UI ◀─── │ Repositories (offline layer) │◀─── typed entities
(no network) │ list / search / count / page │
└───────────────────────────────┘
- Network layer (
AmpacheClient): every library-data fetch persists the response to SQLite first, then returns typed entities read back from the database - never raw JSON. (The media-URL builders are pure string builders: no network, no DB write.pingwithout a stored session is an anonymous probe that persists nothing.) - Offline layer (repositories): query what has been cached. Zero network. Built for UI list views, search boxes, and pagination.
Consequence: offline reads only see what has been fetched at least once. Start
from an empty DB and listSongs() returns nothing until you fetch. This is the
intended cold-cache model, not a bug.
AmpacheClient reference
Constructed as AmpacheClient(dbPath, transport=None) - transport is
an optional HTTP transport injection seam (used by the test suite; the
default is a stdlib urllib transport).
All list methods auto-paginate server-side (loop until short page) - you never page by hand. Entity ids are strings throughout; returned entities are immutable frozen dataclasses with clean field names (no JSON keys, no DB-only columns).
Unfiltered list calls sync everything.
getSongs()with nofilterand nolimitfetches the entire server library into your DB in one call (same forgetArtists(),getAlbums(), …). That is the intended write-through sync behavior - but be deliberate about it.
Session & health
| Method | Returns | Notes |
|---|---|---|
ping() |
PingResult |
Health check: authenticated, api, server, version, sessionExpire. Some servers answer authenticated optimistically - don't rely on ping as a session probe. |
goodbye() |
OperationResult |
Tears down the server session and deletes the persisted token. After goodbye, authenticated calls raise InvalidHandshakeError (no resurrection on this instance) - but ping() falls back to anonymous mode and returns authenticated=False. Create a new AmpacheClient to re-auth. |
lastPayload (property) |
dict or None |
The raw envelope of the last call (informational: total_count, md5, …). |
Sessions persist across restarts and process kills (the token lives in the DB).
On expiry (error 4701 / HTTP 401/403), the client silently re-authenticates
and retries once; a second failure raises InvalidHandshakeError.
Artists
| Method | Returns |
|---|---|
getArtists(filter="", exact=None, add=None, update=None, include=None, albumArtist=None, offset=None, limit=None, cond=None, sort=None) |
list[Artist] |
getArtist(filter, include=None) |
Artist - include="albums,songs" upserts nested rows |
Albums
| Method | Returns |
|---|---|
getAlbums(filter="", exact=None, offset=None, limit=None, add=None, update=None, cond=None, sort=None) |
list[Album] |
getAlbum(filter, include=None) |
Album |
getAlbumsFromArtist(artistId, albumArtist=None, offset=None, limit=None, cond=None, sort=None) |
list[Album] |
getAlbumSongs(albumId, offset=None, limit=None, cond=None, sort=None) |
list[Song] |
Songs
| Method | Returns |
|---|---|
getSongs(filter="", exact=None, add=None, update=None, offset=None, limit=None, cond=None, sort=None) |
list[Song] |
getSong(filter) |
Song |
getArtistSongs(artistId, top50=None, offset=None, limit=None, cond=None, sort=None) |
list[Song] |
condandsortare string parameters, passed through to the server verbatim (condis a;-separated filter string per the Ampache API; anything you pass is stringified into the query - build the string yourself). Power-user parameters; everything else is the common case.
Stats (play-derived)
All take (userId=None, username=None, offset=None, limit=None).
| Songs | Albums |
|---|---|
getRecentSongs() |
getRecentAlbums() |
getFrequentSongs() |
getFrequentAlbums() |
getForgottenSongs() |
getForgottenAlbums() |
getRandomSongs() |
getRandomAlbums() |
getNewestSongs() |
getNewestAlbums() |
getHighestSongs() |
getHighestAlbums() |
Playlists
| Method | Returns |
|---|---|
getPlaylists(filter="", hideSearch=None, showDupes=None, exact=None, add=None, update=None, offset=None, limit=None, cond=None, sort=None) |
list[Playlist] |
getPlaylist(filter) |
Playlist |
getSongsFromPlaylist(playlistId, random=None, offset=None, limit=None) |
list[Song] - order matches the server's playlist positions exactly (no renumbering) |
Media URLs
| Method | Returns | Notes |
|---|---|---|
getStreamUrl(songId, format=None, bitrate=None, offset=None, stats=None) |
str |
Pure URL builder: no network, no DB write. Song-only per API spec. |
getDownloadUrl(songId, format=None, bitrate=None, stats=None) |
str |
Same - download flavor. |
getArtUrl(objectType, objectId, size=None) |
str |
Same - art-image flavor. objectType is one of the four ObjectType values; size is an optional WxH string. |
The URLs embed the live session token as a query parameter - treat them
as secrets: don't log them, share them, or paste them into bug reports.
The library never logs or stores built URLs. Pass stats=0 for any fetch that is not
a real user play (preloading, probing, artwork) - otherwise the server records a
play and pollutes play counts.
Every built URL carries the object UID twice - as both id and filter:
Nextcloud Music reads id and rejects filter-only URLs, Ampache reads
filter, and each backend ignores the parameter it does not know
(backend-developer-confirmed), so both ride for maximal compatibility.
getArtUrl(objectType, objectId, size=None) builds the art-image URL for one
library object - pure URL construction: no network call, no DB write, nothing
persisted. objectType is one of the four ObjectType values ("song",
"album", "artist", "playlist"); size is an optional WxH string such
as "640x480", appended only when set.
Interactions (mutate server state)
| Method | Returns | Notes |
|---|---|---|
flag(objectType, objectId, flagged) |
the re-fetched, refreshed entity | Applies, re-fetches via the type's getter, upserts, verifies - raises CacheVerificationError on mismatch. |
rate(objectType, objectId, rating) |
the re-fetched, refreshed entity | rating validated locally as 0–5 before any network call - out of range raises plain ValueError, not an AmpacheError. Same verify discipline. |
objectType accepts an ObjectType enum member or its plain string value
("song", "album", "artist", "playlist").
Offline query tier
Import Database plus the repositories and read what's cached - no network,
no handshake needed:
from ampachedata import Database, SongRepository, ArtistRepository
db = Database("musicdb.db")
songs = SongRepository(db)
artists = ArtistRepository(db)
page = songs.listSongs(order="recent", limit=100, offset=0)
print(len(page.rows), "of", page.total) # total = honest SQL COUNT
hits = songs.searchSongs("Supernaut") # LIKE search, % and _ are
# escaped - input is literal
byName = artists.searchArtists("megadeth") # case-insensitive (ASCII)
PageResult carries rows and total; total is the SQL COUNT over the full
filtered set - unlike server envelopes, it never lies, so you can compute page
counts reliably.
| Repository | Read methods |
|---|---|
SongRepository |
listSongs(order, limit, offset, artistId, albumId), searchSongs(query, limit, offset), songCount(), playlistSongs(playlistId) (position order; getPlaylistSongs() is an alias), getSong(songId), getAlbumSongs(albumId), getArtistSongs(artistId), getSongsByLastPlayed(ascending=False), getSongsByPlayCount(), getSongs() |
ArtistRepository |
listArtists(), searchArtists(query), artistCount(), getArtist(artistId), getArtists() |
AlbumRepository |
listAlbums(), searchAlbums(query), albumCount(), getAlbum(albumId), getAlbumsFromArtist(artistId), getAlbums() |
PlaylistRepository |
listPlaylists(), searchPlaylists(query), playlistCount(), getPlaylist(playlistId), getPlaylists() |
order for listSongs is one of "title", "artist", "album", "recent"
(whitelisted - arbitrary SQL cannot be injected).
Case-insensitive search is ASCII-only (e.g. Cyrillic matches case-sensitively) -
that is SQLite LIKE semantics, documented rather than worked around.
Errors
Every exception derives from AmpacheError. Catch the family, or the exact
class:
AmpacheError
├── DatabaseError # musicdb.db missing or unusable
├── CredentialValidationError # bootstrap input failed validation
├── CacheVerificationError # post-flag/rate read-back mismatch
└── ApiError # Ampache error envelope
├── InvalidHandshakeError # 4701 / HTTP 401/403 - triggers auto re-auth (except after goodbye)
├── AccessDeniedError # 4703
├── NotFoundError # 4704
├── DeprecatedError # 4706
├── BadRequestError # 4710
└── UnknownApiError # anything else
ApiError carries .code and .message. Error messages never contain the
cleartext password or the stored hash.
Best practices
- Let the library own writes. All upserts (history, playlist positions, ratings) derive from server payloads - never write entity rows by hand.
- Don't trust server
total_countenvelopes - some Ampache builds report stale/wrong counts. The client auto-paginates until a short page; for offline paging usePageResult.totalinstead. - Pass
stats=0on non-playback media fetches so plays aren't recorded. - Treat built stream/download URLs as secrets - they embed the live session token as a query parameter.
- Call
goodbye()when done - it's the clean logout, and it deletes the stored session token. Reuse one client per session rather than one per call. - Create the client fresh after
goodbye()- a post-goodbye client raisesInvalidHandshakeErrorby design (no resurrection). - Exact filters with special characters (
?,%,_) can behave inconsistently on some servers; prefer narrow, plain-text filters. - Songs removed from a playlist stay in the cache - playlist join rows are upsert-only (no deletion), so offline reads keep showing them until the server's next payload omits them.
- Not thread-safe - the client holds one SQLite connection (sqlite3's
default
check_same_threadbehavior); using one client instance across threads raises. Create one client per thread. - Keep the DB per-user - history rows are keyed by user + media id.
Server compatibility
Developed and live-tested against Ampache 8.0.1 (API 8) and Ampache 7.9.2 (API 6) (json.server.php, JSON API). The method set (handshake, ping, artists/albums/songs, stats, playlists, flag, rate, stream, download, goodbye) is long-standing Ampache JSON API surface, live-proven on both generations. Python 3.11–3.14 supported.
License
GPL-3.0-only - the full license text ships in the sdist and wheel.
Links
- Source & issues: https://github.com/icefields/Ampache-Data-Library
- PyPI: https://pypi.org/project/ampachedata/
Metadata
Release files for ampachedata 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ampachedata-0.1.8.tar.gz | 93.8 kB | Without provenance | Details for ampachedata-0.1.8.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ampachedata-0.1.8-py3-none-any.whl | Python 3 | none | any | Details for ampachedata-0.1.8-py3-none-any.whl |
Total release size: 170.7 kB
Release files / ampachedata-0.1.8.tar.gz
| Download URL | ampachedata-0.1.8.tar.gz |
|---|---|
| Size | 93.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
81dd13783948e33482232fe633a9a71e48a8457e5a77777058493b961cb77973
|
|
BLAKE2b-256 checksum How to use checksums |
35ce7af4a0ba117fefa546dc543463c9cbc10e91edbb4a787acb7c5b8f172140
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / ampachedata-0.1.8-py3-none-any.whl
| Download URL | ampachedata-0.1.8-py3-none-any.whl |
|---|---|
| Size | 76.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6b4d263fbcca3f0481eb16fc5b5833f14ca449388c1a95cb2386f3c7f675d699
|
|
BLAKE2b-256 checksum How to use checksums |
6a2dc1d4f2db572f4eafe057dfad853011fa879e2981c0acc9dec54b5f8a0518
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for ampachedata 0.1.8
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.