1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-04-19 19:34:24 +02:00

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.
This commit is contained in:
Jake Stanger 2023-01-25 22:46:42 +00:00
parent 8076412bfc
commit 6d8e647f12
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
14 changed files with 1165 additions and 496 deletions

271
Cargo.lock generated
View file

@ -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",
]

View file

@ -32,6 +32,7 @@ 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"

View file

@ -19,7 +19,7 @@
- [Custom](custom)
- [Focused](focused)
- [Launcher](launcher)
- [MPD](mpd)
- [Music](music)
- [Script](script)
- [Sys_Info](sys-info)
- [Tray](tray)

View file

@ -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. |
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "mpd",
"format": "{icon} {title} / {artist}",
"icons": {
"play": "",
"pause": ""
},
"music_dir": "/home/jake/Music"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "mpd"
format = "{icon} {title} / {artist}"
music_dir = "/home/jake/Music"
[[start.icons]]
play = ""
pause = ""
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "mpd"
format: "{icon} {title} / {artist}"
icons:
play: ""
pause: ""
music_dir: "/home/jake/Music"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "mpd"
format = "{icon} {title} / {artist}"
icons.play = ""
icons.pause = ""
music_dir = "/home/jake/Music"
}
]
}
```
</details>
### 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 |

139
docs/modules/Music.md Normal file
View file

@ -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. |
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "music",
"player_type": "mpd",
"format": "{icon} {title} / {artist}",
"icons": {
"play": "",
"pause": ""
},
"music_dir": "/home/jake/Music"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "music"
player_type = "mpd"
format = "{icon} {title} / {artist}"
music_dir = "/home/jake/Music"
[[start.icons]]
play = ""
pause = ""
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "music"
player_type: "mpd"
format: "{icon} {title} / {artist}"
icons:
play: ""
pause: ""
music_dir: "/home/jake/Music"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "music"
player_type = "mpd"
format = "{icon} {title} / {artist}"
icons.play = ""
icons.pause = ""
music_dir = "/home/jake/Music"
}
]
}
```
</details>
### 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 |

View file

@ -195,7 +195,7 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, 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),
}

View file

@ -1,4 +1,4 @@
pub mod mpd;
pub mod music;
pub mod sway;
pub mod system_tray;
pub mod wayland;

View file

@ -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<Mutex<HashMap<String, Arc<MpdClient>>>> =
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<Self, MpdConnectionError> {
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<C: Command>(&self, command: C) -> Result<C::Response, CommandError> {
self.client.command(command).await
}
}
pub async fn get_client(host: &str) -> Result<Arc<MpdClient>, 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<usize>,
) -> Result<Connection, MpdConnectionError> {
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<Connection, MpdProtocolError> {
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<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await
}
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host).await?;
Client::connect(connection).await
}
/// Gets the duration of the current song
pub fn get_duration(status: &Status) -> Option<u64> {
status.duration.map(|duration| duration.as_secs())
}
/// Gets the elapsed time of the current song
pub fn get_elapsed(status: &Status) -> Option<u64> {
status.elapsed.map(|duration| duration.as_secs())
}

66
src/clients/music/mod.rs Normal file
View file

@ -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<Track>, Status);
#[derive(Clone, Debug)]
pub struct Track {
pub title: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
pub date: Option<String>,
pub disc: Option<u64>,
pub genre: Option<String>,
pub track: Option<u64>,
pub cover_path: Option<PathBuf>,
}
#[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<Duration>,
pub elapsed: Option<Duration>,
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<PlayerUpdate>;
}
pub enum ClientType<'a> {
Mpd { host: &'a str, music_dir: PathBuf },
Mpris,
}
pub async fn get_client(client_type: ClientType<'_>) -> Box<Arc<dyn MusicClient>> {
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()),
}
}

282
src/clients/music/mpd.rs Normal file
View file

