Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CesarArellano/Music-Player-App/llms.txt

Use this file to discover all available pages before exploring further.

Musynth’s data layer is built around the repository pattern: each abstract interface decouples business logic and UI from the underlying platform APIs. Services and Cubits program against the interfaces; GetIt supplies the concrete implementations at runtime without the callers knowing which package or storage mechanism is actually used. This page covers the three repository interfaces (AudioRepository, ArtworkRepository, PreferencesRepository), their concrete implementations, and ArtworkCacheService, which pre-builds the album art file cache on first launch.

Repository Overview

AudioRepository

Queries songs, albums, artists, genres, and playlists from the device media store. Also manages playlist CRUD operations.

ArtworkRepository

Fetches raw artwork bytes and extracts dominant colours for a given song or album ID.

PreferencesRepository

Reads and writes app-level preferences (last played song, favourites list, dominant colour map) backed by SharedPreferences.
All three interfaces are satisfied by two concrete classes registered in di.dart:
  • MusicQuerySelectorRepository implements both AudioRepository and ArtworkRepository.
  • SharedPreferencesRepository implements PreferencesRepository.

AudioRepository

abstract interface class AudioRepository {
  Future<bool>                 requestPermissions();
  Future<List<SongModel>>      querySongs();
  Future<List<AlbumModel>>     queryAlbums();
  Future<List<GenreModel>>     queryGenres();
  Future<List<ArtistModel>>    queryArtists();
  Future<List<PlaylistModel>>  queryPlaylists();
  Future<List<SongModel>>      queryAudiosByGenreId(int genreId);
  Future<List<SongModel>>      queryAudiosByPlaylistId(int playlistId);
  Future<bool>                 createPlaylist(String name);
  Future<bool>                 addToPlaylist(int playlistId, int songId);
  Future<bool>                 removeFromPlaylist(int playlistId, int songId);
  Future<bool>                 removePlaylist(int playlistId);
}
requestPermissions()
Future<bool>
Requests media-library access. On iOS the method polls up to six times (500 ms apart, ~3 s total) because MPMediaLibrary.requestAuthorization fires a callback asynchronously and the Swift wrapper returns false before it executes.
querySongs()
Future<List<SongModel>>
Returns all audio files on the device, sorted alphabetically by title (SongSortType.TITLE).
queryAlbums()
Future<List<AlbumModel>>
Returns all album records found in the media store.
queryGenres()
Future<List<GenreModel>>
Returns all genre records.
queryArtists()
Future<List<ArtistModel>>
Returns all artist records.
queryPlaylists()
Future<List<PlaylistModel>>
Returns all user-created playlists.
queryAudiosByGenreId(int genreId)
Future<List<SongModel>>
Returns all songs belonging to a specific genre, queried with AudiosFromType.GENRE_ID.
queryAudiosByPlaylistId(int playlistId)
Future<List<SongModel>>
Returns all songs inside a specific playlist, queried with AudiosFromType.PLAYLIST.
createPlaylist(String name)
Future<bool>
Creates a new empty playlist. Returns true on success.
addToPlaylist(int playlistId, int songId)
Future<bool>
Adds a song to an existing playlist.
removeFromPlaylist(int playlistId, int songId)
Future<bool>
Removes a song from a playlist without deleting the playlist itself.
removePlaylist(int playlistId)
Future<bool>
Permanently deletes a playlist.

ArtworkRepository

abstract interface class ArtworkRepository {
  Future<Uint8List?>  queryArtwork(int songId, {int size = 500});
  Future<int?>        queryArtworkColor(int albumId);
  Future<int?>        queryArtworkColorFromPath(String path);
}
queryArtwork(int songId, {int size = 500})
Future<Uint8List?>
Returns raw JPEG/PNG bytes for a song’s embedded cover art, scaled to size × size pixels. Returns null if no artwork is embedded. Used by ArtworkCacheService to write <albumId>.jpg files.
queryArtworkColor(int albumId)
Future<int?>
Extracts the dominant colour of an album’s artwork as a packed ARGB integer. Used to theme the player screen background.
queryArtworkColorFromPath(String path)
Future<int?>
Same as above but accepts a file-system path, useful after artwork has already been cached to disk.

MusicQuerySelectorRepository

Both AudioRepository and ArtworkRepository are implemented by a single class:
class MusicQuerySelectorRepository
    implements AudioRepository, ArtworkRepository {
  MusicQuerySelectorRepository(this._query);
  final MusicQuerySelector _query;
  // ... delegates every method to _query
}
GetIt registers it once under AudioRepository and then reuses the same instance for ArtworkRepository by casting:
setupLocator.registerLazySingleton<AudioRepository>(
  () => MusicQuerySelectorRepository(setupLocator<MusicQuerySelector>()),
);

setupLocator.registerLazySingleton<ArtworkRepository>(
  () => setupLocator<AudioRepository>() as ArtworkRepository,
);
Because both registrations point to the same instance, native calls are deduplicated — the MusicQuerySelector plugin is only initialised once regardless of how many services request either interface.

PreferencesRepository

