import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:dbus/dbus.dart'; import 'package:spotube/provider/dbus_provider.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/playback_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; 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("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 { appWindow.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 { Playback playback; /// Creates a new object to expose on [path]. _MprisMediaPlayer2Player({ required this.playback, }) : 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); }()); } void dispose() { dbus.unregisterObject(this); } /// Gets value of property org.mpris.MediaPlayer2.Player.PlaybackStatus Future getPlaybackStatus() async { final status = playback.isPlaying ? "Playing" : playback.playlist == null ? "Stopped" : "Paused"; return DBusMethodSuccessResponse([DBusString(status)]); } /// Gets value of property org.mpris.MediaPlayer2.Player.LoopStatus Future getLoopStatus() async { return DBusMethodSuccessResponse([ playback.isLoop ? const DBusString("Track") : const DBusString("None"), ]); } /// Sets property org.mpris.MediaPlayer2.Player.LoopStatus Future setLoopStatus(String value) async { playback.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(playback.isShuffled)]); } /// Sets property org.mpris.MediaPlayer2.Player.Shuffle Future setShuffle(bool value) async { playback.setIsShuffled(value); return DBusMethodSuccessResponse(); } /// Gets value of property org.mpris.MediaPlayer2.Player.Metadata Future getMetadata() async { try { if (playback.track == null) { return DBusMethodSuccessResponse([DBusDict.stringVariant({})]); } final id = (playback.playlist != null ? playback.playlist!.tracks.indexWhere( (track) => playback.track!.id == track.id!, ) : 0) .abs(); return DBusMethodSuccessResponse([ DBusDict.stringVariant({ "mpris:trackid": DBusString("${path.value}/Track/$id"), "mpris:length": DBusInt32(playback.currentDuration.inMicroseconds), "mpris:artUrl": DBusString( TypeConversionUtils.image_X_UrlString( playback.track?.album?.images, placeholder: ImagePlaceholder.albumArt, ), ), "xesam:album": DBusString(playback.track!.album!.name!), "xesam:artist": DBusArray.string( playback.track!.artists!.map((artist) => artist.name!), ), "xesam:title": DBusString(playback.track!.name!), "xesam:url": DBusString( playback.track is SpotubeTrack ? (playback.track as SpotubeTrack).ytUri : playback.track!.previewUrl!, ), "xesam:genre": const DBusString("Unknown"), }), ]); } catch (e) { print("[DBUS ERROR] $e"); rethrow; } } /// Gets value of property org.mpris.MediaPlayer2.Player.Volume Future getVolume() async { return DBusMethodSuccessResponse([DBusDouble(playback.volume)]); } /// Sets property org.mpris.MediaPlayer2.Player.Volume Future setVolume(double value) async { playback.setVolume(value); return DBusMethodSuccessResponse(); } /// Gets value of property org.mpris.MediaPlayer2.Player.Position Future getPosition() async { return DBusMethodSuccessResponse([ DBusInt64((await playback.player.getDuration())?.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( playback.playlist?.tracks.isNotEmpty == true, ) ]); } /// Gets value of property org.mpris.MediaPlayer2.Player.CanGoPrevious Future getCanGoPrevious() async { return DBusMethodSuccessResponse([ DBusBoolean( playback.playlist?.tracks.isNotEmpty == true, ) ]); } /// 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 { playback.seekForward(); return DBusMethodSuccessResponse(); } /// Implementation of org.mpris.MediaPlayer2.Player.Previous() Future doPrevious() async { playback.seekBackward(); return DBusMethodSuccessResponse(); } /// Implementation of org.mpris.MediaPlayer2.Player.Pause() Future doPause() async { playback.pause(); return DBusMethodSuccessResponse(); } /// Implementation of org.mpris.MediaPlayer2.Player.PlayPause() Future doPlayPause() async { playback.isPlaying ? playback.pause() : playback.resume(); return DBusMethodSuccessResponse(); } /// Implementation of org.mpris.MediaPlayer2.Player.Stop() Future doStop() async { playback.stop(); return DBusMethodSuccessResponse(); } /// Implementation of org.mpris.MediaPlayer2.Player.Play() Future doPlay() async { playback.resume(); return DBusMethodSuccessResponse(); } /// Implementation of org.mpris.MediaPlayer2.Player.Seek() Future doSeek(int offset) async { playback.seekPosition(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(Playback playback) async { this.playback = playback; 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(Playback playback) : mp2 = _MprisMediaPlayer2(), player = _MprisMediaPlayer2Player(playback: playback); void dispose() { mp2.dispose(); player.dispose(); } }