Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
767cca7
Add mediaserver\ampache
lachlan-00 Jul 27, 2026
87cd639
Update test_provider_migration_integration.py
lachlan-00 Jul 27, 2026
e1a5d96
Merge branch 'main' into AmpacheConnector
NeptuneHub Jul 29, 2026
29bd88b
Complete Ampache support: fix dispatcher contract bugs, register it i…
NeptuneHub Jul 29, 2026
e6013fd
Fix 13 Ampache backend defects: library-picker shape, API-key-only au…
NeptuneHub Jul 29, 2026
c44c0f8
SonarCloud fix
NeptuneHub Jul 29, 2026
64cc792
Merge branch 'main' into AmpacheConnector
NeptuneHub Jul 29, 2026
cd855c3
issues, docs and tests
lachlan-00 Jul 29, 2026
199266a
gate on API8+. Albums carry catalogs now
lachlan-00 Jul 29, 2026
e928514
Read tracks from a mounted library instead of downloading them
lachlan-00 Jul 30, 2026
bb3a411
Merge branch 'LocalFileAccess' into Ampache+localfile
lachlan-00 Jul 30, 2026
8941afe
stop handshaking all the time
lachlan-00 Jul 30, 2026
aa29c44
Merge branch 'AmpacheConnector' into Ampache+localfile
lachlan-00 Jul 30, 2026
c527433
use APIkey bearer token if possible first
lachlan-00 Jul 30, 2026
c97a9af
Merge branch 'AmpacheConnector' into Ampache+localfile
lachlan-00 Jul 30, 2026
618c2ee
allow larger page sizes
lachlan-00 Jul 30, 2026
66a25bc
Merge branch 'AmpacheConnector' into Ampache+localfile
lachlan-00 Jul 30, 2026
eee7d41
reduce lyrics calls using the album_songs call
lachlan-00 Jul 30, 2026
6744507
Merge branch 'AmpacheConnector' into Ampache+localfile
lachlan-00 Jul 30, 2026
9f9fc0d
simplify id lookup with browse
lachlan-00 Jul 30, 2026
4b51147
simplify id lookup with browse
lachlan-00 Jul 30, 2026
81ab481
Merge branch 'Ampache+localfile' into AmpacheConnector
lachlan-00 Jul 30, 2026
576d222
Revert "Read tracks from a mounted library instead of downloading them"
lachlan-00 Jul 30, 2026
3621258
sonarcube issues
lachlan-00 Jul 30, 2026
9043273
tests codestyle
lachlan-00 Jul 30, 2026
a217afb
test scripts (will remove)
lachlan-00 Jul 30, 2026
f27149b
Revert "test scripts (will remove)"
lachlan-00 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ You can run it locally with Docker Compose or Podman, deploy it at scale in a Ku
> **Prefer not to self-host?** [Elestio](https://elest.io/open-source/audiomuse-ai) offers AudioMuse-AI as a managed cloud service, and their [YouTube video](https://www.youtube.com/watch?v=Ow89q6gQ1mM) is a good introduction to the project and its features.

AudioMuse-AI lets you explore your music library in innovative ways, just **start with an initial analysis**, and you’ll unlock features like:
* **Multiple Music Servers** (from `v3.0.0`): connect several media servers - any mix of Navidrome, Jellyfin, LMS, Lyrion, Emby and Plex - to a **single AudioMuse-AI deployment**. Built-in duplicate detection recognizes the same song across servers, so each track is **analyzed only once** and every server shares the result.
* **Multiple Music Servers** (from `v3.0.0`): connect several media servers - any mix of Navidrome, Jellyfin, LMS, Lyrion, Emby, Plex and Ampache - to a **single AudioMuse-AI deployment**. Built-in duplicate detection recognizes the same song across servers, so each track is **analyzed only once** and every server shares the result.
* **Clustering**: Automatically groups sonically similar songs, creating genre-defying playlists based on the music's actual sound.
* **Instant Playlists**: Simply tell the AI what you want to hear-like "high-tempo, low-energy music" and it will instantly generate a playlist for you.
* **Music Map**: Discover your music collection visually with a vibrant, genre-based 2D map.
Expand Down Expand Up @@ -84,7 +84,7 @@ From `v1.0.0`, only PostgreSQL, Redis and `TZ` are configured via environment va

**Prerequisites:**
* Docker and Docker Compose installed
* A running media server (Navidrome, Jellyfin, Lyrion, Emby, or Plex)
* A running media server (Navidrome, Jellyfin, Lyrion, Emby, Plex, or Ampache)
* See [Hardware Requirements](#hardware-requirements)

**Steps:**
Expand Down
2 changes: 1 addition & 1 deletion app_music_servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

music_servers_bp = Blueprint('music_servers_bp', __name__)

_SUPPORTED_TYPES = ('jellyfin', 'emby', 'navidrome', 'lyrion', 'plex')
_SUPPORTED_TYPES = ('jellyfin', 'emby', 'navidrome', 'lyrion', 'plex', 'ampache')


def _setup_in_progress():
Expand Down
41 changes: 12 additions & 29 deletions app_provider_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def __getattr__(self, name):
# Supported target providers (what the tool knows how to talk to)
# ---------------------------------------------------------------------------

_SUPPORTED_TARGETS = frozenset({'jellyfin', 'navidrome', 'emby', 'lyrion', 'plex'})
_SUPPORTED_TARGETS = frozenset({'jellyfin', 'navidrome', 'emby', 'lyrion', 'plex', 'ampache'})


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -156,32 +156,15 @@ def _current_provider_creds():
import config as cfg

t = (getattr(cfg, 'MEDIASERVER_TYPE', '') or '').lower()
if t == 'jellyfin':
return t, {
'url': getattr(cfg, 'JELLYFIN_URL', ''),
'user_id': getattr(cfg, 'JELLYFIN_USER_ID', ''),
'token': getattr(cfg, 'JELLYFIN_TOKEN', ''),
}
if t == 'emby':
return t, {
'url': getattr(cfg, 'EMBY_URL', ''),
'user_id': getattr(cfg, 'EMBY_USER_ID', ''),
'token': getattr(cfg, 'EMBY_TOKEN', ''),
}
if t == 'navidrome':
return t, {
'url': getattr(cfg, 'NAVIDROME_URL', ''),
'user': getattr(cfg, 'NAVIDROME_USER', ''),
'password': getattr(cfg, 'NAVIDROME_PASSWORD', ''),
}
if t == 'lyrion':
return t, {'url': getattr(cfg, 'LYRION_URL', '')}
if t == 'plex':
return t, {
'url': getattr(cfg, 'PLEX_URL', ''),
'token': getattr(cfg, 'PLEX_TOKEN', ''),
}
return None, {}
fields = cfg.MEDIASERVER_FIELDS_BY_TYPE.get(t)
if not fields:
return None, {}
creds = {}
for field in fields:
key = cfg.MEDIASERVER_CRED_KEY_BY_FIELD.get(field)
if key:
creds[key] = getattr(cfg, field, '')
return t, creds


def _apply_source_path_overrides(old_rows, overrides):
Expand Down Expand Up @@ -270,7 +253,7 @@ def session_start():
properties:
target_type:
type: string
enum: [jellyfin, emby, navidrome, lyrion, plex]
enum: [jellyfin, emby, navidrome, lyrion, plex, ampache]
target_creds:
type: object
additionalProperties: true
Expand Down Expand Up @@ -484,7 +467,7 @@ def probe_test():
properties:
type:
type: string
enum: [jellyfin, emby, navidrome, lyrion, plex]
enum: [jellyfin, emby, navidrome, lyrion, plex, ampache]
creds:
type: object
additionalProperties: true
Expand Down
1 change: 1 addition & 0 deletions app_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def _plex_pin_headers(client_id):
"JELLYFIN_TOKEN",
"EMBY_TOKEN",
"NAVIDROME_PASSWORD",
"AMPACHE_PASSWORD",
"PLEX_TOKEN",
"JWT_SECRET",
"AI_CHAT_DB_USER_PASSWORD",
Expand Down
9 changes: 9 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ def _compute_headers():
NAVIDROME_USER = os.environ.get("NAVIDROME_USER", "")
NAVIDROME_PASSWORD = os.environ.get("NAVIDROME_PASSWORD", "") # Use the password directly

# --- Ampache Constants ---
# These are used only if MEDIASERVER_TYPE is "ampache". AMPACHE_PASSWORD takes either the
# account password or an API key; the handshake tries both, so no separate key field exists.
AMPACHE_URL = os.environ.get("AMPACHE_URL", "") # e.g. http://your-ampache-server
AMPACHE_USER = os.environ.get("AMPACHE_USER", "")
AMPACHE_PASSWORD = os.environ.get("AMPACHE_PASSWORD", "")

# --- Lyrion (LMS) Constants ---
# These are used only if MEDIASERVER_TYPE is "lyrion".
LYRION_URL = os.environ.get("LYRION_URL", "")
Expand All @@ -105,6 +112,7 @@ def _compute_headers():
'lyrion': ['LYRION_URL'],
'emby': ['EMBY_URL', 'EMBY_USER_ID', 'EMBY_TOKEN'],
'plex': ['PLEX_URL', 'PLEX_TOKEN'],
'ampache': ['AMPACHE_URL', 'AMPACHE_USER', 'AMPACHE_PASSWORD'],
}

MEDIASERVER_OBSOLETE_FIELDS_BY_TYPE = {
Expand All @@ -128,6 +136,7 @@ def _compute_headers():
'NAVIDROME_URL': 'url', 'NAVIDROME_USER': 'user', 'NAVIDROME_PASSWORD': 'password',
'LYRION_URL': 'url',
'PLEX_URL': 'url', 'PLEX_TOKEN': 'token',
'AMPACHE_URL': 'url', 'AMPACHE_USER': 'user', 'AMPACHE_PASSWORD': 'password',
}

# The ONLY persistent home of these settings is the music_servers registry
Expand Down
7 changes: 4 additions & 3 deletions docs/MULTI_SERVER.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Multiple Music Servers

AudioMuse-AI can talk to several media servers at once - for example a Navidrome
plus two Jellyfins plus a Plex, in any combination, including several instances
of the same type. This is fully backward compatible: an install that only ever
plus two Jellyfins plus a Plex plus an Ampache, in any combination, including
several instances of the same type. This is fully backward compatible: an install that only ever
configures one server behaves exactly as it always has.

## The model in one picture
Expand Down Expand Up @@ -92,7 +92,8 @@ partial and pruning is skipped, so a transient provider error never
mass-deletes valid mappings. Only map rows are ever removed, never analyzed
tracks. A server's library filter is honoured by every provider: Jellyfin and
Emby fetch only the selected libraries, Plex only the selected sections,
Navidrome only the selected music folders, and Lyrion only the selected paths -
Navidrome only the selected music folders, Ampache only the selected catalogs,
and Lyrion only the selected paths -
so nothing outside the libraries you picked is ever mapped, counted or pruned.

Matching runs in bounded memory even on very large libraries: the fetched
Expand Down
5 changes: 4 additions & 1 deletion docs/PARAMETERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The **mandatory** parameter that you need to change from the example are this:
| Parameter | Description | Default Value |
|----------------------|-------------------------------------------------------------------------|-----------------------------------|
| **Mediaserver General** | | |
| `MEDIASERVER_TYPE` | (Required) Which media server to use: `jellyfin`, `navidrome`, `emby`, `lyrion` or `plex`. | `jellyfin` |
| `MEDIASERVER_TYPE` | (Required) Which media server to use: `jellyfin`, `navidrome`, `emby`, `lyrion`, `plex` or `ampache`. | `jellyfin` |
| `NAVIDROME_URL` | (Required) Your Navidrome server's full URL | `http://YOUR_NAVIDROME_IP:4533` |
| `NAVIDROME_USER` | (Required) Navidrome User ID. | *(N/A - from Secret)* |
| `NAVIDROME_PASSWORD` | (Required) Navidrome user Password. | *(N/A - from Secret)* |
Expand All @@ -39,6 +39,9 @@ The **mandatory** parameter that you need to change from the example are this:
| `LYRION_URL` | (Required) Your Lyrion server's full URL | `http://YOUR_LYRION_IP:9000` |
| `PLEX_URL` | (Required) Your Plex Media Server's full URL | `http://YOUR_PLEX_IP:32400` |
| `PLEX_TOKEN` | (Required) Plex API token (X-Plex-Token). | *(N/A - from Secret)* |
| `AMPACHE_URL` | (Required) Your Ampache server's full URL | `http://YOUR_AMPACHE_IP` |
| `AMPACHE_USER` | (Required) Ampache username. Leave empty when using an API key. | *(N/A - from Secret)* |
| `AMPACHE_PASSWORD` | (Required) Ampache user password or API key. | *(N/A - from Secret)* |
| `POSTGRES_USER` | (Required) PostgreSQL username. | *(N/A - from Secret)* |
| `POSTGRES_PASSWORD` | (Required) PostgreSQL password. | *(N/A - from Secret)* |
| `POSTGRES_DB` | (Required) PostgreSQL database name. | *(N/A - from Secret)* |
Expand Down
5 changes: 5 additions & 0 deletions static/music_servers_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
plex: [
{ key: 'url', label: 'Server URL', placeholder: 'http://plex:32400' },
{ key: 'token', label: 'Plex Token', secret: true }
],
ampache: [
{ key: 'url', label: 'Server URL', placeholder: 'http://ampache' },

Check warning on line 37 in static/music_servers_admin.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using http protocol is insecure. Use https instead.

See more on https://sonarcloud.io/project/issues?id=NeptuneHub_AudioMuse-AI&issues=AZ-u2aChXd3PQyj1Am5P&open=AZ-u2aChXd3PQyj1Am5P&pullRequest=810
{ key: 'user', label: 'Username' },
{ key: 'password', label: 'Password or API key', secret: true }
]
};