abstract interface class PreferencesRepository {
  bool              get isNotFirstTime;   set isNotFirstTime(bool value);
  int               get lastSongId;       set lastSongId(int value);
  int               get lastSongDuration; set lastSongDuration(int value);
  int               get numberOfSongs;    set numberOfSongs(int value);
  String            get appDirectory;     set appDirectory(String value);
  List<String>      get favoriteSongList; set favoriteSongList(List<String> value);
  Map<String, String> get dominantColorCollection;
                      set dominantColorCollection(Map<String, String> value);
}
isNotFirstTime
bool
Set to true by ArtworkCacheService.buildArtworkCache once an initial cache build completes. LibraryCubit checks this flag at startup: while it is false, buildArtworkCache is called with force: true, bypassing the numberOfSongs guard and ensuring the cache is always built on the very first launch.
lastSongId
int
The id of the most recently played song. Read on startup to restore the player to the last track.
lastSongDuration
int
The playback position (in milliseconds) at the time of the last pause() or stop(). Passed as initialPosition to loadPlaylist via MusicOrchestratorService.initSong.
numberOfSongs
int
The song count from the last successful library scan. ArtworkCacheService compares this with the current count to decide whether to rebuild the artwork cache.
appDirectory
String
The application documents directory path, resolved once by ArtworkCacheService.resolveAppDirectory and cached here for the lifetime of the app.
favoriteSongList
List<String>
Persisted list of favourite song IDs (stored as strings). Loaded at startup to hydrate FavoritesCubit.
dominantColorCollection
Map<String, String>
Album-ID-to-colour-hex map used to theme the player screen. Decoded once from JSON and served from an in-memory cache on subsequent reads.

SharedPreferencesRepository

class SharedPreferencesRepository implements PreferencesRepository {
  SharedPreferencesRepository(this._prefs);
  final UserPreferences _prefs;

  Map<String, String>? _colorCache;

  @override
  Map<String, String> get dominantColorCollection =>
      _colorCache ??= _prefs.dominantColorCollection;

  @override
  set dominantColorCollection(Map<String, String> value) {
    _colorCache = value;
    Isolate.run(() => json.encode(value))
        .then((encoded) => _prefs.setRawDominantColor(encoded));
  }
}
All scalar properties (isNotFirstTime, lastSongId, etc.) are simple pass-throughs to UserPreferences, which wraps SharedPreferences. The dominantColorCollection property has two performance optimisations:
1

In-memory read cache

The getter uses ??= to decode the JSON stored in SharedPreferences only once per app session. Every subsequent read returns _colorCache directly, skipping JSON parsing entirely.
2

Isolate-offloaded write

The setter stores the new map in _colorCache immediately (so reads reflect the latest value without waiting for I/O), then calls Isolate.run(() => json.encode(value)) to serialise the potentially large map on a background isolate. The encoded string is written to SharedPreferences only after the isolate completes.
Isolate.run spawns a short-lived isolate for the encoding work, preventing json.encode on a large colour map from blocking the main thread’s frame budget.

ArtworkCacheService

ArtworkCacheService builds and maintains the file-based album artwork cache that JustAudioPlaybackService.loadPlaylist references via artUri: Uri.file('$appDirectory/<albumId>.jpg').
Future<void> buildArtworkCache({
  required List<SongModel> songs,
  required String appDirectory,
  required bool force,
}) async {
  if (!force && _prefs.numberOfSongs == songs.length) return;

  // One artwork file per album; skip albums already cached on disk.
  final seenAlbumIds = <Object?>{};
  final pending = <SongModel>[];
  for (final song in songs) {
    if (!seenAlbumIds.add(song.albumId)) continue;
    if (File('$appDirectory/${song.albumId}.jpg').existsSync()) continue;
    pending.add(song);
  }

  // Bounded worker pool. Dart is single-threaded per isolate, so `next++`
  // between await points is a safe shared cursor — each worker pulls the
  // next album as it finishes, keeping all slots busy.
  int next = 0;
  Future<void> worker() async {
    while (next < pending.length) {
      final song = pending[next++];
      final bytes = await _artwork.queryArtwork(song.id);
      if (bytes == null) continue;
      try {
        await File('$appDirectory/${song.albumId}.jpg').writeAsBytes(bytes);
      } catch (_) {}
    }
  }

  final workerCount = pending.length < _maxConcurrentArtworkFetches
      ? pending.length
      : _maxConcurrentArtworkFetches;
  await Future.wait([for (var i = 0; i < workerCount; i++) worker()]);

  _prefs.isNotFirstTime = true;
}

How caching works

ScenarioBehaviour
First launch (isNotFirstTime == false)Full build: fetches and writes one JPEG per unique album ID.
Library unchanged (numberOfSongs matches)Skipped entirely unless force: true.
force: trueRe-checks every album; only fetches albums whose .jpg file is missing from disk.
Album already cachedFile.existsSync() check skips it even on a forced rebuild.
The worker pool is capped at six concurrent fetches (_maxConcurrentArtworkFetches = 6) to avoid spiking native memory with many simultaneous bitmap decodes. Because Dart is single-threaded per isolate, the shared next++ cursor between await points is a safe way for each worker to claim the next pending album without a mutex.

Build docs developers (and LLMs) love