From 1abcad1de510c209a34196f2de17045af4dd3bc2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 2 Jun 2023 12:33:27 +0600 Subject: [PATCH] fix: linux mpris not showing up and overall media notification service --- .../proxy_playlist_provider.dart | 12 + .../audio_services/audio_services.dart | 21 +- .../audio_services/linux_audio_service.dart | 820 +++++++++++++++--- .../audio_services/windows_audio_service.dart | 2 +- 4 files changed, 753 insertions(+), 102 deletions(-) diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index e0e57389a..f7324457a 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -222,6 +222,8 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier active: initialIndex, ); + await notificationService.addTrack(addableTrack); + await audioPlayer.openPlaylist( state.tracks.map(makeAppropriateSource).toList(), initialIndex: initialIndex, @@ -248,6 +250,10 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } await audioPlayer.jumpTo(index); + if (oldTrack != null || track != null) { + await notificationService.addTrack(track ?? oldTrack!); + } + if (oldTrack != null && track != null) { await storeTrack( oldTrack, @@ -316,6 +322,9 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } await audioPlayer.skipToNext(); + if (oldTrack != null || track != null) { + await notificationService.addTrack(track ?? oldTrack!); + } if (oldTrack != null && track != null) { await storeTrack( oldTrack, @@ -344,6 +353,9 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ); } await audioPlayer.skipToPrevious(); + if (oldTrack != null || track != null) { + await notificationService.addTrack(track ?? oldTrack!); + } if (oldTrack != null && track != null) { await storeTrack( oldTrack, diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index f630712cf..6d6c9d436 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/linux_audio_service.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart'; @@ -37,19 +38,35 @@ class AudioServices { final mpris = DesktopTools.platform.isLinux ? LinuxAudioService(ref, playback) : null; + if (mpris != null) { + playback.addListener((state) { + mpris.player.updateProperties(); + }); + audioPlayer.playerStateStream.listen((state) { + mpris.player.updateProperties(); + }); + audioPlayer.positionStream.listen((state) async { + await mpris.player.emitPropertiesChanged( + "org.mpris.MediaPlayer2.Player", + changedProperties: { + "Position": (await mpris.player.getPosition()).returnValues.first, + }, + ); + }); + } + return AudioServices(mobile, smtc, mpris); } Future addTrack(Track track) async { await smtc?.addTrack(track); - await mpris?.addTrack(track); mobile?.addItem(MediaItem( id: track.id!, album: track.album?.name ?? "", title: track.name!, artist: TypeConversionUtils.artists_X_String(track.artists ?? []), duration: track is SpotubeTrack - ? track.ytTrack.duration! + ? track.ytTrack.duration : Duration(milliseconds: track.durationMs ?? 0), artUri: Uri.parse(TypeConversionUtils.image_X_UrlString( track.album?.images ?? [], diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart index 30bee0a77..661bf5a6f 100644 --- a/lib/services/audio_services/linux_audio_service.dart +++ b/lib/services/audio_services/linux_audio_service.dart @@ -1,116 +1,738 @@ -import 'dart:async'; import 'dart:io'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:mpris_service/mpris_service.dart'; -import 'package:spotify/spotify.dart'; +import 'package:dbus/dbus.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:spotube/provider/dbus_provider.dart'; import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/loop_mode.dart'; -import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:window_manager/window_manager.dart'; -class LinuxAudioService { - late final MPRIS mpris; +class _MprisMediaPlayer2 extends DBusObject { + /// Creates a new object to expose on [path]. + _MprisMediaPlayer2() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) { + dbus.registerObject(this); + } + + void dispose() { + dbus.unregisterObject(this); + } + + /// Gets value of property org.mpris.MediaPlayer2.CanQuit + Future getCanQuit() async { + return DBusMethodSuccessResponse([const DBusBoolean(true)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Fullscreen + Future getFullscreen() async { + return DBusMethodSuccessResponse([const DBusBoolean(false)]); + } + + /// Sets property org.mpris.MediaPlayer2.Fullscreen + Future setFullscreen(bool value) async { + return DBusMethodSuccessResponse(); + } + + /// Gets value of property org.mpris.MediaPlayer2.CanSetFullscreen + Future getCanSetFullscreen() async { + return DBusMethodSuccessResponse([const DBusBoolean(false)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.CanRaise + Future getCanRaise() async { + return DBusMethodSuccessResponse([const DBusBoolean(false)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.HasTrackList + Future getHasTrackList() async { + return DBusMethodSuccessResponse([const DBusBoolean(false)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Identity + Future getIdentity() async { + return DBusMethodSuccessResponse([const DBusString("Spotube")]); + } + + /// Gets value of property org.mpris.MediaPlayer2.DesktopEntry + Future getDesktopEntry() async { + return DBusMethodSuccessResponse( + [const DBusString("/usr/share/application/spotube")], + ); + } + + /// Gets value of property org.mpris.MediaPlayer2.SupportedUriSchemes + Future getSupportedUriSchemes() async { + return DBusMethodSuccessResponse([ + DBusArray.string(["http"]) + ]); + } + + /// Gets value of property org.mpris.MediaPlayer2.SupportedMimeTypes + Future getSupportedMimeTypes() async { + return DBusMethodSuccessResponse([ + DBusArray.string(["audio/mpeg"]) + ]); + } + + /// Implementation of org.mpris.MediaPlayer2.Raise() + Future doRaise() async { + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Quit() + Future doQuit() async { + await windowManager.close(); + return DBusMethodSuccessResponse(); + } + + @override + List introspect() { + return [ + DBusIntrospectInterface('org.mpris.MediaPlayer2', methods: [ + DBusIntrospectMethod('Raise'), + DBusIntrospectMethod('Quit') + ], properties: [ + DBusIntrospectProperty('CanQuit', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('Fullscreen', DBusSignature('b'), + access: DBusPropertyAccess.readwrite), + DBusIntrospectProperty('CanSetFullscreen', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanRaise', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('HasTrackList', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('Identity', DBusSignature('s'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('DesktopEntry', DBusSignature('s'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('SupportedUriSchemes', DBusSignature('as'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('SupportedMimeTypes', DBusSignature('as'), + access: DBusPropertyAccess.read) + ]) + ]; + } + + @override + Future handleMethodCall(DBusMethodCall methodCall) async { + if (methodCall.interface == 'org.mpris.MediaPlayer2') { + if (methodCall.name == 'Raise') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doRaise(); + } else if (methodCall.name == 'Quit') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doQuit(); + } else { + return DBusMethodErrorResponse.unknownMethod(); + } + } else { + return DBusMethodErrorResponse.unknownInterface(); + } + } + + @override + Future getProperty(String interface, String name) async { + if (interface == 'org.mpris.MediaPlayer2') { + if (name == 'CanQuit') { + return getCanQuit(); + } else if (name == 'Fullscreen') { + return getFullscreen(); + } else if (name == 'CanSetFullscreen') { + return getCanSetFullscreen(); + } else if (name == 'CanRaise') { + return getCanRaise(); + } else if (name == 'HasTrackList') { + return getHasTrackList(); + } else if (name == 'Identity') { + return getIdentity(); + } else if (name == 'DesktopEntry') { + return getDesktopEntry(); + } else if (name == 'SupportedUriSchemes') { + return getSupportedUriSchemes(); + } else if (name == 'SupportedMimeTypes') { + return getSupportedMimeTypes(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + @override + Future setProperty( + String interface, String name, DBusValue value) async { + if (interface == 'org.mpris.MediaPlayer2') { + if (name == 'CanQuit') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'Fullscreen') { + if (value.signature != DBusSignature('b')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return setFullscreen((value as DBusBoolean).value); + } else if (name == 'CanSetFullscreen') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanRaise') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'HasTrackList') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'Identity') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'DesktopEntry') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'SupportedUriSchemes') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'SupportedMimeTypes') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + @override + Future getAllProperties(String interface) async { + var properties = {}; + if (interface == 'org.mpris.MediaPlayer2') { + properties['CanQuit'] = (await getCanQuit()).returnValues[0]; + properties['Fullscreen'] = (await getFullscreen()).returnValues[0]; + properties['CanSetFullscreen'] = + (await getCanSetFullscreen()).returnValues[0]; + properties['CanRaise'] = (await getCanRaise()).returnValues[0]; + properties['HasTrackList'] = (await getHasTrackList()).returnValues[0]; + properties['Identity'] = (await getIdentity()).returnValues[0]; + properties['DesktopEntry'] = (await getDesktopEntry()).returnValues[0]; + properties['SupportedUriSchemes'] = + (await getSupportedUriSchemes()).returnValues[0]; + properties['SupportedMimeTypes'] = + (await getSupportedMimeTypes()).returnValues[0]; + } + return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]); + } +} + +class _MprisMediaPlayer2Player extends DBusObject { final Ref ref; final ProxyPlaylistNotifier playlistNotifier; - final subscriptions = []; - - LinuxAudioService(this.ref, this.playlistNotifier) { - MPRIS - .create( - busName: 'org.mpris.MediaPlayer2.spotube', - identity: 'Spotube', - desktopEntry: Platform.resolvedExecutable, - ) - .then((value) => mpris = value) - .then((_) { - mpris.playbackStatus = MPRISPlaybackStatus.stopped; - mpris.setEventHandler(MPRISEventHandler( - loopStatus: (value) async { - audioPlayer.setLoopMode( - PlaybackLoopMode.fromMPRISLoopStatus(value), - ); - }, - next: playlistNotifier.next, - pause: audioPlayer.pause, - play: audioPlayer.resume, - playPause: () async { - if (audioPlayer.isPlaying) { - await audioPlayer.pause(); - } else { - await audioPlayer.resume(); - } - }, - seek: audioPlayer.seek, - shuffle: audioPlayer.setShuffle, - stop: playlistNotifier.stop, - volume: audioPlayer.setVolume, - previous: playlistNotifier.previous, - )); - - final playerStateStream = - audioPlayer.playerStateStream.listen((state) async { - switch (state) { - case AudioPlaybackState.buffering: - case AudioPlaybackState.playing: - mpris.playbackStatus = MPRISPlaybackStatus.playing; - break; - case AudioPlaybackState.paused: - mpris.playbackStatus = MPRISPlaybackStatus.paused; - break; - case AudioPlaybackState.stopped: - case AudioPlaybackState.completed: - mpris.playbackStatus = MPRISPlaybackStatus.stopped; - break; - default: - break; - } - }); - - final positionStream = audioPlayer.positionStream.listen((pos) async { - mpris.position = pos; - }); - - final durationStream = - audioPlayer.durationStream.listen((duration) async { - mpris.metadata = mpris.metadata.copyWith(length: duration); - }); - - subscriptions.addAll([ - playerStateStream, - positionStream, - durationStream, - ]); - }); - } - - Future addTrack(Track track) async { - mpris.metadata = MPRISMetadata( - track is SpotubeTrack ? Uri.parse(track.ytUri) : Uri.parse(track.uri!), - album: track.album?.name ?? "", - albumArtist: [track.album?.artists?.first.name ?? ""], - artUrl: Uri.parse(TypeConversionUtils.image_X_UrlString( - track.album?.images ?? [], - placeholder: ImagePlaceholder.albumArt, - )), - artist: track.artists?.map((e) => e.name!).toList(), - contentCreated: DateTime.tryParse(track.album?.releaseDate ?? ""), - discNumber: track.discNumber, - length: track is SpotubeTrack - ? track.ytTrack.duration! - : Duration(milliseconds: track.durationMs!), - title: track.name!, - trackNumber: track.trackNumber, - ); + /// Creates a new object to expose on [path]. + _MprisMediaPlayer2Player(this.ref, this.playlistNotifier) + : super(DBusObjectPath("/org/mpris/MediaPlayer2")) { + (() async { + final nameStatus = + await dbus.requestName("org.mpris.MediaPlayer2.spotube"); + if (nameStatus == DBusRequestNameReply.exists) { + await dbus.requestName("org.mpris.MediaPlayer2.spotube.instance$pid"); + } + await dbus.registerObject(this); + }()); } + ProxyPlaylist get playlist => playlistNotifier.state; + void dispose() { - mpris.dispose(); - for (var element in subscriptions) { - element.cancel(); + dbus.unregisterObject(this); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.PlaybackStatus + Future getPlaybackStatus() async { + final status = audioPlayer.isPlaying + ? "Playing" + : playlist.active == null + ? "Stopped" + : "Paused"; + return DBusMethodSuccessResponse([DBusString(status)]); + } + + // TODO: Implement Track Loop + + /// Gets value of property org.mpris.MediaPlayer2.Player.LoopStatus + Future getLoopStatus() async { + final loopMode = switch (await audioPlayer.loopMode) { + PlaybackLoopMode.all => "Playlist", + PlaybackLoopMode.one => "Track", + PlaybackLoopMode.none => "None", + }; + + return DBusMethodSuccessResponse([DBusString(loopMode)]); + } + + /// Sets property org.mpris.MediaPlayer2.Player.LoopStatus + Future setLoopStatus(String value) async { + // playlistNotifier.setIsLoop(value == "Track"); + return DBusMethodSuccessResponse(); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.Rate + Future getRate() async { + return DBusMethodSuccessResponse([const DBusDouble(1)]); + } + + /// Sets property org.mpris.MediaPlayer2.Player.Rate + Future setRate(double value) async { + return DBusMethodSuccessResponse(); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle + Future getShuffle() async { + return DBusMethodSuccessResponse( + [DBusBoolean(await audioPlayer.isShuffled)]); + } + + /// Sets property org.mpris.MediaPlayer2.Player.Shuffle + Future setShuffle(bool value) async { + audioPlayer.setShuffle(value); + return DBusMethodSuccessResponse(); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.Metadata + Future getMetadata() async { + if (playlist.activeTrack == null || playlist.isFetching) { + return DBusMethodSuccessResponse([DBusDict.stringVariant({})]); } + final id = playlist.activeTrack!.id; + + return DBusMethodSuccessResponse([ + DBusDict.stringVariant({ + "mpris:trackid": DBusString("${path.value}/Track/$id"), + "mpris:length": DBusInt32( + (await audioPlayer.duration)?.inMicroseconds ?? 0, + ), + "mpris:artUrl": DBusString( + TypeConversionUtils.image_X_UrlString( + playlist.activeTrack?.album?.images, + placeholder: ImagePlaceholder.albumArt, + ), + ), + "xesam:album": DBusString(playlist.activeTrack!.album!.name!), + "xesam:artist": DBusArray.string( + playlist.activeTrack!.artists!.map((artist) => artist.name!), + ), + "xesam:title": DBusString(playlist.activeTrack!.name!), + "xesam:url": DBusString( + playlist.activeTrack is SpotubeTrack + ? (playlist.activeTrack as SpotubeTrack).ytUri + : playlist.activeTrack!.previewUrl!, + ), + "xesam:genre": const DBusString("Unknown"), + }), + ]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.Volume + Future getVolume() async { + return DBusMethodSuccessResponse([DBusDouble(audioPlayer.volume)]); + } + + /// Sets property org.mpris.MediaPlayer2.Player.Volume + Future setVolume(double value) async { + await audioPlayer.setVolume(value); + return DBusMethodSuccessResponse(); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.Position + Future getPosition() async { + return DBusMethodSuccessResponse([ + DBusInt64((await audioPlayer.position)?.inMicroseconds ?? 0), + ]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.MinimumRate + Future getMinimumRate() async { + return DBusMethodSuccessResponse([const DBusDouble(1)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.MaximumRate + Future getMaximumRate() async { + return DBusMethodSuccessResponse([const DBusDouble(1)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.CanGoNext + Future getCanGoNext() async { + return DBusMethodSuccessResponse([ + DBusBoolean( + (playlist.tracks.length) > 1, + ) + ]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.CanGoPrevious + Future getCanGoPrevious() async { + return DBusMethodSuccessResponse([ + DBusBoolean( + (playlist.tracks.length) > 1, + ) + ]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.CanPlay + Future getCanPlay() async { + return DBusMethodSuccessResponse([const DBusBoolean(true)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.CanPause + Future getCanPause() async { + return DBusMethodSuccessResponse([const DBusBoolean(true)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.CanSeek + Future getCanSeek() async { + return DBusMethodSuccessResponse([const DBusBoolean(true)]); + } + + /// Gets value of property org.mpris.MediaPlayer2.Player.CanControl + Future getCanControl() async { + return DBusMethodSuccessResponse([const DBusBoolean(true)]); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.Next() + Future doNext() async { + await playlistNotifier.next(); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.Previous() + Future doPrevious() async { + await playlistNotifier.previous(); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.Pause() + Future doPause() async { + await audioPlayer.pause(); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.PlayPause() + Future doPlayPause() async { + audioPlayer.isPlaying + ? await audioPlayer.pause() + : await audioPlayer.resume(); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.Stop() + Future doStop() async { + playlistNotifier.stop(); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.Play() + Future doPlay() async { + await audioPlayer.resume(); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.Seek() + Future doSeek(int offset) async { + await audioPlayer.seek(Duration(microseconds: offset)); + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.SetPosition() + Future doSetPosition(String TrackId, int Position) async { + return DBusMethodSuccessResponse(); + } + + /// Implementation of org.mpris.MediaPlayer2.Player.OpenUri() + Future doOpenUri(String Uri) async { + return DBusMethodSuccessResponse(); + } + + /// Emits signal org.mpris.MediaPlayer2.Player.Seeked + Future emitSeeked(int position) async { + await emitSignal( + 'org.mpris.MediaPlayer2.Player', + 'Seeked', + [DBusInt64(position)], + ); + } + + Future updateProperties() async { + return emitPropertiesChanged( + "org.mpris.MediaPlayer2.Player", + changedProperties: { + "PlaybackStatus": (await getPlaybackStatus()).returnValues.first, + "LoopStatus": (await getLoopStatus()).returnValues.first, + "Rate": (await getRate()).returnValues.first, + "Shuffle": (await getShuffle()).returnValues.first, + "Metadata": (await getMetadata()).returnValues.first, + "Volume": (await getVolume()).returnValues.first, + "Position": (await getPosition()).returnValues.first, + "MinimumRate": (await getMinimumRate()).returnValues.first, + "MaximumRate": (await getMaximumRate()).returnValues.first, + "CanGoNext": (await getCanGoNext()).returnValues.first, + "CanGoPrevious": (await getCanGoPrevious()).returnValues.first, + "CanPlay": (await getCanPlay()).returnValues.first, + "CanPause": (await getCanPause()).returnValues.first, + "CanSeek": (await getCanSeek()).returnValues.first, + "CanControl": (await getCanControl()).returnValues.first, + }, + ); + } + + @override + List introspect() { + return [ + DBusIntrospectInterface('org.mpris.MediaPlayer2.Player', methods: [ + DBusIntrospectMethod('Next'), + DBusIntrospectMethod('Previous'), + DBusIntrospectMethod('Pause'), + DBusIntrospectMethod('PlayPause'), + DBusIntrospectMethod('Stop'), + DBusIntrospectMethod('Play'), + DBusIntrospectMethod('Seek', args: [ + DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.in_, + name: 'Offset') + ]), + DBusIntrospectMethod('SetPosition', args: [ + DBusIntrospectArgument(DBusSignature('o'), DBusArgumentDirection.in_, + name: 'TrackId'), + DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.in_, + name: 'Position') + ]), + DBusIntrospectMethod('OpenUri', args: [ + DBusIntrospectArgument(DBusSignature('s'), DBusArgumentDirection.in_, + name: 'Uri') + ]) + ], signals: [ + DBusIntrospectSignal('Seeked', args: [ + DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.out, + name: 'Position') + ]) + ], properties: [ + DBusIntrospectProperty('PlaybackStatus', DBusSignature('s'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('LoopStatus', DBusSignature('s'), + access: DBusPropertyAccess.readwrite), + DBusIntrospectProperty('Rate', DBusSignature('d'), + access: DBusPropertyAccess.readwrite), + DBusIntrospectProperty('Shuffle', DBusSignature('b'), + access: DBusPropertyAccess.readwrite), + DBusIntrospectProperty('Metadata', DBusSignature('a{sv}'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('Volume', DBusSignature('d'), + access: DBusPropertyAccess.readwrite), + DBusIntrospectProperty('Position', DBusSignature('x'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('MinimumRate', DBusSignature('d'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('MaximumRate', DBusSignature('d'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanGoNext', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanGoPrevious', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanPlay', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanPause', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanSeek', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('CanControl', DBusSignature('b'), + access: DBusPropertyAccess.read) + ]) + ]; + } + + @override + Future handleMethodCall(DBusMethodCall methodCall) async { + if (methodCall.interface == 'org.mpris.MediaPlayer2.Player') { + if (methodCall.name == 'Next') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doNext(); + } else if (methodCall.name == 'Previous') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doPrevious(); + } else if (methodCall.name == 'Pause') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doPause(); + } else if (methodCall.name == 'PlayPause') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doPlayPause(); + } else if (methodCall.name == 'Stop') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doStop(); + } else if (methodCall.name == 'Play') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doPlay(); + } else if (methodCall.name == 'Seek') { + if (methodCall.signature != DBusSignature('x')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doSeek((methodCall.values[0] as DBusInt64).value); + } else if (methodCall.name == 'SetPosition') { + if (methodCall.signature != DBusSignature('ox')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doSetPosition((methodCall.values[0] as DBusObjectPath).value, + (methodCall.values[1] as DBusInt64).value); + } else if (methodCall.name == 'OpenUri') { + if (methodCall.signature != DBusSignature('s')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return doOpenUri((methodCall.values[0] as DBusString).value); + } else { + return DBusMethodErrorResponse.unknownMethod(); + } + } else { + return DBusMethodErrorResponse.unknownInterface(); + } + } + + @override + Future getProperty(String interface, String name) async { + if (interface == 'org.mpris.MediaPlayer2.Player') { + if (name == 'PlaybackStatus') { + return getPlaybackStatus(); + } else if (name == 'LoopStatus') { + return getLoopStatus(); + } else if (name == 'Rate') { + return getRate(); + } else if (name == 'Shuffle') { + return getShuffle(); + } else if (name == 'Metadata') { + return getMetadata(); + } else if (name == 'Volume') { + return getVolume(); + } else if (name == 'Position') { + return getPosition(); + } else if (name == 'MinimumRate') { + return getMinimumRate(); + } else if (name == 'MaximumRate') { + return getMaximumRate(); + } else if (name == 'CanGoNext') { + return getCanGoNext(); + } else if (name == 'CanGoPrevious') { + return getCanGoPrevious(); + } else if (name == 'CanPlay') { + return getCanPlay(); + } else if (name == 'CanPause') { + return getCanPause(); + } else if (name == 'CanSeek') { + return getCanSeek(); + } else if (name == 'CanControl') { + return getCanControl(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + @override + Future setProperty( + String interface, String name, DBusValue value) async { + if (interface == 'org.mpris.MediaPlayer2.Player') { + if (name == 'PlaybackStatus') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'LoopStatus') { + if (value.signature != DBusSignature('s')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return setLoopStatus((value as DBusString).value); + } else if (name == 'Rate') { + if (value.signature != DBusSignature('d')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return setRate((value as DBusDouble).value); + } else if (name == 'Shuffle') { + if (value.signature != DBusSignature('b')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return setShuffle((value as DBusBoolean).value); + } else if (name == 'Metadata') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'Volume') { + if (value.signature != DBusSignature('d')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return setVolume((value as DBusDouble).value); + } else if (name == 'Position') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'MinimumRate') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'MaximumRate') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanGoNext') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanGoPrevious') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanPlay') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanPause') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanSeek') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'CanControl') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + @override + Future getAllProperties(String interface) async { + var properties = {}; + if (interface == 'org.mpris.MediaPlayer2.Player') { + properties['PlaybackStatus'] = + (await getPlaybackStatus()).returnValues[0]; + properties['LoopStatus'] = (await getLoopStatus()).returnValues[0]; + properties['Rate'] = (await getRate()).returnValues[0]; + properties['Shuffle'] = (await getShuffle()).returnValues[0]; + properties['Metadata'] = (await getMetadata()).returnValues[0]; + properties['Volume'] = (await getVolume()).returnValues[0]; + properties['Position'] = (await getPosition()).returnValues[0]; + properties['MinimumRate'] = (await getMinimumRate()).returnValues[0]; + properties['MaximumRate'] = (await getMaximumRate()).returnValues[0]; + properties['CanGoNext'] = (await getCanGoNext()).returnValues[0]; + properties['CanGoPrevious'] = (await getCanGoPrevious()).returnValues[0]; + properties['CanPlay'] = (await getCanPlay()).returnValues[0]; + properties['CanPause'] = (await getCanPause()).returnValues[0]; + properties['CanSeek'] = (await getCanSeek()).returnValues[0]; + properties['CanControl'] = (await getCanControl()).returnValues[0]; + } + return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]); + } +} + +class LinuxAudioService { + _MprisMediaPlayer2 mp2; + _MprisMediaPlayer2Player player; + + LinuxAudioService(Ref ref, ProxyPlaylistNotifier playlistNotifier) + : mp2 = _MprisMediaPlayer2(), + player = _MprisMediaPlayer2Player(ref, playlistNotifier); + + void dispose() { + mp2.dispose(); + player.dispose(); } } diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index 15e45656e..4481140b8 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -84,7 +84,7 @@ class WindowsAudioService { } await smtc.updateMetadata(MusicMetadata( title: track.name!, - albumArtist: track.artists?.first.name ?? "Unknown", + albumArtist: track.artists?.firstOrNull?.name ?? "Unknown", artist: TypeConversionUtils.artists_X_String(track.artists ?? []), album: track.album?.name ?? "Unknown", thumbnail: TypeConversionUtils.image_X_UrlString(