import 'package:dart_mpd/dart_mpd.dart' as api; import 'package:flutter_ics_homescreen/export.dart'; import 'playlist_notifier.dart'; class ArtStateEntry { bool reading = false; bool read = false; ArtStateEntry({required this.reading, required this.read}); } class MpdClient { final MpdConfig config; final Ref ref; late api.MpdClient eventClient; late api.MpdClient client; Map artState = {}; MpdClient({required this.config, required this.ref}) { debugPrint("Connecting to MPD at ${config.hostname}:${config.port}"); client = api.MpdClient( connectionDetails: api.MpdConnectionDetails( host: config.hostname, port: config.port, timeout: const Duration(minutes: 2)), onConnect: () => handleConnect(), onDone: () => handleDone(), onError: (e, st) => handleError(e, st)); // Second client instance to keep running in idle state to receive // events eventClient = api.MpdClient( connectionDetails: api.MpdConnectionDetails( host: config.hostname, port: config.port, timeout: const Duration(minutes: 2)), onConnect: () => handleConnect(), onDone: () => handleDone(), onError: (e, st) => handleError(e, st)); } void run() async { var idleEvents = { api.MpdSubsystem.database, api.MpdSubsystem.playlist, api.MpdSubsystem.player }; bool done = false; while (!done) { //debugPrint("Calling MPD idle!"); var events = await eventClient.idle(idleEvents); for (var event in events) { switch (event) { case api.MpdSubsystem.database: //debugPrint("Got MPD database event"); await handleDatabaseEvent(); break; case api.MpdSubsystem.playlist: //debugPrint("Got MPD queue event"); await handleQueueEvent(); break; case api.MpdSubsystem.player: //debugPrint("Got MPD player event"); await handlePlayerEvent(); break; default: break; } } } } void handleConnect() { debugPrint("Connected to MPD!"); } void handleResponse(api.MpdResponse response) { debugPrint('Got MPD response $response'); } void handleDone() { debugPrint('Got MPD done!'); } void handleError(Object e, StackTrace st) { debugPrint('ERROR:\n$st'); } // Idle state event handlers Future handleDatabaseEvent() async { eventClient.clear(); eventClient.add("/"); } Future handleQueueEvent() async { ref.read(playlistProvider.notifier).clear(); artState.clear(); ref.read(playlistArtProvider.notifier).clear(); ref.read(mediaPlayerStateProvider.notifier).reset(); var songs = await eventClient.playlistinfo(); for (var song in songs) { //debugPrint("Got song ${song.title} - ${song.artist} at pos ${song.pos}"); int position = 0; if (song.pos != null) { position = song.pos!; } else { //debugPrint("WARNING: song has no position in queue, ignoring"); continue; } String title = ""; if (song.title != null) { title = song.title!.join(" "); } else { // Just use filename title = song.file; } String album = ""; if (song.album != null) { album = song.album!.join(" "); } String artist = ""; if (song.artist != null) { artist = song.artist!.join(" "); } Duration duration = Duration.zero; if (song.duration != null) { duration = song.duration!; } //debugPrint( // "Got playlist entry \"$title\" - \"$album\" - \"$artist\" at $position"); if (position == 0) { ref.read(mediaPlayerStateProvider.notifier).updateCurrent(PlaylistEntry( title: title, album: album, artist: artist, file: song.file, duration: duration, position: position)); if (song.file.isNotEmpty) { readSongArt(position, song.file); } } ref.read(playlistProvider.notifier).add(PlaylistEntry( title: title, album: album, artist: artist, file: song.file, duration: duration, position: position)); } } Future handlePlayerEvent() async { String songFile = ""; int songPosition = -1; var song = await eventClient.currentsong(); if (song != null) { //debugPrint( // "Player event: song ${song.title} - ${song.artist} at pos ${song.pos}"); if (song.pos != null) { songPosition = song.pos!; } if (songPosition < 0) { debugPrint("WARNING: song has no position in queue, ignoring"); return; } String title = ""; if (song.title != null) { title = song.title!.first; } else { // Just use filename title = song.file; } String album = ""; if (song.album != null) { album = song.album!.first; } String artist = ""; if (song.artist != null) { artist = song.artist!.first; } Duration duration = Duration.zero; if (song.duration != null) { duration = song.duration!; } songFile = song.file; //debugPrint( // "Got song \"$title\" - \"$album\" - \"$artist\" at $songPosition, file \"$songFile\""); ref.read(mediaPlayerStateProvider.notifier).updateCurrent(PlaylistEntry( title: title, album: album, artist: artist, file: songFile, duration: duration, position: songPosition)); } var status = await eventClient.status(); if (status.elapsed != null) { //debugPrint("Using elapsed time ${status.elapsed} s"); ref .read(mediaPlayerPositionProvider.notifier) .set(Duration(milliseconds: (status.elapsed! * 1000.0).toInt())); } PlayState playState = PlayState.stopped; if (status.state != null) { switch (status.state!) { case api.MpdState.stop: //debugPrint("status.state = stop"); playState = PlayState.stopped; ref.read(mediaPlayerPositionProvider.notifier).pause(); break; case api.MpdState.play: //debugPrint("status.state = play"); playState = PlayState.playing; ref.read(mediaPlayerPositionProvider.notifier).play(); break; case api.MpdState.pause: //debugPrint("status.state = paused"); playState = PlayState.paused; ref.read(mediaPlayerPositionProvider.notifier).pause(); break; default: break; } ref.read(mediaPlayerStateProvider.notifier).updatePlayState(playState); } if (playState != PlayState.playing) { // No need to attempt to load art, exit return; } if (!artState.containsKey(songPosition) || !(artState[songPosition]!.read || artState[songPosition]!.reading)) { if (songFile.isNotEmpty) { readSongArt(songPosition, songFile); } else { // Do not attempt any fallback for providing the art for now debugPrint("No file for position $songPosition, no art"); artState[songPosition] = ArtStateEntry(reading: false, read: true); ref .read(playlistArtProvider.notifier) .update(songPosition, Uint8List(0)); } } } void readSongArt(int position, String file) async { int offset = 0; int size = 0; List bytes = []; if (artState.containsKey(position)) { if (artState[position]!.reading) { return; } else { //debugPrint("Reading art for position $position"); artState[position]!.reading = true; } } else { artState[position] = ArtStateEntry(reading: true, read: false); } debugPrint("Reading art for \"$file\""); // Work around dart_mpd not escaping spaced strings itself String escapedFile = file.replaceAll(RegExp(r" "), "\\ "); escapedFile = "\"$file\""; bool first = true; do { //debugPrint("Reading, offset = $offset, size = $size"); var chunk = await client.readpicture(escapedFile, offset); if (chunk != null) { if (chunk.size != null) { if (chunk.size == 0) { if (first) { // No art, exit break; } } size = chunk.size!; } // else unexpected error if (chunk.bytes.isNotEmpty) { //debugPrint("Got ${chunk.bytes.length} bytes of album art"); bytes = bytes + chunk.bytes; offset += chunk.bytes.length; } else { break; } } } while (offset < size); //debugPrint("Done, offset = $offset, size = $size"); if (offset == size) { debugPrint("Read $size bytes of album art for $file"); } else { // else error, leave art empty bytes.clear(); } artState[position]!.read = true; artState[position]!.reading = false; ref .read(playlistArtProvider.notifier) .update(position, Uint8List.fromList(bytes)); } // Player commands void play() async { var playState = ref.read(mediaPlayerStateProvider .select((mediaplayer) => mediaplayer.playState)); if (playState == PlayState.stopped) { int position = ref.read(mediaPlayerStateProvider .select((mediaplayer) => mediaplayer.playlistPosition)); //debugPrint("Calling MPD play, position = $position"); if (position >= 0) { client.play(position); } } else if (playState == PlayState.paused) { client.pause(false); } } void pause() async { if (ref.read(mediaPlayerStateProvider .select((mediaplayer) => mediaplayer.playState)) == PlayState.playing) { client.pause(true); } } void next() async { if (ref.read(mediaPlayerStateProvider .select((mediaplayer) => mediaplayer.playState)) == PlayState.playing) { client.next(); } } void previous() async { if (ref.read(mediaPlayerStateProvider .select((mediaplayer) => mediaplayer.playState)) == PlayState.playing) { client.previous(); } } void seek(int milliseconds) async { client.seekcur((milliseconds / 1000.0).toString()); } void fastForward(int milliseconds) async { if (milliseconds > 0) { client.seekcur("+${(milliseconds / 1000.0).toString()}"); } } void rewind(int milliseconds) async { if (milliseconds > 0) { client.seekcur("-${(milliseconds / 1000.0).toString()}"); } } void pickTrack(int position) async { if (position >= 0) { client.play(position); } } void loopPlaylist(bool loop) async {} }