diff --git a/README.md b/README.md index 72ca90e..b877b46 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # mpris-java -Simple MPRIS 2 implementation with Kotlin for Java. -Code is mainly done, but largely untested. No guarantees for it to work. +Simple [MPRIS 2.2](https://specifications.freedesktop.org/mpris-spec/2.2/) implementation with Kotlin for the JVM. +Code is basically done, but largely untested. No guarantees for it to work. -Look into extensions/test/xerus.mpris.MPRISPlayer for a test implementation. +A test implementation can be found in [extensions/test/xerus/mpris/MPRISPlayer.kt](extensions/test/xerus/mpris/MPRISPlayer.kt). -It has two modules, the project itself and mpris-extensions, -which is meant to ease the use of this library by providing reference -implementations and abstractions. \ No newline at end of file +The project is separated into two modules, the base/root module and [mpris-extensions](extensions), which is meant to ease the use of this library by providing reference implementations and abstractions. + +The base module should be compatible with any JVM language, while the extensions module is specifically fitted to Kotlin. \ No newline at end of file diff --git a/extensions/build.gradle.kts b/extensions/build.gradle.kts index fe18eae..2865034 100644 --- a/extensions/build.gradle.kts +++ b/extensions/build.gradle.kts @@ -7,7 +7,7 @@ sourceSets.main.get().java.srcDir("src") sourceSets.main.get().java.srcDir("test") application { - mainClassName = "xerus.mpris.DBusPropertyDelegateKt" + mainClassName = "xerus.mpris.MPRISPlayerKt" } dependencies { diff --git a/extensions/src/xerus/mpris/AbstractMPRISPlayer.kt b/extensions/src/xerus/mpris/AbstractMPRISPlayer.kt index e4853cc..c669629 100644 --- a/extensions/src/xerus/mpris/AbstractMPRISPlayer.kt +++ b/extensions/src/xerus/mpris/AbstractMPRISPlayer.kt @@ -25,6 +25,9 @@ abstract class AbstractMPRISPlayer: MediaPlayerX, PlayerX, DefaultDBus { propertyListeners[property_name]?.invoke(value) } + /** Requests the bus name for this player and exports it, so that it can be called from DBus. + * + * Blocks the current Thread as long as the object is exported to DBus. */ fun exportAs(playerName: String) { connection.requestBusName("org.mpris.MediaPlayer2.$playerName") connection.exportObject("/org/mpris/MediaPlayer2", this) @@ -74,6 +77,7 @@ interface PlayerX: Player { } +/** Extension of the [MediaPlayer2] interface which adds its properties. */ interface MediaPlayerX: MediaPlayer2 { val supportedUriSchemes: Array val supportedMimeTypes: Array diff --git a/extensions/src/xerus/mpris/DBusPropertyDelegate.kt b/extensions/src/xerus/mpris/DBusPropertyDelegate.kt index fb41b71..436920b 100644 --- a/extensions/src/xerus/mpris/DBusPropertyDelegate.kt +++ b/extensions/src/xerus/mpris/DBusPropertyDelegate.kt @@ -4,16 +4,12 @@ package xerus.mpris import javafx.beans.value.ObservableValue import org.freedesktop.dbus.DBusInterfaceName -import org.mpris.MediaPlayer2.PlaylistOrdering import org.slf4j.LoggerFactory import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KClass import kotlin.reflect.KProperty -fun main(args: Array) { - println(arrayOf(PlaylistOrdering.Alphabetical).variant()) -} - val logger = LoggerFactory.getLogger("xerus.mpris.properties") private fun findInterface(clazz: Class<*>, name: String): Class<*>? = @@ -50,9 +46,15 @@ class DBusProperty(private val initial: T, private val observable: Obser } -class DBusMapProperty(private val initial: Map = HashMap(), private val observable: ObservableValue>? = null, private val onSet: ((Map) -> Unit)? = null) { +class DBusMapProperty( + private val keyClass: KClass, + private val valueClass: KClass, + private val initial: Map = HashMap(), + private val observable: ObservableValue>? = null, + private val onSet: ((Map) -> Unit)? = null) { - constructor(observable: ObservableValue>, onSet: ((Map) -> Unit)? = null): this(observable.value, observable, onSet) + constructor(keyClass: KClass, valueClass: KClass, observable: ObservableValue>, onSet: ((Map) -> Unit)? = null): + this(keyClass, valueClass, observable.value, observable, onSet) operator fun provideDelegate( thisRef: AbstractMPRISPlayer, @@ -64,7 +66,7 @@ class DBusMapProperty(private val initial: Map = HashMap(), pr val interfaceName = (clazz.annotations.find { it is DBusInterfaceName } as? DBusInterfaceName)?.value ?: clazz.name logger.trace("Registered Property ${prop.name} for $interfaceName") - //thisRef.properties.getOrPut(interfaceName) { HashMap() }[name] = initial.variant() + thisRef.properties.getOrPut(interfaceName) { HashMap() }[name] = initial.variant(keyClass, valueClass) val property = Property>(interfaceName, name) if(onSet != null) thisRef.propertyListeners[name] = onSet as ((Any) -> Unit) @@ -94,8 +96,14 @@ class DBusConstant(private val value: T) { private class Property(private val interfaceName: String, private val name: String): ReadWriteProperty { override fun setValue(thisRef: AbstractMPRISPlayer, property: KProperty<*>, value: T) { - if(thisRef.properties.getValue(interfaceName).put(name, value.variant())?.value != value) - thisRef.connection.sendSignal(thisRef.propertyChanged(interfaceName, name)) + println("setting $name in $interfaceName to $value") + try { + if(thisRef.properties.getValue(interfaceName).put(name, value.variant())?.value != value) + thisRef.connection.sendSignal(thisRef.propertyChanged(interfaceName, name)) + } catch(t: Throwable) { + t.printStackTrace() + } + println("set $value") } override fun getValue(thisRef: AbstractMPRISPlayer, property: KProperty<*>) = diff --git a/extensions/src/xerus/mpris/DefaultDBus.kt b/extensions/src/xerus/mpris/DefaultDBus.kt index e065748..8bb03b7 100644 --- a/extensions/src/xerus/mpris/DefaultDBus.kt +++ b/extensions/src/xerus/mpris/DefaultDBus.kt @@ -5,18 +5,24 @@ import org.freedesktop.dbus.Variant import org.freedesktop.dbus.types.DBusMapType import java.security.InvalidParameterException import java.util.* +import kotlin.reflect.KClass fun Any.variant(): Variant<*> { if(this is Variant<*>) return this if(this is Map<*, *>) - throw InvalidParameterException("Use Map.variant for Maps!") + throw InvalidParameterException("Map.variant has to be used for Maps") return Variant(this) } inline fun Map.variant() = Variant(this, DBusMapType(K::class.java, V::class.java)) +fun Map.variant( + keyClass: KClass, + valueClass: KClass) = + Variant(this, DBusMapType(keyClass.java, valueClass.java)) + interface DefaultDBus: Properties { override fun isRemote() = false @@ -26,12 +32,12 @@ interface DefaultDBus: Properties { GetAll(interface_name)[property_name]?.value as A override fun Set(interface_name: String, property_name: String, value: A) { - GetAll(interface_name).put(property_name, value.variant()) + GetAll(interface_name)[property_name] = value.variant() propertyChanged(interface_name, property_name) } /** returns a new [Properties.PropertiesChanged] signal */ fun propertyChanged(interface_name: String, property_name: String) = - Properties.PropertiesChanged(objectPath, interface_name, Collections.singletonMap(property_name, Get(interface_name, property_name).variant()) as Map>, null) + Properties.PropertiesChanged(objectPath, interface_name, Collections.singletonMap>(property_name, Get(interface_name, property_name).variant()), null) } diff --git a/extensions/src/xerus/mpris/PropertyMap.kt b/extensions/src/xerus/mpris/PropertyMap.kt index 7502461..8fa3290 100644 --- a/extensions/src/xerus/mpris/PropertyMap.kt +++ b/extensions/src/xerus/mpris/PropertyMap.kt @@ -3,25 +3,25 @@ package xerus.mpris import org.freedesktop.dbus.Variant import java.util.* -class PropertyMap private constructor(initial: PropertyMap.() -> Unit, private val map: HashMap>): Map> by map { +class PropertyMap private constructor(private val map: MutableMap>): MutableMap> by map { - @JvmOverloads - constructor(initial: PropertyMap.() -> Unit = {}): this(initial, HashMap()) + constructor(initializer: PropertyMap.() -> Unit): this(HashMap()) { + initializer(this) + } + + constructor(vararg elements: Pair>): this(mutableMapOf(*elements)) - override val entries: Set>> + override val entries: MutableSet>> get() = refresh().entries - override val values: Collection> + override val values: MutableCollection> get() = refresh().values override fun containsValue(value: Variant<*>) = refresh().containsValue(value) private val mapCallable = HashMap Any?>() - init { - initial(this) - } - fun refresh(): Map> { + fun refresh(): MutableMap> { mapCallable.forEach { key, cal -> refresh(key, cal) } return map } diff --git a/extensions/src/xerus/mpris/Track.kt b/extensions/src/xerus/mpris/Track.kt index 1e59e51..b0122f9 100644 --- a/extensions/src/xerus/mpris/Track.kt +++ b/extensions/src/xerus/mpris/Track.kt @@ -2,14 +2,16 @@ package xerus.mpris import org.freedesktop.dbus.Variant +/** Represents a Track with Metadata. + * This is not an actual DBus-object, but rather a convenience wrapper around the metadata. */ interface Track { - /** the TrackID, must be a valid DBus-Path */ + /** The TrackID, must be a valid DBus-Path. */ val id: String - get() = metadata["mpris:trackid"]!!.value as String - /** Metadata of the Track */ - val metadata: Map?> + get() = metadata.getValue("mpris:trackid").value as String + /** Metadata of the Track. */ + val metadata: Map> } -class SimpleTrack(override val metadata: Map?>): Track { +class SimpleTrack(override val metadata: Map>): Track { constructor(createMetadata: PropertyMap.() -> Unit): this(PropertyMap(createMetadata)) } diff --git a/extensions/test/xerus/mpris/MPRISPlayer.kt b/extensions/test/xerus/mpris/MPRISPlayer.kt index c5d07cd..1f57993 100644 --- a/extensions/test/xerus/mpris/MPRISPlayer.kt +++ b/extensions/test/xerus/mpris/MPRISPlayer.kt @@ -1,30 +1,36 @@ package xerus.mpris +import org.freedesktop.DBus import org.freedesktop.DBus.Properties.PropertiesChanged import org.freedesktop.dbus.Variant +import org.freedesktop.dbus.types.DBusMapType import org.mpris.MediaPlayer2.LoopStatus +import org.mpris.MediaPlayer2.MediaPlayer2 import org.mpris.MediaPlayer2.PlaybackStatus +import org.mpris.MediaPlayer2.Player +import java.io.File import java.util.* -operator fun HashMap.set(k: K, value: V) = put(k, value) +val playerName = "TestPlayer" +val cover = "file://" + File("extensions/test/cover.png").absoluteFile.toString() -fun main(args: Array) { +fun main() { println("Connecting to DBus") - MPRISPlayer().exportAs("TestPlayer") + MPRISPlayer().exportAs(playerName) } -class MPRISPlayer: AbstractMPRISPlayer() { +class MPRISPlayer: AbstractMPRISPlayer(), MediaPlayer2, Player, DBus.Properties { override var playbackStatus by DBusProperty(PlaybackStatus.Stopped) override var loopStatus: LoopStatus = LoopStatus.None override var rate by DBusProperty(1.0) override var shuffle: Boolean = false - override val metadata by DBusMapProperty(PropertyMap { - put("mpris:trackid", "/playerfx/songs/untiltheend") - put("mpris:length", 10_000000) - put("mpris:artUrl", "file:///home/janek/daten/musik/Monstercat/Aero Chord - Love & Hate EP/cover.jpeg") + override val metadata by DBusMapProperty(String::class, Variant::class, PropertyMap { + put("mpris:trackid", "/playerfx/songs/monstercat") + put("mpris:length", 10_000_000) + put("mpris:artUrl", cover) put("xesam:artist", arrayOf("Aero Chord", "Fractal")) - put("xesam:title", "Until The End (feat. Q'AILA)") + put("xesam:title", "Monstercat") }) override var volume: Double get() = TODO("not implemented") @@ -71,7 +77,6 @@ class MPRISPlayer: AbstractMPRISPlayer() { init { println("Constructing MPRISPlayer") - /* properties["org.mpris.MediaPlayer2"] = PropertyMap { put("CanSeek", true) put("CanQuit", true) @@ -82,7 +87,7 @@ class MPRISPlayer: AbstractMPRISPlayer() { put("SupportedMimeTypes", arrayOf("audio/mpeg", "audio/mp4")) //put("DesktopEntry") } - + properties["org.mpris.MediaPlayer2.Player"] = PropertyMap { put("PlaybackStatus", PlaybackStatus.Stopped) put("LoopStatus", LoopStatus.None) @@ -100,7 +105,6 @@ class MPRISPlayer: AbstractMPRISPlayer() { put("CanSeek", true) put("CanControl", true) } - */ println("MPRISPlayer constructed") } @@ -119,8 +123,10 @@ class MPRISPlayer: AbstractMPRISPlayer() { } fun updateStatus(status: PlaybackStatus) { + println(status) playbackStatus = status - updateProperty("org.mpris.MediaPlayer2.Player", "PlaybackStatus", status.name) + println(status) + updateProperty("org.mpris.MediaPlayer2.$playerName", "PlaybackStatus", status.name) } override fun PlayPause() {