Expand Down
9 changes: 7 additions & 2 deletions static/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
plex: [
{name: 'PLEX_URL', label: 'Plex URL', placeholder: 'http://your-plex-server:32400', tooltip: 'Base URL of your Plex Media Server, including http:// or https:// and the port (default 32400). Must be reachable from the AudioMuse-AI container.'},
{name: 'PLEX_TOKEN', label: 'Plex API token', placeholder: 'your-plex-token', tooltip: 'Your X-Plex-Token for the server. See https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ to find it.'}
],
ampache: [
{name: 'AMPACHE_URL', label: 'Ampache URL', placeholder: 'http://your-ampache-server', tooltip: 'Base URL of your Ampache server, including http:// or https:// and the port if it is not the default.'},

Check warning on line 25 in static/setup.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using http protocol is insecure. Use https instead.

See more on https://sonarcloud.io/project/issues?id=NeptuneHub_AudioMuse-AI&issues=AZ-u2aDyXd3PQyj1Am5Q&open=AZ-u2aDyXd3PQyj1Am5Q&pullRequest=810
{name: 'AMPACHE_USER', label: 'Ampache username', placeholder: 'your-username', tooltip: 'Username of an Ampache account that can read the music library. Leave empty if you authenticate with an API key instead of a password.'},
{name: 'AMPACHE_PASSWORD', label: 'Ampache password or API key', placeholder: 'your-password-or-api-key', tooltip: 'Password for the Ampache user above, or an Ampache API key. The handshake accepts either, so there is no separate API key field.'}
]
};

