MusicController Library Sync Architecture in Music Assistant: A Deep Dive
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. - 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(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,tracks.py). - Providers — External sources such as Spotify or Plex that implement the
MusicProviderinterface and expose async_library(media_type)coroutine. Defined inmusic_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__.pyand integrated viamusic_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 (lines 66-98). This method creates a BackgroundTask for every enabled provider-media-type pair.
# 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:
# 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:
# 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:
# 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:
# 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:
# 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) 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
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
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
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_lockasync 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, 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) 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →