From 6d8e647f123e54ba389c5ab2fe908200aa5e4cf6 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Wed, 25 Jan 2023 22:46:42 +0000 Subject: [PATCH] feat: mpris support Resolves #25. Completely refactors the MPD module to be the 'music' module. This now supports both MPD and MPRIS with the same UI for both. BREAKING CHANGE: The `mpd` module has been renamed to `music`. You will need to update the `type` value in your config and add `player_type` to continue using MPD. You will also need to update your styles. --- Cargo.lock | 271 ++++++++++++++++++++++----- Cargo.toml | 3 +- docs/_Sidebar.md | 2 +- docs/modules/MPD.md | 131 ------------- docs/modules/Music.md | 139 ++++++++++++++ src/bar.rs | 2 +- src/clients/mod.rs | 2 +- src/clients/mpd.rs | 167 ----------------- src/clients/music/mod.rs | 66 +++++++ src/clients/music/mpd.rs | 282 ++++++++++++++++++++++++++++ src/clients/music/mpris.rs | 283 ++++++++++++++++++++++++++++ src/config/mod.rs | 4 +- src/modules/mod.rs | 2 +- src/modules/{mpd.rs => music.rs} | 307 ++++++++++++++++--------------- 14 files changed, 1165 insertions(+), 496 deletions(-) delete mode 100644 docs/modules/MPD.md create mode 100644 docs/modules/Music.md delete mode 100644 src/clients/mpd.rs create mode 100644 src/clients/music/mod.rs create mode 100644 src/clients/music/mpd.rs create mode 100644 src/clients/music/mpris.rs rename src/modules/{mpd.rs => music.rs} (60%) diff --git a/Cargo.lock b/Cargo.lock index 3f9fbea..1625420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,8 +141,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -158,8 +158,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -456,9 +456,9 @@ dependencies = [ "codespan-reporting", "once_cell", "proc-macro2", - "quote", + "quote 1.0.21", "scratch", - "syn", + "syn 1.0.105", ] [[package]] @@ -474,8 +474,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote 1.0.21", + "strsim", + "syn 1.0.105", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote 1.0.21", + "syn 1.0.105", +] + +[[package]] +name = "dbus" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", ] [[package]] @@ -485,8 +531,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", +] + +[[package]] +name = "derive_is_enum_variant" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" +dependencies = [ + "heck 0.3.3", + "quote 0.3.15", + "syn 0.11.11", ] [[package]] @@ -540,6 +597,17 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "enum-kinds" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" +dependencies = [ + "proc-macro2", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "enumflags2" version = "0.7.5" @@ -557,8 +625,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -608,6 +676,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "from_variants" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf36180ca6f3c021e91b194e16b670ef5cbdd0cea48354ff6f5f83e3c2d1629" +dependencies = [ + "from_variants_impl", +] + +[[package]] +name = "from_variants_impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13abfd95d43eabb051a8d4b408ef92dfe6d8d4aa17651e5786d5c761e5e6e7ad" +dependencies = [ + "darling", + "proc-macro2", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "futures-channel" version = "0.3.25" @@ -662,8 +757,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -841,12 +936,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", - "heck", + "heck 0.4.0", "proc-macro-crate", "proc-macro-error", "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -949,8 +1044,8 @@ dependencies = [ "proc-macro-crate", "proc-macro-error", "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -959,6 +1054,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.4.0" @@ -1004,6 +1108,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indenter" version = "0.3.3" @@ -1065,6 +1175,7 @@ dependencies = [ "lazy_static", "libcorn", "mpd_client", + "mpris", "notify", "regex", "serde", @@ -1146,6 +1257,15 @@ dependencies = [ "toml", ] +[[package]] +name = "libdbus-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1278,6 +1398,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "mpris" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3d377fd27d9d5c7145341cd3affcb83839c24c73e7460488b3ae0a3f9c5166" +dependencies = [ + "dbus", + "derive_is_enum_variant", + "enum-kinds", + "from_variants", + "thiserror", +] + [[package]] name = "nix" version = "0.23.2" @@ -1511,8 +1644,8 @@ dependencies = [ "pest", "pest_meta", "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1583,8 +1716,8 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", "version_check", ] @@ -1595,7 +1728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.21", "version_check", ] @@ -1608,6 +1741,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + [[package]] name = "quote" version = "1.0.21" @@ -1800,8 +1939,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1822,8 +1961,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1969,6 +2108,12 @@ dependencies = [ "vte", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "swayipc-async" version = "2.0.1" @@ -1994,6 +2139,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid", +] + [[package]] name = "syn" version = "1.0.105" @@ -2001,10 +2157,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.21", "unicode-ident", ] +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + [[package]] name = "sysinfo" version = "0.27.0" @@ -2027,7 +2192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" dependencies = [ "cfg-expr", - "heck", + "heck 0.4.0", "pkg-config", "toml", "version-compare", @@ -2072,8 +2237,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2149,8 +2314,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2203,8 +2368,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2284,12 +2449,24 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + [[package]] name = "unsafe-libyaml" version = "0.2.4" @@ -2344,7 +2521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.21", ] [[package]] @@ -2396,8 +2573,8 @@ dependencies = [ "log", "once_cell", "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-shared", ] @@ -2407,7 +2584,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote", + "quote 1.0.21", "wasm-bindgen-macro-support", ] @@ -2418,8 +2595,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2487,7 +2664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.21", "xml-rs", ] @@ -2660,9 +2837,9 @@ checksum = "1f8fb5186d1c87ae88cf234974c240671238b4a679158ad3b94ec465237349a6" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote", + "quote 1.0.21", "regex", - "syn", + "syn 1.0.105", ] [[package]] @@ -2698,6 +2875,6 @@ checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] diff --git a/Cargo.toml b/Cargo.toml index 89361cf..8be0b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,9 @@ dirs = "4.0.0" walkdir = "2.3.2" notify = { version = "5.0.0", default-features = false } mpd_client = "1.0.0" +mpris = "2.0.0" swayipc-async = { version = "2.0.1" } sysinfo = "0.27.0" wayland-client = "0.29.5" wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] } -smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] } \ No newline at end of file +smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] } diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 33b5791..32641df 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -19,7 +19,7 @@ - [Custom](custom) - [Focused](focused) - [Launcher](launcher) -- [MPD](mpd) +- [Music](music) - [Script](script) - [Sys_Info](sys-info) - [Tray](tray) diff --git a/docs/modules/MPD.md b/docs/modules/MPD.md deleted file mode 100644 index 0cfd8b6..0000000 --- a/docs/modules/MPD.md +++ /dev/null @@ -1,131 +0,0 @@ -Displays currently playing song from MPD. -Clicking on the widget opens a popout displaying info about the current song, album art -and playback controls. - -![Screenshot showing MPD widget with track playing with popout open](https://user-images.githubusercontent.com/5057870/184539664-a8f3ad5b-69c0-492d-a27d-82303c09a347.png) - -## Configuration - -> Type: `mpd` - -| | Type | Default | Description | -|----------------|----------|-----------------------------|-----------------------------------------------------------------------| -| `host` | `string` | `localhost:6600` | TCP or Unix socket for the MPD server. | -| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. | -| `icons.play` | `string` | `` | Icon to show when playing. | -| `icons.pause` | `string` | `` | Icon to show when paused. | -| `icons.volume` | `string` | `墳` | Icon to show under popup volume slider. | -| `music_dir` | `string` | `$HOME/Music` | Path to MPD server's music directory on disc. Required for album art. | - -
-JSON - -```json -{ - "start": [ - { - "type": "mpd", - "format": "{icon} {title} / {artist}", - "icons": { - "play": "", - "pause": "" - }, - "music_dir": "/home/jake/Music" - } - ] -} -``` - -
- -
-TOML - -```toml -[[start]] -type = "mpd" -format = "{icon} {title} / {artist}" -music_dir = "/home/jake/Music" - -[[start.icons]] -play = "" -pause = "" -``` - -
- -
-YAML - -```yaml -start: - - type: "mpd" - format: "{icon} {title} / {artist}" - icons: - play: "" - pause: "" - music_dir: "/home/jake/Music" -``` - -
- -
-Corn - -```corn -{ - start = [ - { - type = "mpd" - format = "{icon} {title} / {artist}" - icons.play = "" - icons.pause = "" - music_dir = "/home/jake/Music" - } - ] -} -``` - -
- -### Formatting Tokens - -The following tokens can be used in the `format` config option, -and will be replaced with values from the currently playing track: - -| Token | Description | -|--------------|--------------------------------------| -| `{icon}` | Either `icons.play` or `icons.pause` | -| `{title}` | Title | -| `{album}` | Album name | -| `{artist}` | Artist name | -| `{date}` | Release date | -| `{track}` | Track number | -| `{disc}` | Disc number | -| `{genre}` | Genre | -| `{duration}` | Duration in `mm:ss` | -| `{elapsed}` | Time elapsed in `mm:ss` | - -## Styling - -| Selector | Description | -|----------------------------------------|------------------------------------------| -| `#mpd` | Tray widget button | -| `#popup-mpd` | Popup box | -| `#popup-mpd #album-art` | Album art image inside popup box | -| `#popup-mpd #title` | Track title container inside popup box | -| `#popup-mpd #title .icon` | Track title icon label inside popup box | -| `#popup-mpd #title .label` | Track title label inside popup box | -| `#popup-mpd #album` | Track album container inside popup box | -| `#popup-mpd #album .icon` | Track album icon label inside popup box | -| `#popup-mpd #album .label` | Track album label inside popup box | -| `#popup-mpd #artist` | Track artist container inside popup box | -| `#popup-mpd #artist .icon` | Track artist icon label inside popup box | -| `#popup-mpd #artist .label` | Track artist label inside popup box | -| `#popup-mpd #controls` | Controls container inside popup box | -| `#popup-mpd #controls #btn-prev` | Previous button inside popup box | -| `#popup-mpd #controls #btn-play-pause` | Play/pause button inside popup box | -| `#popup-mpd #controls #btn-next` | Next button inside popup box | -| `#popup-mpd #volume` | Volume container inside popup box | -| `#popup-mpd #volume #slider` | Volume slider popup box | -| `#popup-mpd #volume .icon` | Volume icon label inside popup box | \ No newline at end of file diff --git a/docs/modules/Music.md b/docs/modules/Music.md new file mode 100644 index 0000000..51fbbff --- /dev/null +++ b/docs/modules/Music.md @@ -0,0 +1,139 @@ +Displays currently playing song from your music player. +This module supports both MPRIS players and MPD servers. +Clicking on the widget opens a popout displaying info about the current song, album art +and playback controls. + +in MPRIS mode, the widget will listen to all players and automatically detect/display the active one. + +![Screenshot showing MPD widget with track playing with popout open](https://user-images.githubusercontent.com/5057870/184539664-a8f3ad5b-69c0-492d-a27d-82303c09a347.png) + +## Configuration + +> Type: `music` + +| | Type | Default | Description | +|----------------|------------------|-----------------------------|----------------------------------------------------------------------------------| +| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. | +| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. | +| `icons.play` | `string` | `` | Icon to show when playing. | +| `icons.pause` | `string` | `` | Icon to show when paused. | +| `icons.volume` | `string` | `墳` | Icon to show under popup volume slider. | +| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. | +| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. | + +
+JSON + +```json +{ + "start": [ + { + "type": "music", + "player_type": "mpd", + "format": "{icon} {title} / {artist}", + "icons": { + "play": "", + "pause": "" + }, + "music_dir": "/home/jake/Music" + } + ] +} +``` + +
+ +
+TOML + +```toml +[[start]] +type = "music" +player_type = "mpd" +format = "{icon} {title} / {artist}" +music_dir = "/home/jake/Music" + +[[start.icons]] +play = "" +pause = "" +``` + +
+ +
+YAML + +```yaml +start: + - type: "music" + player_type: "mpd" + format: "{icon} {title} / {artist}" + icons: + play: "" + pause: "" + music_dir: "/home/jake/Music" +``` + +
+ +
+Corn + +```corn +{ + start = [ + { + type = "music" + player_type = "mpd" + format = "{icon} {title} / {artist}" + icons.play = "" + icons.pause = "" + music_dir = "/home/jake/Music" + } + ] +} +``` + +
+ +### Formatting Tokens + +The following tokens can be used in the `format` config option, +and will be replaced with values from the currently playing track: + +| Token | Description | +|--------------|--------------------------------------| +| `{icon}` | Either `icons.play` or `icons.pause` | +| `{title}` | Title | +| `{album}` | Album name | +| `{artist}` | Artist name | +| `{date}` | Release date | +| `{track}` | Track number | +| `{disc}` | Disc number | +| `{genre}` | Genre | +| `{duration}` | Duration in `mm:ss` | +| `{elapsed}` | Time elapsed in `mm:ss` | + +## Styling + +| Selector | Description | +|------------------------------------------|------------------------------------------| +| `#music` | Tray widget button | +| `#popup-music` | Popup box | +| `#popup-music #album-art` | Album art image inside popup box | +| `#popup-music #title` | Track title container inside popup box | +| `#popup-music #title .icon` | Track title icon label inside popup box | +| `#popup-music #title .label` | Track title label inside popup box | +| `#popup-music #album` | Track album container inside popup box | +| `#popup-music #album .icon` | Track album icon label inside popup box | +| `#popup-music #album .label` | Track album label inside popup box | +| `#popup-music #artist` | Track artist container inside popup box | +| `#popup-music #artist .icon` | Track artist icon label inside popup box | +| `#popup-music #artist .label` | Track artist label inside popup box | +| `#popup-music #controls` | Controls container inside popup box | +| `#popup-music #controls #btn-prev` | Previous button inside popup box | +| `#popup-music #controls #btn-play-pause` | Play/pause button inside popup box | +| `#popup-music #controls #btn-next` | Next button inside popup box | +| `#popup-music #volume` | Volume container inside popup box | +| `#popup-music #volume #slider` | Volume slider popup box | +| `#popup-music #volume .icon` | Volume icon label inside popup box | \ No newline at end of file diff --git a/src/bar.rs b/src/bar.rs index ce7d83d..c5db9a6 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -195,7 +195,7 @@ fn add_modules(content: >k::Box, modules: Vec, info: &ModuleInfo ModuleConfig::Focused(mut module) => add_module!(module, id), ModuleConfig::Workspaces(mut module) => add_module!(module, id), ModuleConfig::Tray(mut module) => add_module!(module, id), - ModuleConfig::Mpd(mut module) => add_module!(module, id), + ModuleConfig::Music(mut module) => add_module!(module, id), ModuleConfig::Launcher(mut module) => add_module!(module, id), ModuleConfig::Custom(mut module) => add_module!(module, id), } diff --git a/src/clients/mod.rs b/src/clients/mod.rs index ea192bd..3fbfac8 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -1,4 +1,4 @@ -pub mod mpd; +pub mod music; pub mod sway; pub mod system_tray; pub mod wayland; diff --git a/src/clients/mpd.rs b/src/clients/mpd.rs deleted file mode 100644 index 852750f..0000000 --- a/src/clients/mpd.rs +++ /dev/null @@ -1,167 +0,0 @@ -use lazy_static::lazy_static; -use mpd_client::client::{CommandError, Connection, ConnectionEvent, Subsystem}; -use mpd_client::commands::Command; -use mpd_client::protocol::MpdProtocolError; -use mpd_client::responses::Status; -use mpd_client::Client; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::os::unix::fs::FileTypeExt; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; -use tokio::net::{TcpStream, UnixStream}; -use tokio::spawn; -use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender}; -use tokio::sync::Mutex; -use tokio::time::sleep; -use tracing::debug; - -lazy_static! { - static ref CONNECTIONS: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); -} - -pub struct MpdClient { - client: Client, - tx: Sender<()>, - _rx: Receiver<()>, -} - -#[derive(Debug)] -pub enum MpdConnectionError { - MaxRetries, - ProtocolError(MpdProtocolError), -} - -impl Display for MpdConnectionError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::MaxRetries => write!(f, "Reached max retries"), - Self::ProtocolError(e) => write!(f, "{:?}", e), - } - } -} - -impl std::error::Error for MpdConnectionError {} - -impl MpdClient { - async fn new(host: &str) -> Result { - debug!("Creating new MPD connection to {}", host); - - let (client, mut state_changes) = - wait_for_connection(host, Duration::from_secs(5), None).await?; - - let (tx, rx) = channel(16); - let tx2 = tx.clone(); - - spawn(async move { - while let Some(change) = state_changes.next().await { - debug!("Received state change: {:?}", change); - - if let ConnectionEvent::SubsystemChange( - Subsystem::Player | Subsystem::Queue | Subsystem::Mixer, - ) = change - { - tx2.send(())?; - } - } - - Ok::<(), SendError<()>>(()) - }); - - Ok(Self { - client, - tx, - _rx: rx, - }) - } - - pub fn subscribe(&self) -> Receiver<()> { - self.tx.subscribe() - } - - pub async fn command(&self, command: C) -> Result { - self.client.command(command).await - } -} - -pub async fn get_client(host: &str) -> Result, MpdConnectionError> { - let mut connections = CONNECTIONS.lock().await; - match connections.get(host) { - None => { - let client = MpdClient::new(host).await?; - let client = Arc::new(client); - connections.insert(host.to_string(), Arc::clone(&client)); - Ok(client) - } - Some(client) => Ok(Arc::clone(client)), - } -} - -async fn wait_for_connection( - host: &str, - interval: Duration, - max_retries: Option, -) -> Result { - let mut retries = 0; - let max_retries = max_retries.unwrap_or(usize::MAX); - - loop { - if retries == max_retries { - break Err(MpdConnectionError::MaxRetries); - } - - retries += 1; - - match try_get_mpd_conn(host).await { - Ok(conn) => break Ok(conn), - Err(err) => { - if retries == max_retries { - break Err(MpdConnectionError::ProtocolError(err)); - } - } - } - - sleep(interval).await; - } -} - -/// Cycles through each MPD host and -/// returns the first one which connects, -/// or none if there are none -async fn try_get_mpd_conn(host: &str) -> Result { - if is_unix_socket(host) { - connect_unix(host).await - } else { - connect_tcp(host).await - } -} - -fn is_unix_socket(host: &str) -> bool { - let path = PathBuf::from(host); - path.exists() - && path - .metadata() - .map_or(false, |metadata| metadata.file_type().is_socket()) -} - -async fn connect_unix(host: &str) -> Result { - let connection = UnixStream::connect(host).await?; - Client::connect(connection).await -} - -async fn connect_tcp(host: &str) -> Result { - let connection = TcpStream::connect(host).await?; - Client::connect(connection).await -} - -/// Gets the duration of the current song -pub fn get_duration(status: &Status) -> Option { - status.duration.map(|duration| duration.as_secs()) -} - -/// Gets the elapsed time of the current song -pub fn get_elapsed(status: &Status) -> Option { - status.elapsed.map(|duration| duration.as_secs()) -} diff --git a/src/clients/music/mod.rs b/src/clients/music/mod.rs new file mode 100644 index 0000000..6c3edd2 --- /dev/null +++ b/src/clients/music/mod.rs @@ -0,0 +1,66 @@ +use color_eyre::Result; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::broadcast; + +pub mod mpd; +pub mod mpris; + +pub type PlayerUpdate = (Option, Status); + +#[derive(Clone, Debug)] +pub struct Track { + pub title: Option, + pub album: Option, + pub artist: Option, + pub date: Option, + pub disc: Option, + pub genre: Option, + pub track: Option, + pub cover_path: Option, +} + +#[derive(Clone, Debug)] +pub enum PlayerState { + Playing, + Paused, + Stopped, +} + +#[derive(Clone, Debug)] +pub struct Status { + pub state: PlayerState, + pub volume_percent: u8, + pub duration: Option, + pub elapsed: Option, + pub playlist_position: u32, + pub playlist_length: u32, +} + +pub trait MusicClient { + fn play(&self) -> Result<()>; + fn pause(&self) -> Result<()>; + fn next(&self) -> Result<()>; + fn prev(&self) -> Result<()>; + + fn set_volume_percent(&self, vol: u8) -> Result<()>; + + fn subscribe_change(&self) -> broadcast::Receiver; +} + +pub enum ClientType<'a> { + Mpd { host: &'a str, music_dir: PathBuf }, + Mpris, +} + +pub async fn get_client(client_type: ClientType<'_>) -> Box> { + match client_type { + ClientType::Mpd { host, music_dir } => Box::new( + mpd::get_client(host, music_dir) + .await + .expect("Failed to connect to MPD client"), + ), + ClientType::Mpris => Box::new(mpris::get_client()), + } +} diff --git a/src/clients/music/mpd.rs b/src/clients/music/mpd.rs new file mode 100644 index 0000000..770d8b1 --- /dev/null +++ b/src/clients/music/mpd.rs @@ -0,0 +1,282 @@ +use super::{MusicClient, Status, Track}; +use crate::await_sync; +use crate::clients::music::{PlayerState, PlayerUpdate}; +use color_eyre::Result; +use lazy_static::lazy_static; +use mpd_client::client::{Connection, ConnectionEvent, Subsystem}; +use mpd_client::protocol::MpdProtocolError; +use mpd_client::responses::{PlayState, Song}; +use mpd_client::tag::Tag; +use mpd_client::{commands, Client}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::os::unix::fs::FileTypeExt; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::{TcpStream, UnixStream}; +use tokio::spawn; +use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender}; +use tokio::sync::Mutex; +use tokio::time::sleep; +use tracing::{debug, error}; + +lazy_static! { + static ref CONNECTIONS: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); +} + +pub struct MpdClient { + client: Client, + music_dir: PathBuf, + tx: Sender, + _rx: Receiver, +} + +#[derive(Debug)] +pub enum MpdConnectionError { + MaxRetries, + ProtocolError(MpdProtocolError), +} + +impl Display for MpdConnectionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::MaxRetries => write!(f, "Reached max retries"), + Self::ProtocolError(e) => write!(f, "{e:?}"), + } + } +} + +impl std::error::Error for MpdConnectionError {} + +impl MpdClient { + async fn new(host: &str, music_dir: PathBuf) -> Result { + debug!("Creating new MPD connection to {}", host); + + let (client, mut state_changes) = + wait_for_connection(host, Duration::from_secs(5), None).await?; + + let (tx, rx) = channel(16); + + { + let music_dir = music_dir.clone(); + let tx = tx.clone(); + let client = client.clone(); + + spawn(async move { + while let Some(change) = state_changes.next().await { + debug!("Received state change: {:?}", change); + + if let ConnectionEvent::SubsystemChange( + Subsystem::Player | Subsystem::Queue | Subsystem::Mixer, + ) = change + { + Self::send_update(&client, &tx, &music_dir).await?; + } + } + + Ok::<(), SendError<(Option, Status)>>(()) + }); + } + + Ok(Self { + client, + music_dir, + tx, + _rx: rx, + }) + } + + async fn send_update( + client: &Client, + tx: &Sender, + music_dir: &Path, + ) -> Result<(), SendError<(Option, Status)>> { + let current_song = client.command(commands::CurrentSong).await; + let status = client.command(commands::Status).await; + + if let (Ok(current_song), Ok(status)) = (current_song, status) { + let track = current_song.map(|s| Self::convert_song(&s.song, music_dir)); + let status = Status::from(status); + + tx.send((track, status))?; + } + + Ok(()) + } + + fn convert_song(song: &Song, music_dir: &Path) -> Track { + let (track, disc) = song.number(); + + let cover_path = music_dir.join( + song.file_path() + .parent() + .expect("Song path should not be root") + .join("cover.jpg"), + ); + + Track { + title: song.title().map(std::string::ToString::to_string), + album: song.album().map(std::string::ToString::to_string), + artist: Some(song.artists().join(", ")), + date: try_get_first_tag(song, &Tag::Date).map(std::string::ToString::to_string), + genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string), + disc: Some(disc), + track: Some(track), + cover_path: Some(cover_path), + } + } +} + +macro_rules! async_command { + ($client:expr, $command:expr) => { + await_sync(async { + $client + .command($command) + .await + .unwrap_or_else(|err| error!("Failed to send command: {err:?}")) + }) + }; +} + +impl MusicClient for MpdClient { + fn play(&self) -> Result<()> { + async_command!(self.client, commands::SetPause(false)); + Ok(()) + } + + fn pause(&self) -> Result<()> { + async_command!(self.client, commands::SetPause(true)); + Ok(()) + } + + fn next(&self) -> Result<()> { + async_command!(self.client, commands::Next); + Ok(()) + } + + fn prev(&self) -> Result<()> { + async_command!(self.client, commands::Previous); + Ok(()) + } + + fn set_volume_percent(&self, vol: u8) -> Result<()> { + async_command!(self.client, commands::SetVolume(vol)); + Ok(()) + } + + fn subscribe_change(&self) -> Receiver { + let rx = self.tx.subscribe(); + await_sync(async { + Self::send_update(&self.client, &self.tx, &self.music_dir) + .await + .expect("Failed to send player update"); + }); + rx + } +} + +pub async fn get_client( + host: &str, + music_dir: PathBuf, +) -> Result, MpdConnectionError> { + let mut connections = CONNECTIONS.lock().await; + match connections.get(host) { + None => { + let client = MpdClient::new(host, music_dir).await?; + let client = Arc::new(client); + connections.insert(host.to_string(), Arc::clone(&client)); + Ok(client) + } + Some(client) => Ok(Arc::clone(client)), + } +} + +async fn wait_for_connection( + host: &str, + interval: Duration, + max_retries: Option, +) -> Result { + let mut retries = 0; + let max_retries = max_retries.unwrap_or(usize::MAX); + + loop { + if retries == max_retries { + break Err(MpdConnectionError::MaxRetries); + } + + retries += 1; + + match try_get_mpd_conn(host).await { + Ok(conn) => break Ok(conn), + Err(err) => { + if retries == max_retries { + break Err(MpdConnectionError::ProtocolError(err)); + } + } + } + + sleep(interval).await; + } +} + +/// Cycles through each MPD host and +/// returns the first one which connects, +/// or none if there are none +async fn try_get_mpd_conn(host: &str) -> Result { + if is_unix_socket(host) { + connect_unix(host).await + } else { + connect_tcp(host).await + } +} + +fn is_unix_socket(host: &str) -> bool { + let path = PathBuf::from(host); + path.exists() + && path + .metadata() + .map_or(false, |metadata| metadata.file_type().is_socket()) +} + +async fn connect_unix(host: &str) -> Result { + let connection = UnixStream::connect(host).await?; + Client::connect(connection).await +} + +async fn connect_tcp(host: &str) -> Result { + let connection = TcpStream::connect(host).await?; + Client::connect(connection).await +} + +/// Attempts to read the first value for a tag +/// (since the MPD client returns a vector of tags, or None) +pub fn try_get_first_tag<'a>(song: &'a Song, tag: &'a Tag) -> Option<&'a str> { + song.tags + .get(tag) + .and_then(|vec| vec.first().map(String::as_str)) +} + +impl From for Status { + fn from(status: mpd_client::responses::Status) -> Self { + Self { + state: PlayerState::from(status.state), + volume_percent: status.volume, + duration: status.duration, + elapsed: status.elapsed, + playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32), + playlist_length: status.playlist_length as u32, + } + } +} + +impl From for PlayerState { + fn from(value: PlayState) -> Self { + match value { + PlayState::Stopped => Self::Stopped, + PlayState::Playing => Self::Playing, + PlayState::Paused => Self::Paused, + } + } +} diff --git a/src/clients/music/mpris.rs b/src/clients/music/mpris.rs new file mode 100644 index 0000000..af7aeef --- /dev/null +++ b/src/clients/music/mpris.rs @@ -0,0 +1,283 @@ +use super::{MusicClient, PlayerUpdate, Status, Track}; +use crate::clients::music::PlayerState; +use crate::error::ERR_MUTEX_LOCK; +use color_eyre::Result; +use lazy_static::lazy_static; +use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::thread::sleep; +use std::time::Duration; +use tokio::sync::broadcast::{channel, Receiver, Sender}; +use tokio::task::spawn_blocking; +use tracing::{debug, error, trace}; + +lazy_static! { + static ref CLIENT: Arc = Arc::new(Client::new()); +} + +pub struct Client { + current_player: Arc>>, + tx: Sender, + _rx: Receiver, +} + +impl Client { + fn new() -> Self { + let (tx, rx) = channel(32); + + let current_player = Arc::new(Mutex::new(None)); + + { + let players_list = Arc::new(Mutex::new(HashSet::new())); + let current_player = current_player.clone(); + let tx = tx.clone(); + + spawn_blocking(move || { + let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus"); + + // D-Bus gives no event for new players, + // so we have to keep polling the player list + loop { + let players = player_finder + .find_all() + .expect("Failed to connect to D-Bus"); + + let mut players_list_val = players_list.lock().expect(ERR_MUTEX_LOCK); + for player in players { + let identity = player.identity(); + + if !players_list_val.contains(identity) { + debug!("Adding MPRIS player '{identity}'"); + players_list_val.insert(identity.to_string()); + + let status = player + .get_playback_status() + .expect("Failed to connect to D-Bus"); + + { + let mut current_player = + current_player.lock().expect(ERR_MUTEX_LOCK); + + if status == PlaybackStatus::Playing || current_player.is_none() { + debug!("Setting active player to '{identity}'"); + + current_player.replace(identity.to_string()); + if let Err(err) = Self::send_update(&player, &tx) { + error!("{err:?}"); + } + } + } + + Self::listen_player_events( + identity.to_string(), + players_list.clone(), + current_player.clone(), + tx.clone(), + ); + } + } + + // wait 1 second before re-checking players + sleep(Duration::from_secs(1)); + } + }); + } + + Self { + current_player, + tx, + _rx: rx, + } + } + + fn listen_player_events( + player_id: String, + players: Arc>>, + current_player: Arc>>, + tx: Sender, + ) { + spawn_blocking(move || { + let player_finder = PlayerFinder::new()?; + + if let Ok(player) = player_finder.find_by_name(&player_id) { + let identity = player.identity(); + + for event in player.events()? { + trace!("Received player event from '{identity}': {event:?}"); + match event { + Ok(Event::PlayerShutDown) => { + current_player.lock().expect(ERR_MUTEX_LOCK).take(); + players.lock().expect(ERR_MUTEX_LOCK).remove(identity); + break; + } + Ok(Event::Playing) => { + current_player + .lock() + .expect(ERR_MUTEX_LOCK) + .replace(identity.to_string()); + + if let Err(err) = Self::send_update(&player, &tx) { + error!("{err:?}"); + } + } + Ok(_) => { + let current_player = current_player.lock().expect(ERR_MUTEX_LOCK); + let current_player = current_player.as_ref(); + if let Some(current_player) = current_player { + if current_player == identity { + if let Err(err) = Self::send_update(&player, &tx) { + error!("{err:?}"); + } + } + } + } + Err(err) => error!("{err:?}"), + } + } + } + + Ok::<(), DBusError>(()) + }); + } + + fn send_update(player: &Player, tx: &Sender) -> Result<()> { + debug!("Sending update using '{}'", player.identity()); + + let metadata = player.get_metadata()?; + let playback_status = player + .get_playback_status() + .unwrap_or(PlaybackStatus::Stopped); + + let track_list = player.get_track_list(); + + let volume_percent = player + .get_volume() + .map(|vol| (vol * 100.0) as u8) + .unwrap_or(0); + + let status = Status { + playlist_position: 0, + playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(1), + state: PlayerState::from(playback_status), + elapsed: player.get_position().ok(), + duration: metadata.length(), + volume_percent, + }; + + let track = Track::from(metadata); + + let player_update: PlayerUpdate = (Some(track), status); + + tx.send(player_update) + .expect("Failed to send player update"); + + Ok(()) + } + + fn get_player(&self) -> Option { + let player_name = self.current_player.lock().expect(ERR_MUTEX_LOCK); + let player_name = player_name.as_ref(); + + player_name.and_then(|player_name| { + let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus"); + player_finder.find_by_name(player_name).ok() + }) + } +} + +macro_rules! command { + ($self:ident, $func:ident) => { + if let Some(player) = Self::get_player($self) { + player.$func()?; + } else { + error!("Could not find player"); + } + }; +} + +impl MusicClient for Client { + fn play(&self) -> Result<()> { + command!(self, play); + Ok(()) + } + + fn pause(&self) -> Result<()> { + command!(self, pause); + Ok(()) + } + + fn next(&self) -> Result<()> { + command!(self, next); + Ok(()) + } + + fn prev(&self) -> Result<()> { + command!(self, previous); + Ok(()) + } + + fn set_volume_percent(&self, vol: u8) -> Result<()> { + if let Some(player) = Self::get_player(self) { + player.set_volume(vol as f64 / 100.0)?; + } else { + error!("Could not find player"); + } + Ok(()) + } + + fn subscribe_change(&self) -> Receiver { + debug!("Creating new subscription"); + let rx = self.tx.subscribe(); + + if let Some(player) = self.get_player() { + if let Err(err) = Self::send_update(&player, &self.tx) { + error!("{err:?}"); + } + } + + rx + } +} + +pub fn get_client() -> Arc { + CLIENT.clone() +} + +impl From for Track { + fn from(value: Metadata) -> Self { + const KEY_DATE: &str = "xesam:contentCreated"; + const KEY_GENRE: &str = "xesam:genre"; + + Self { + title: value.title().map(std::string::ToString::to_string), + album: value.album_name().map(std::string::ToString::to_string), + artist: value.artists().map(|artists| artists.join(", ")), + date: value + .get(KEY_DATE) + .and_then(mpris::MetadataValue::as_string) + .map(std::string::ToString::to_string), + disc: value.disc_number().map(|disc| disc as u64), + genre: value + .get(KEY_GENRE) + .and_then(mpris::MetadataValue::as_str_array) + .and_then(|arr| arr.first().map(|val| (*val).to_string())), + track: value.track_number().map(|track| track as u64), + cover_path: value + .art_url() + .map(|path| path.replace("file://", "")) + .map(PathBuf::from), + } + } +} + +impl From for PlayerState { + fn from(value: PlaybackStatus) -> Self { + match value { + PlaybackStatus::Playing => Self::Playing, + PlaybackStatus::Paused => Self::Paused, + PlaybackStatus::Stopped => Self::Stopped, + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 91a4681..c054a3d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,7 +4,7 @@ use crate::modules::clock::ClockModule; use crate::modules::custom::CustomModule; use crate::modules::focused::FocusedModule; use crate::modules::launcher::LauncherModule; -use crate::modules::mpd::MpdModule; +use crate::modules::music::MusicModule; use crate::modules::script::ScriptModule; use crate::modules::sysinfo::SysInfoModule; use crate::modules::tray::TrayModule; @@ -30,7 +30,7 @@ pub struct CommonConfig { #[serde(tag = "type", rename_all = "snake_case")] pub enum ModuleConfig { Clock(ClockModule), - Mpd(MpdModule), + Music(MusicModule), Tray(TrayModule), Workspaces(WorkspacesModule), SysInfo(SysInfoModule), diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 1f1a169..f29787e 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -8,7 +8,7 @@ pub mod clock; pub mod custom; pub mod focused; pub mod launcher; -pub mod mpd; +pub mod music; pub mod script; pub mod sysinfo; pub mod tray; diff --git a/src/modules/mpd.rs b/src/modules/music.rs similarity index 60% rename from src/modules/mpd.rs rename to src/modules/music.rs index ed083f0..7a1af22 100644 --- a/src/modules/mpd.rs +++ b/src/modules/music.rs @@ -1,4 +1,4 @@ -use crate::clients::mpd::{get_client, get_duration, get_elapsed, MpdConnectionError}; +use crate::clients::music::{self, MusicClient, PlayerState, Status, Track}; use crate::config::CommonConfig; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::popup::Popup; @@ -9,12 +9,11 @@ use glib::Continue; use gtk::gdk_pixbuf::Pixbuf; use gtk::prelude::*; use gtk::{Button, Image, Label, Orientation, Scale}; -use mpd_client::commands; -use mpd_client::responses::{PlayState, Song, Status}; -use mpd_client::tag::Tag; use regex::Regex; use serde::Deserialize; use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; use tokio::spawn; use tokio::sync::mpsc; use tokio::sync::mpsc::{Receiver, Sender}; @@ -23,7 +22,8 @@ use tracing::error; #[derive(Debug)] pub enum PlayerCommand { Previous, - Toggle, + Play, + Pause, Next, Volume(u8), } @@ -51,11 +51,26 @@ impl Default for Icons { } } +#[derive(Debug, Deserialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum PlayerType { + // Auto, + Mpd, + Mpris, +} + +impl Default for PlayerType { + fn default() -> Self { + Self::Mpris + } +} + #[derive(Debug, Deserialize, Clone)] -pub struct MpdModule { - /// TCP or Unix socket address. - #[serde(default = "default_socket")] - host: String, +pub struct MusicModule { + /// Type of player to connect to + #[serde(default)] + player_type: PlayerType, + /// Format of current song info to display on the bar. #[serde(default = "default_format")] format: String, @@ -64,6 +79,10 @@ pub struct MpdModule { #[serde(default)] icons: Icons, + // -- MPD -- + /// TCP or Unix socket address. + #[serde(default = "default_socket")] + host: String, /// Path to root of music directory. #[serde(default = "default_music_dir")] music_dir: PathBuf, @@ -96,15 +115,10 @@ fn default_music_dir() -> PathBuf { audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default()) } -/// Attempts to read the first value for a tag -/// (since the MPD client returns a vector of tags, or None) -pub fn try_get_first_tag(vec: Option<&Vec>) -> Option<&str> { - vec.and_then(|vec| vec.first().map(String::as_str)) -} - /// Formats a duration given in seconds /// in hh:mm format -fn format_time(time: u64) -> String { +fn format_time(duration: Duration) -> String { + let time = duration.as_secs(); let minutes = (time / 60) % 60; let seconds = time % 60; @@ -120,17 +134,29 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec { #[derive(Clone, Debug)] pub struct SongUpdate { - song: Song, + song: Track, status: Status, display_string: String, } -impl Module