diff options
Diffstat (limited to 'lib/data/data_providers/mpd_client.dart')
-rw-r--r-- | lib/data/data_providers/mpd_client.dart | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/lib/data/data_providers/mpd_client.dart b/lib/data/data_providers/mpd_client.dart new file mode 100644 index 0000000..98d8918 --- /dev/null +++ b/lib/data/data_providers/mpd_client.dart @@ -0,0 +1,378 @@ +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<int, ArtStateEntry> 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>{ + 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<int> 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 {} +} |