Expand Down Expand Up @@ -276,7 +281,7 @@
value = values[field.name];
}
var secret = false;
var secretKeys = ['NAVIDROME_PASSWORD', 'AUDIOMUSE_PASSWORD', 'API_TOKEN', 'JELLYFIN_TOKEN', 'EMBY_TOKEN', 'PLEX_TOKEN'];
var secretKeys = ['NAVIDROME_PASSWORD', 'AUDIOMUSE_PASSWORD', 'API_TOKEN', 'JELLYFIN_TOKEN', 'EMBY_TOKEN', 'PLEX_TOKEN', 'AMPACHE_PASSWORD'];
for (var i = 0; i < secretKeys.length; i++) {
if (secretKeys[i] === field.name) {
secret = true;
Expand Down Expand Up @@ -625,7 +630,7 @@

function saveCurrentServerValues() {
var currentServerType = document.getElementById('MEDIASERVER_TYPE').value;
var keys = ['JELLYFIN_URL', 'JELLYFIN_USER_ID', 'JELLYFIN_TOKEN', 'NAVIDROME_URL', 'NAVIDROME_USER', 'NAVIDROME_PASSWORD', 'LYRION_URL', 'EMBY_URL', 'EMBY_USER_ID', 'EMBY_TOKEN', 'PLEX_URL', 'PLEX_TOKEN'];
var keys = ['JELLYFIN_URL', 'JELLYFIN_USER_ID', 'JELLYFIN_TOKEN', 'NAVIDROME_URL', 'NAVIDROME_USER', 'NAVIDROME_PASSWORD', 'LYRION_URL', 'EMBY_URL', 'EMBY_USER_ID', 'EMBY_TOKEN', 'PLEX_URL', 'PLEX_TOKEN', 'AMPACHE_URL', 'AMPACHE_USER', 'AMPACHE_PASSWORD'];
keys.forEach(function(key) {
var input = document.getElementById(key);
if (input) {
Expand Down
2 changes: 1 addition & 1 deletion tasks/mediaserver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

logger = logging.getLogger(__name__)

_PROVIDER_NAMES = ('jellyfin', 'navidrome', 'lyrion', 'emby', 'plex')
_PROVIDER_NAMES = ('jellyfin', 'navidrome', 'lyrion', 'emby', 'plex', 'ampache')
_warned_unsupported = set()

_PLAYLIST_NAME_REQUIRED = "Playlist name is required."
Expand Down
Loading