# MusicController Library Sync Architecture in Music Assistant: A Deep Dive

> Explore the MusicController library sync architecture in Music Assistant. Understand its five layers, background synchronization, and how it integrates external media into a local SQLite database.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: architecture
- Published: 2026-06-13

---

**The MusicController library sync architecture in Music Assistant coordinates five interacting layers—Core controller, MusicController, Media sub-controllers, Providers, and Task system—to manage asynchronous background synchronization of media libraries from external providers into a local SQLite database using deterministic task IDs and async locking mechanisms.**

The music-assistant/server repository implements a sophisticated synchronization system through its MusicController library sync architecture. This design handles the complex orchestration of importing metadata from streaming services and local sources into a unified database. Understanding this architecture is essential for developers extending the platform, debugging sync issues, or integrating custom providers.

## Five-Layer Architecture Overview

The MusicController library sync architecture consists of five distinct layers that work together to manage library synchronization:

- **Core Controller** — The base class providing common utilities including logging, event signaling, and task handling. Implemented in [`music_assistant/models/core_controller.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/core_controller.py).
- **MusicController** — The primary orchestrator that manages provider sync operations, registers background tasks, and maintains the internal SQLite database. Found in [`music_assistant/controllers/music.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/music.py) (class definition starts at line 137).
- **Media Sub-Controllers** — Specialized controllers for each media type (artists, albums, tracks, playlists, radio, audiobooks, podcasts, genres) that handle CRUD operations and database searches. Located in `music_assistant/controllers/media/*.py` (e.g., [`albums.py`](https://github.com/music-assistant/server/blob/main/albums.py), [`tracks.py`](https://github.com/music-assistant/server/blob/main/tracks.py)).
- **Providers** — External sources such as Spotify or Plex that implement the `MusicProvider` interface and expose a `sync_library(media_type)` coroutine. Defined in [`music_assistant/models/music_provider.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/music_provider.py).
- **Task System** — Centralized background task manager that persists schedules, handles execution, and manages retries. Implemented in [`music_assistant/tasks/__init__.py`](https://github.com/music-assistant/server/blob/main/music_assistant/tasks/__init__.py) and integrated via [`music_assistant/helpers/api.py`](https://github.com/music-assistant/server/blob/main/music_assistant/helpers/api.py).

Each layer communicates through well-defined interfaces, with the MusicController acting as the central coordinator between the task system and provider implementations.

## The Sync Lifecycle

### Triggering Synchronization via API

When a user or plugin initiates synchronization, the API command `music/sync` invokes the `start_sync` method in [`music_assistant/controllers/music.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/music.py) (lines 66-98). This method creates a **BackgroundTask** for every enabled provider-media-type pair.

```python

# music_assistant/controllers/music.py – start_sync (lines 66-98)

tasks: list[BackgroundTask] = []
if media_types is None:
    media_types = MediaType.ALL
if providers is None:
    providers = [x.instance_id for x in self.providers]

for media_type in media_types:
    for provider in self.providers:
        if provider.instance_id not in providers:
            continue                      # ⟶ skip disabled provider

        if not self.library_supported(provider, media_type):
            continue                      # ⟶ provider does not expose this media_type

        # … read per‑provider sync config …

        await self._schedule_provider_mediatype_sync(provider, media_type, True)
        task_id = self._get_sync_task_id(provider, media_type)
        tasks.append(self.mass.tasks.run_background_task(
            task_id=task_id,
            name=self._get_sync_task_name(provider, media_type),
            handler=self._create_provider_sync_handler(provider, media_type),
            …))

```

Each sync task receives a deterministic ID formatted as `music_sync_<provider>_<type>`, allowing the scheduler to query, unregister, or prevent duplicate tasks.

### Task Scheduling and Registration

When providers load or unload, the controller automatically manages sync task registration. The `schedule_provider_sync` method (lines 92-104) iterates through all media types and registers tasks for supported combinations:

```python

# music_assistant/controllers/music.py – schedule_provider_sync (lines 92-104)

async def schedule_provider_sync(self, provider_instance_id: str) -> None:
    provider = self.mass.get_provider(provider_instance_id, provider_type=MusicProvider)
    if not provider:
        return
    self.unschedule_provider_sync(provider.instance_id, clear_persisted_state=False)
    for media_type in MediaType:
        if self.library_supported(provider, media_type):
            await self._schedule_provider_mediatype_sync(provider, media_type, True)

```

Conversely, `unschedule_provider_sync` (lines 104-116) removes all scheduled tasks for a specific provider:

```python

# music_assistant/controllers/music.py – unschedule_provider_sync (lines 104-116)

def unschedule_provider_sync(self, provider_instance_id: str, clear_persisted_state: bool = True) -> None:
    for media_type in MediaType:
        self.mass.tasks.unregister_scheduled_task(
            self._get_sync_task_id(provider_instance_id, media_type),
            clear_persisted_state=clear_persisted_state,
        )

```

### Provider-Specific Sync Handlers

The actual synchronization work executes within a coroutine generated by `_create_provider_sync_handler` (lines 158-169). This handler acquires an async lock (`self._sync_lock`) to prevent overlapping syncs, calls the provider's `sync_library` method, and schedules a completion check:

```python

# music_assistant/controllers/music.py – _create_provider_sync_handler (lines 158-169)

def _create_provider_sync_handler(self, provider, media_type):
    async def run_sync():
        try:
            async with self._sync_lock:
                await provider.sync_library(media_type)
        finally:
            # after each sync we schedule a quick check that runs when

            # no other sync tasks are active

            self.mass.call_later(
                0, self._handle_sync_completion_check,
                task_id=MUSIC_SYNC_COMPLETION_CHECK_TASK_ID,
            )
    return run_sync

```

The `_sync_lock` ensures that only one provider synchronizes at a time, preventing database contention and API rate limit issues.

## Completion Handling and Database Maintenance

When the final active sync task completes, `_handle_sync_completion_check` (lines 176-182) emits a global `MUSIC_SYNC_COMPLETED` event and triggers database maintenance:

```python

# music_assistant/controllers/music.py – _handle_sync_completion_check (lines 176-182)

if not self.active_sync_tasks:
    self.mass.signal_event(EventType.MUSIC_SYNC_COMPLETED)
    self._queue_database_cleanup_task()

```

The cleanup task removes stale play-log entries and orphaned database rows. This maintenance task is registered with a daily schedule via `_register_database_cleanup_task`, ensuring the SQLite database remains optimized without manual intervention.

## Database Initialization

At startup, the MusicController initializes a dedicated SQLite database through `_setup_database` (lines 67-73). The system creates the database file at `$HOME/.musicassistant/library.db`, executes migration scripts, builds indexes, and compacts the database if sufficient free space exists:

```python

# music_assistant/controllers/music.py – _setup_database (lines 67-73)

db_path = os.path.join(self.mass.storage_path, "library.db")
self._database = DatabaseConnection(db_path)
await self._database.setup()
await self.__create_database_tables()

# … migrate if needed, create indexes, vacuum …

```

The `DatabaseConnection` wrapper ([`music_assistant/helpers/database.py`](https://github.com/music-assistant/server/blob/main/music_assistant/helpers/database.py)) provides async access to SQLite, allowing the sync architecture to perform non-blocking database operations during synchronization.

## Practical Implementation Examples

### Triggering a Full Library Sync

```python
from music_assistant import MusicAssistant

async def sync_all():
    mass = MusicAssistant()
    await mass.start()                     # initialises all controllers

    # Start sync for every provider and every media type

    await mass.music.start_sync()
    # Wait for the background tasks to finish

    await mass.tasks.wait_for_all(domain="music_sync")

```

### Manually Scheduling a Daily Sync

```python
from music_assistant.models import MediaType

provider_id = "spotify"                    # instance ID from config

media_type   = MediaType.TRACK

# Register a daily task at 02:00 UTC

mass.tasks.register_scheduled_task(
    task_id=mass.music._get_sync_task_id(provider_id, media_type),
    name=mass.music._get_sync_task_name(mass.music.get_provider(provider_id), media_type),
    handler=mass.music._create_provider_sync_handler(
        mass.get_provider(provider_id, provider_type=MusicProvider),
        media_type,
    ),
    schedule=TaskSchedule.daily(hour=2, minute=0),   # ↪ `music_assistant/models/task.py`

    translation_key=mass.music._get_sync_task_translation_key(media_type),
    metadata=mass.music._get_sync_task_metadata(
        mass.get_provider(provider_id, provider_type=MusicProvider),
        media_type,
    ),
)

```

### Inspecting the Current Sync Schedule

```python
schedule = mass.music.get_provider_sync_schedule("spotify", MediaType.ALBUM)
print(schedule)          # → TaskSchedule object (or None if disabled)

```

## Summary

- The MusicController library sync architecture uses five coordinated layers to separate concerns between core utilities, orchestration, media-specific operations, external providers, and task management.
- Each sync operation generates a deterministic task ID (`music_sync_<provider>_<type>`) that enables persistent scheduling and prevents duplicate executions across restarts.
- The `_sync_lock` async lock guarantees thread-safe execution by allowing only one provider synchronization at a time.
- Automatic cleanup and database compaction occur after every sync cycle completes, maintaining optimal SQLite performance.
- All sync tasks integrate with the centralized task manager in [`music_assistant/tasks/__init__.py`](https://github.com/music-assistant/server/blob/main/music_assistant/tasks/__init__.py), supporting both immediate background execution and scheduled recurring synchronization.

## Frequently Asked Questions

### How does MusicController prevent concurrent sync conflicts?

The architecture implements an async lock (`self._sync_lock`) within the `_create_provider_sync_handler` method. When a sync task executes, it acquires this lock before calling `provider.sync_library()`, ensuring that only one provider synchronizes at a time. This prevents database write conflicts and helps manage API rate limits from external services.

### What happens when a provider is removed from the configuration?

The controller automatically invokes `unschedule_provider_sync`, which iterates through all media types and unregisters each scheduled task using `self.mass.tasks.unregister_scheduled_task`. By default, this clears the persisted task state, ensuring no orphaned tasks remain in the schedule after provider removal.

### Where is the synchronized library data stored?

The MusicController initializes a SQLite database at `$HOME/.musicassistant/library.db` via the `_setup_database` method. This `DatabaseConnection` instance (from [`music_assistant/helpers/database.py`](https://github.com/music-assistant/server/blob/main/music_assistant/helpers/database.py)) stores all media metadata, provider mappings, and play logs, with automatic migration and indexing handled at startup.

### How can I programmatically check if a sync is currently running?

You can inspect the `active_sync_tasks` property of the MusicController instance or query the task manager directly. The controller tracks all running sync operations, and the completion check (`_handle_sync_completion_check`) only fires when this collection is empty, indicating no active synchronization tasks remain.