Skip to main content

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: AmpacheClient for 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. ping without 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 no filter and no limit fetches the entire server library into your DB in one call (same for getArtists(), 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]

cond and sort are string parameters, passed through to the server verbatim (cond is 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_count envelopes - some Ampache builds report stale/wrong counts. The client auto-paginates until a short page; for offline paging use PageResult.total instead.
  • Pass stats=0 on 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 raises InvalidHandshakeError by 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_thread behavior); 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.

Release files for ampachedata 0.1.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ampachedata 0.1.7
File Size Uploaded
ampachedata-0.1.7.tar.gz 93.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ampachedata 0.1.7
File Interpreter ABI Platform
ampachedata-0.1.7-py3-none-any.whl Python 3 none any Details

Total release size: 170.5 kB

Release files / ampachedata-0.1.7.tar.gz

Download URL ampachedata-0.1.7.tar.gz
Size 93.7 kB
Tags Source
SHA-256 checksum
How to use checksums
775d968fdf58b06e785de443be39e9eaea811cba7d9ffe796d389050749383b7
BLAKE2b-256 checksum
How to use checksums
fc665e20430ac2a5167b5e128ac71c845a567e17caf20add97b096a15d19dda3
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.7-py3-none-any.whl

Download URL ampachedata-0.1.7-py3-none-any.whl
Size 76.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
679adc114db5a31cd5998182c2962c91bc649bf62490d2b8426fb93d3c48c6df
BLAKE2b-256 checksum
How to use checksums
60f797eb1e15efa4c79b25bafd8c07d313bac1803f6fda48f2cf9bfc53f004d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

0.1.7 This release

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page