@ -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<Mutex<HashMap<String, Arc<MpdClient>>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub struct MpdClient {
client: Client,
music_dir: PathBuf,
tx: Sender<PlayerUpdate>,
_rx: Receiver<PlayerUpdate>,
}
#[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<Self, MpdConnectionError> {
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<Track>, Status)>>(())
});
}
Ok(Self {
client,
music_dir,
tx,
_rx: rx,
})
}
async fn send_update(
client: &Client,
tx: &Sender<PlayerUpdate>,
music_dir: &Path,
) -> Result<(), SendError<(Option<Track>, 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<PlayerUpdate> {
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<Arc<MpdClient>, 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<usize>,
) -> Result<Connection, MpdConnectionError> {
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<Connection, MpdProtocolError> {
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<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await
}
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
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<mpd_client::responses::Status> 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<PlayState> for PlayerState {
fn from(value: PlayState) -> Self {
match value {
PlayState::Stopped => Self::Stopped,
PlayState::Playing => Self::Playing,
PlayState::Paused => Self::Paused,
}
}
}

283
src/clients/music/mpris.rs Normal file
View file

@ -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<Client> = Arc::new(Client::new());
}
pub struct Client {
current_player: Arc<Mutex<Option<String>>>,
tx: Sender<PlayerUpdate>,
_rx: Receiver<PlayerUpdate>,
}
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<Mutex<HashSet<String>>>,
current_player: Arc<Mutex<Option<String>>>,
tx: Sender<PlayerUpdate>,
) {
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<PlayerUpdate>) -> 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<Player> {
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<PlayerUpdate> {
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> {
CLIENT.clone()
}
impl From<Metadata> 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<PlaybackStatus> for PlayerState {
fn from(value: PlaybackStatus) -> Self {
match value {
PlaybackStatus::Playing => Self::Playing,
PlaybackStatus::Paused => Self::Paused,
PlaybackStatus::Stopped => Self::Stopped,
}
}
}

View file

@ -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),

View file

@ -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;

View file

@ -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<String>>) -> 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<String> {
#[derive(Clone, Debug)]
pub struct SongUpdate {
song: Song,
song: Track,
status: Status,
display_string: String,
}
impl Module<Button> for MpdModule {
async fn get_client(
player_type: PlayerType,
host: &str,
music_dir: PathBuf,
) -> Box<Arc<dyn MusicClient>> {
match player_type {
PlayerType::Mpd => music::get_client(music::ClientType::Mpd { host, music_dir }),
PlayerType::Mpris => music::get_client(music::ClientType::Mpris {}),
}
.await
}
impl Module<Button> for MusicModule {
type SendMessage = Option<SongUpdate>;
type ReceiveMessage = PlayerCommand;
fn name() -> &'static str {
"mpd"
"music"
}
fn spawn_controller(
@ -139,73 +165,69 @@ impl Module<Button> for MpdModule {
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let host1 = self.host.clone();
let host2 = self.host.clone();
let format = self.format.clone();
let icons = self.icons.clone();
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
// poll mpd server
// receive player updates
{
let player_type = self.player_type;
let host = self.host.clone();
let music_dir = self.music_dir.clone();
spawn(async move {
let client = get_client(&host1).await.expect("Failed to connect to MPD");
let mut mpd_rx = client.subscribe();
let mut rx = {
let client = get_client(player_type, &host, music_dir).await;
client.subscribe_change()
};
loop {
let current_song = client.command(commands::CurrentSong).await;
let status = client.command(commands::Status).await;
if let (Ok(Some(song)), Ok(status)) = (current_song, status) {
while let Ok((track, status)) = rx.recv().await {
match track {
Some(track) => {
let display_string =
replace_tokens(format.as_str(), &tokens, &song.song, &status, &icons);
replace_tokens(format.as_str(), &tokens, &track, &status, &icons);
let update = SongUpdate {
song: song.song,
song: track,
status,
display_string,
};
tx.send(ModuleUpdateEvent::Update(Some(update))).await?;
} else {
tx.send(ModuleUpdateEvent::Update(None)).await?;
}
// wait for player state change
if mpd_rx.recv().await.is_err() {
break;
None => tx.send(ModuleUpdateEvent::Update(None)).await?,
}
}
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
});
}
// listen to ui events
spawn(async move {
let client = get_client(&host2).await?;
{
let player_type = self.player_type;
let host = self.host.clone();
let music_dir = self.music_dir.clone();
spawn(async move {
while let Some(event) = rx.recv().await {
let client = get_client(player_type, &host, music_dir.clone()).await;
let res = match event {
PlayerCommand::Previous => client.command(commands::Previous).await,
PlayerCommand::Toggle => match client.command(commands::Status).await {
Ok(status) => match status.state {
PlayState::Playing => client.command(commands::SetPause(true)).await,
PlayState::Paused => client.command(commands::SetPause(false)).await,
PlayState::Stopped => Ok(()),
},
Err(err) => Err(err),
},
PlayerCommand::Next => client.command(commands::Next).await,
PlayerCommand::Volume(vol) => client.command(commands::SetVolume(vol)).await,
PlayerCommand::Previous => client.prev(),
PlayerCommand::Play => client.play(),
PlayerCommand::Pause => client.pause(),
PlayerCommand::Next => client.next(),
PlayerCommand::Volume(vol) => client.set_volume_percent(vol), // .unwrap_or_else(|_| error!("Failed to update player volume")),
};
if let Err(err) = res {
error!("Failed to send command to MPD server: {:?}", err);
error!("Failed to send command to server: {:?}", err);
}
}
Ok::<(), MpdConnectionError>(())
});
}
Ok(())
}
@ -266,7 +288,7 @@ impl Module<Button> for MpdModule {
let container = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.spacing(10)
.name("popup-mpd")
.name("popup-music")
.build();
let album_image = Image::builder()
@ -325,8 +347,12 @@ impl Module<Button> for MpdModule {
});
let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |_| {
try_send!(tx_toggle, PlayerCommand::Toggle);
btn_play_pause.connect_clicked(move |button| {
if button.style_context().has_class("playing") {
try_send!(tx_toggle, PlayerCommand::Pause);
} else {
try_send!(tx_toggle, PlayerCommand::Play);
}
});
let tx_next = tx.clone();
@ -343,70 +369,66 @@ impl Module<Button> for MpdModule {
container.show_all();
{
let music_dir = self.music_dir;
let mut prev_cover = None;
rx.attach(None, move |update| {
if let Some(update) = update {
let prev_album = album_label.label.text();
let curr_album = update.song.album().unwrap_or_default();
// only update art when album changes
if prev_album != curr_album {
let cover_path = music_dir.join(
update
.song
.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
);
Pixbuf::from_file_at_scale(cover_path, 128, 128, true).map_or_else(
|_| {
album_image.set_from_pixbuf(None);
},
|pixbuf| {
album_image.set_from_pixbuf(Some(&pixbuf));
},
);
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover = new_cover.clone();
match new_cover.map(|cover_path| {
Pixbuf::from_file_at_scale(cover_path, 128, 128, true)
}) {
Some(Ok(pixbuf)) => album_image.set_from_pixbuf(Some(&pixbuf)),
Some(Err(err)) => {
error!("{:?}", err);
album_image.set_from_pixbuf(None)
}
None => album_image.set_from_pixbuf(None),
};
}
title_label
.label
.set_text(update.song.title().unwrap_or_default());
album_label.label.set_text(curr_album);
.set_text(&update.song.title.unwrap_or_default());
album_label
.label
.set_text(&update.song.album.unwrap_or_default());
artist_label
.label
.set_text(update.song.artists().first().unwrap_or(&String::new()));
.set_text(&update.song.artist.unwrap_or_default());
match update.status.state {
PlayState::Stopped => {
PlayerState::Stopped => {
btn_play_pause.set_sensitive(false);
}
PlayState::Playing => {
PlayerState::Playing => {
btn_play_pause.set_sensitive(true);
btn_play_pause.set_label("");
btn_play_pause.set_label(&self.icons.pause);
let style_context = btn_play_pause.style_context();
style_context.add_class("playing");
style_context.remove_class("paused");
}
PlayState::Paused => {
PlayerState::Paused => {
btn_play_pause.set_sensitive(true);
btn_play_pause.set_label("");
btn_play_pause.set_label(&self.icons.play);
let style_context = btn_play_pause.style_context();
style_context.add_class("paused");
style_context.remove_class("playing");
}
}
let enable_prev = match update.status.current_song {
Some((pos, _)) => pos.0 > 0,
None => false,
};
let enable_prev = update.status.playlist_position > 0;
let enable_next = match update.status.current_song {
Some((pos, _)) => pos.0 < update.status.playlist_length,
None => false,
};
let enable_next =
update.status.playlist_position < update.status.playlist_length;
btn_prev.set_sensitive(enable_prev);
btn_next.set_sensitive(enable_next);
volume_slider.set_value(update.status.volume as f64);
volume_slider.set_value(update.status.volume_percent as f64);
}
Continue(true)
@ -418,11 +440,11 @@ impl Module<Button> for MpdModule {
}
/// Replaces each of the formatting tokens in the formatting string
/// with actual data pulled from MPD
/// with actual data pulled from the music player
fn replace_tokens(
format_string: &str,
tokens: &Vec<String>,
song: &Song,
song: &Track,
status: &Status,
icons: &Icons,
) -> String {
@ -436,30 +458,27 @@ fn replace_tokens(
}
/// Converts a string format token value
/// into its respective MPD value.
fn get_token_value(song: &Song, status: &Status, icons: &Icons, token: &str) -> String {
let s = match token {
"icon" => {
let icon = match status.state {
PlayState::Stopped => None,
PlayState::Playing => Some(&icons.play),
PlayState::Paused => Some(&icons.pause),
};
icon.map(String::as_str)
/// into its respective value.
fn get_token_value(song: &Track, status: &Status, icons: &Icons, token: &str) -> String {
match token {
"icon" => match status.state {
PlayerState::Stopped => None,
PlayerState::Playing => Some(&icons.play),
PlayerState::Paused => Some(&icons.pause),
}
"title" => song.title(),
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
"artist" => try_get_first_tag(song.tags.get(&Tag::Artist)),
"date" => try_get_first_tag(song.tags.get(&Tag::Date)),
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
"duration" => return get_duration(status).map(format_time).unwrap_or_default(),
"elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
_ => Some(token),
};
s.unwrap_or_default().to_string()
.map(|s| s.to_string()),
"title" => song.title.clone(),
"album" => song.album.clone(),
"artist" => song.artist.clone(),
"date" => song.date.clone(),
"disc" => song.disc.map(|x| x.to_string()),
"genre" => song.genre.clone(),
"track" => song.track.map(|x| x.to_string()),
"duration" => status.duration.map(format_time),
"elapsed" => status.elapsed.map(format_time),
_ => Some(token.to_string()),
}
.unwrap_or_default()
}
#[derive(Clone)]