pympris2 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: pympris2
3
+ Version: 0.2.0
4
+ Summary: An implementation of an MPRIS D-Bus client
5
+ Author: Stefan Tatschner
6
+ Author-email: Stefan Tatschner <stefan.tatschner@mailbox.org>
7
+ License-Expression: EUPL-1.2
8
+ Requires-Dist: sdbus>=0.14.2
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ <!--
13
+ SPDX-FileCopyrightText: 2026 Stefan Tatschner
14
+
15
+ SPDX-License-Identifier: CC0-1.0
16
+ -->
17
+
18
+ # pympris2
19
+
20
+ An implementation of the [MPRIS D-Bus Interface Specification](https://specifications.freedesktop.org/mpris/latest/index.html) using sdbus.
21
+
22
+ ## Example
23
+
24
+ ``` python
25
+ import asyncio
26
+
27
+ from pympris2 import get_active_players, MPRISInterface
28
+
29
+
30
+ async def main() -> None:
31
+ player = await get_active_players()[0]
32
+ proxy = MPRISInterface.connect(player)
33
+ await proxy.play_pause()
34
+ print(await proxy.volume)
35
+ await proxy.volume.set_async(0.5)
36
+ # …
37
+
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ All methods and properties of the implemented interfaces are available in the `proxy` object.
43
+ There is also a CLI tool `mprisctl` available; just check `--help`.
44
+
45
+ ## Implementation Status
46
+
47
+ * [org.mpris.MediaPlayer2](https://specifications.freedesktop.org/mpris/latest/Media_Player.html) implemented
48
+ * [org.mpris.MediaPlayer2.Player](https://specifications.freedesktop.org/mpris/latest/Player_Interface.html) implemented
49
+ * [org.mpris.MediaPlayer2.TrackList](https://specifications.freedesktop.org/mpris/latest/Track_List_Interface.html) not implemented
50
+ * [org.mpris.MediaPlayer2.Playlists](https://specifications.freedesktop.org/mpris/latest/Playlists_Interface.html) not implemented
@@ -0,0 +1,39 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Stefan Tatschner
3
+
4
+ SPDX-License-Identifier: CC0-1.0
5
+ -->
6
+
7
+ # pympris2
8
+
9
+ An implementation of the [MPRIS D-Bus Interface Specification](https://specifications.freedesktop.org/mpris/latest/index.html) using sdbus.
10
+
11
+ ## Example
12
+
13
+ ``` python
14
+ import asyncio
15
+
16
+ from pympris2 import get_active_players, MPRISInterface
17
+
18
+
19
+ async def main() -> None:
20
+ player = await get_active_players()[0]
21
+ proxy = MPRISInterface.connect(player)
22
+ await proxy.play_pause()
23
+ print(await proxy.volume)
24
+ await proxy.volume.set_async(0.5)
25
+ # …
26
+
27
+
28
+ asyncio.run(main())
29
+ ```
30
+
31
+ All methods and properties of the implemented interfaces are available in the `proxy` object.
32
+ There is also a CLI tool `mprisctl` available; just check `--help`.
33
+
34
+ ## Implementation Status
35
+
36
+ * [org.mpris.MediaPlayer2](https://specifications.freedesktop.org/mpris/latest/Media_Player.html) implemented
37
+ * [org.mpris.MediaPlayer2.Player](https://specifications.freedesktop.org/mpris/latest/Player_Interface.html) implemented
38
+ * [org.mpris.MediaPlayer2.TrackList](https://specifications.freedesktop.org/mpris/latest/Track_List_Interface.html) not implemented
39
+ * [org.mpris.MediaPlayer2.Playlists](https://specifications.freedesktop.org/mpris/latest/Playlists_Interface.html) not implemented
@@ -0,0 +1,64 @@
1
+ # SPDX-FileCopyrightText: 2026 Stefan Tatschner
2
+ #
3
+ # SPDX-License-Identifier: CC0-1.0
4
+
5
+ [build-system]
6
+ requires = ["uv_build>=0.11.15,<0.12.0"]
7
+ build-backend = "uv_build"
8
+
9
+ [project]
10
+ name = "pympris2"
11
+ version = "0.2.0"
12
+ description = "An implementation of an MPRIS D-Bus client"
13
+ license = "EUPL-1.2"
14
+ readme = "README.md"
15
+ authors = [
16
+ { name = "Stefan Tatschner", email = "stefan.tatschner@mailbox.org" }
17
+ ]
18
+ requires-python = ">=3.14"
19
+ dependencies = [
20
+ "sdbus>=0.14.2",
21
+ ]
22
+
23
+ [project.scripts]
24
+ "mprisctl" = "pympris2.cli:main"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "mypy>=2.1.0",
29
+ "reuse>=6.2.0",
30
+ "ruff>=0.15.16",
31
+ "ty>=0.0.46",
32
+ ]
33
+
34
+ [tool.mypy]
35
+ strict = true
36
+ native_parser = true
37
+ num_workers = 4
38
+
39
+
40
+ [tool.ruff.lint]
41
+ select = [
42
+ "B", # flake8-bugbear
43
+ "A", # flake8-builtins
44
+ "ASYNC",# flake8-async
45
+ "C4", # flake8-comprehensions
46
+ "E", # pycodestlye
47
+ "F", # pyflakes
48
+ "FA", # flake8-future-annotations
49
+ "FLY", # flynt
50
+ "FURB", # refurb
51
+ "I", # isort
52
+ "IC", # flake8-import-conventions
53
+ "LOG", # flake8-logging
54
+ "PGH", # pygrep-hooks
55
+ "PIE", # flake8-pie
56
+ "PL", # pylint
57
+ "PTH", # flake8-use-pathlib
58
+ "Q", # flake8-quotes
59
+ "RSE", # flake8-raise
60
+ "RUF100", # unused-noqa
61
+ "TD", # flake8-todos
62
+ "TID", # flake8-tidy-imports
63
+ "UP", # pyupgrade
64
+ ]
@@ -0,0 +1,213 @@
1
+ # SPDX-FileCopyrightText: 2026 Stefan Tatschner
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ from typing import Self
6
+
7
+ from sdbus import (
8
+ DbusInterfaceCommonAsync,
9
+ dbus_method_async,
10
+ dbus_property_async,
11
+ sd_bus_open_user,
12
+ )
13
+ from sdbus_async.dbus_daemon import FreedesktopDbus
14
+
15
+
16
+ async def get_active_players() -> list[str]:
17
+ bus = sd_bus_open_user()
18
+ proxy = FreedesktopDbus(bus)
19
+
20
+ all_services = await proxy.list_names()
21
+
22
+ return [name for name in all_services if name.startswith("org.mpris.MediaPlayer2.")]
23
+
24
+
25
+ class MediaPlayer2Interface(
26
+ DbusInterfaceCommonAsync,
27
+ interface_name="org.mpris.MediaPlayer2",
28
+ ):
29
+ # Raise () → nothing
30
+ @dbus_method_async(method_name="Raise")
31
+ async def raise_(self) -> None:
32
+ raise NotImplementedError
33
+
34
+ # Quit () → nothing
35
+ @dbus_method_async()
36
+ async def quit(self) -> None:
37
+ raise NotImplementedError
38
+
39
+ # CanQuit b Read only
40
+ @dbus_property_async("b")
41
+ def can_quit(self) -> bool:
42
+ raise NotImplementedError
43
+
44
+ # Fullscreen b Read/Write
45
+ @dbus_property_async("b")
46
+ def fullscreen(self) -> bool:
47
+ raise NotImplementedError
48
+
49
+ # CanSetFullscreen b Read only
50
+ @dbus_property_async("b")
51
+ def can_set_fullscreen(self) -> bool:
52
+ raise NotImplementedError
53
+
54
+ # CanRaise b Read only
55
+ @dbus_property_async("b")
56
+ def can_raise(self) -> bool:
57
+ raise NotImplementedError
58
+
59
+ # HasTrackList b Read only
60
+ @dbus_property_async("b")
61
+ def has_track_list(self) -> bool:
62
+ raise NotImplementedError
63
+
64
+ # Identity s Read only
65
+ @dbus_property_async("s")
66
+ def identity(self) -> str:
67
+ raise NotImplementedError
68
+
69
+ # DesktopEntry s Read only
70
+ @dbus_property_async("s")
71
+ def desktop_entry(self) -> str:
72
+ raise NotImplementedError
73
+
74
+ # SupportedUriSchemes as Read only
75
+ @dbus_property_async("as")
76
+ def supported_uri_schemes(self) -> list[str]:
77
+ raise NotImplementedError
78
+
79
+ # SupportedMimeTypes as Read only
80
+ @dbus_property_async("as")
81
+ def supported_mime_types(self) -> list[str]:
82
+ raise NotImplementedError
83
+
84
+
85
+ class MediaPlayer2PlayerInterface(
86
+ DbusInterfaceCommonAsync,
87
+ interface_name="org.mpris.MediaPlayer2.Player",
88
+ ):
89
+ # Next () → nothing
90
+ @dbus_method_async()
91
+ async def next(self) -> None:
92
+ raise NotImplementedError
93
+
94
+ # Previous () → nothing
95
+ @dbus_method_async()
96
+ async def previous(self) -> None:
97
+ raise NotImplementedError
98
+
99
+ # Pause () → nothing
100
+ @dbus_method_async()
101
+ async def pause(self) -> None:
102
+ raise NotImplementedError
103
+
104
+ # PlayPause () → nothing
105
+ @dbus_method_async()
106
+ async def play_pause(self) -> None:
107
+ raise NotImplementedError
108
+
109
+ # Stop () → nothing
110
+ @dbus_method_async()
111
+ async def stop(self) -> None:
112
+ raise NotImplementedError
113
+
114
+ # Play () → nothing
115
+ @dbus_method_async()
116
+ async def play(self) -> None:
117
+ raise NotImplementedError
118
+
119
+ # Seek (x: Offset) → nothing
120
+ @dbus_method_async("x")
121
+ async def seek(self) -> None:
122
+ raise NotImplementedError
123
+
124
+ # SetPosition (o: TrackId, x: Position) → nothing
125
+ @dbus_method_async("ox")
126
+ async def set_position(self) -> None:
127
+ raise NotImplementedError
128
+
129
+ # OpenUri (s: Uri) → nothing
130
+ @dbus_method_async("s")
131
+ async def open_uri(self, uri: str) -> None:
132
+ raise NotImplementedError
133
+
134
+ # PlaybackStatus s ( Playback_Status) Read only
135
+ @dbus_property_async("s")
136
+ def playback_status(self) -> str:
137
+ raise NotImplementedError
138
+
139
+ # LoopStatus s ( Loop_Status) Read/Write
140
+ @dbus_property_async("s")
141
+ def loop_status(self) -> str:
142
+ raise NotImplementedError
143
+
144
+ # Rate d ( Playback_Rate) Read/Write
145
+ @dbus_property_async("d")
146
+ def rate(self) -> int:
147
+ raise NotImplementedError
148
+
149
+ # Shuffle b Read/Write
150
+ @dbus_property_async("b")
151
+ def shuffle(self) -> bool:
152
+ raise NotImplementedError
153
+
154
+ # Metadata a{sv} ( Metadata_Map) Read only
155
+ @dbus_property_async("a{sv}")
156
+ def metadata(self) -> dict[str, tuple[str, str | int]]:
157
+ raise NotImplementedError
158
+
159
+ # Volume d ( Volume) Read/Write
160
+ @dbus_property_async("d")
161
+ def volume(self) -> int:
162
+ raise NotImplementedError
163
+
164
+ # Position x ( Time_In_Us) Read only
165
+ @dbus_property_async("x")
166
+ def position(self) -> int:
167
+ raise NotImplementedError
168
+
169
+ # MinimumRate d ( Playback_Rate) Read only
170
+ @dbus_property_async("d")
171
+ def minumum_rate(self) -> int:
172
+ raise NotImplementedError
173
+
174
+ # MaximumRate d ( Playback_Rate) Read only
175
+ @dbus_property_async("b")
176
+ def maximum_rate(self) -> int:
177
+ raise NotImplementedError
178
+
179
+ # CanGoNext b Read only
180
+ @dbus_property_async("b")
181
+ def can_go_next(self) -> bool:
182
+ raise NotImplementedError
183
+
184
+ # CanGoPrevious b Read only
185
+ @dbus_property_async("b")
186
+ def can_go_previous(self) -> bool:
187
+ raise NotImplementedError
188
+
189
+ # CanPlay b Read only
190
+ @dbus_property_async("b")
191
+ def can_play(self) -> bool:
192
+ raise NotImplementedError
193
+
194
+ # CanPause b Read only
195
+ @dbus_property_async("b")
196
+ def can_pause(self) -> bool:
197
+ raise NotImplementedError
198
+
199
+ # CanSeek b Read only
200
+ @dbus_property_async("b")
201
+ def can_seek(self) -> bool:
202
+ raise NotImplementedError
203
+
204
+ # CanControl b Read only
205
+ @dbus_property_async("b")
206
+ def can_control(self) -> bool:
207
+ raise NotImplementedError
208
+
209
+
210
+ class MPRISInterface(MediaPlayer2Interface, MediaPlayer2PlayerInterface):
211
+ @classmethod
212
+ def connect(cls, player: str) -> Self:
213
+ return cls.new_proxy(player, "/org/mpris/MediaPlayer2")
@@ -0,0 +1,152 @@
1
+ # SPDX-FileCopyrightText: 2026 Stefan Tatschner
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import sys
9
+
10
+ from pympris2 import MPRISInterface, get_active_players
11
+
12
+
13
+ def parse_args() -> tuple[argparse.Namespace, argparse.ArgumentParser]:
14
+ parser = argparse.ArgumentParser()
15
+ parser.add_argument(
16
+ "-p",
17
+ "--player",
18
+ metavar="NAME",
19
+ help="connect to this mpris2 player, default: first available player",
20
+ )
21
+
22
+ subparsers = parser.add_subparsers(title="available commands", required=True)
23
+
24
+ sp = subparsers.add_parser("list", help="list the names of available players")
25
+ sp.set_defaults(cmd="list")
26
+
27
+ sp = subparsers.add_parser("play", help="starts or resumes playback")
28
+ sp.set_defaults(cmd="play")
29
+
30
+ sp = subparsers.add_parser("pause", help="pause playback")
31
+ sp.set_defaults(cmd="pause")
32
+
33
+ sp = subparsers.add_parser("play_pause", help="pause or pause playback")
34
+ sp.set_defaults(cmd="play_pause")
35
+
36
+ sp = subparsers.add_parser("stop", help="stop playback")
37
+ sp.set_defaults(cmd="stop")
38
+
39
+ sp = subparsers.add_parser("next", help="skip to the next track in the tracklist")
40
+ sp.set_defaults(cmd="next")
41
+
42
+ sp = subparsers.add_parser(
43
+ "previous", help="skip to the next track in the tracklist"
44
+ )
45
+ sp.set_defaults(cmd="previous")
46
+
47
+ sp = subparsers.add_parser("status", help="current playback status")
48
+ sp.set_defaults(cmd="status")
49
+
50
+ sp = subparsers.add_parser(
51
+ "metadata", help="print metadata information for the current track"
52
+ )
53
+ sp.add_argument("--json", action="store_true", help="output as json")
54
+ sp.set_defaults(cmd="metadata")
55
+
56
+ sp = subparsers.add_parser("volume", help="print or set the volume to LEVEL")
57
+ sp.add_argument(
58
+ "VOLUME",
59
+ metavar="LEVEL",
60
+ type=float,
61
+ nargs="?",
62
+ help="value to set volume to from 0.0 to 1.0",
63
+ )
64
+ sp.set_defaults(cmd="volume")
65
+
66
+ sp = subparsers.add_parser("open", help="open the given URI")
67
+ sp.add_argument(
68
+ "URI",
69
+ help="either file path or remote URL",
70
+ )
71
+ sp.set_defaults(cmd="open")
72
+
73
+ sp = subparsers.add_parser("loop", help="print or set the loop status")
74
+ sp.add_argument(
75
+ "STATUS",
76
+ metavar="STATUS",
77
+ nargs="?",
78
+ choices=["None", "Track", "Playlist"],
79
+ help="'None', 'Track', or 'Playlist'",
80
+ )
81
+ sp.set_defaults(cmd="loop")
82
+
83
+ return parser.parse_args(), parser
84
+
85
+
86
+ async def get_player_target(chosen: str | None) -> str:
87
+ avail_players = await get_active_players()
88
+
89
+ if isinstance(chosen, str) and chosen in avail_players:
90
+ return chosen
91
+
92
+ return avail_players[0] if avail_players else ""
93
+
94
+
95
+ def _remove_dbus_types(d: dict[str, tuple[str, str | int]]) -> dict[str, str | int]:
96
+ out = {}
97
+ for k, v in d.items():
98
+ out[k] = v[1]
99
+ return out
100
+
101
+
102
+ async def _main() -> None: # noqa: PLR0912
103
+ args, parser = parse_args()
104
+
105
+ if args.cmd == "list":
106
+ for player in await get_active_players():
107
+ print(player)
108
+ sys.exit(0)
109
+
110
+ player = await get_player_target(args.player)
111
+ if not player:
112
+ parser.error("player not available")
113
+
114
+ proxy = MPRISInterface.connect(player)
115
+ match args.cmd:
116
+ case "play":
117
+ await proxy.play()
118
+ case "play_pause":
119
+ await proxy.play_pause()
120
+ case "pause":
121
+ await proxy.pause()
122
+ case "stop":
123
+ await proxy.stop()
124
+ case "next":
125
+ await proxy.next()
126
+ case "previous":
127
+ await proxy.previous()
128
+ case "status":
129
+ print(await proxy.playback_status)
130
+ case "volume":
131
+ if args.VOLUME:
132
+ await proxy.volume.set_async(args.VOLUME)
133
+ else:
134
+ print(await proxy.volume)
135
+ case "loop":
136
+ if args.STATUS:
137
+ await proxy.loop_status.set_async(args.STATUS)
138
+ else:
139
+ print(await proxy.loop_status)
140
+ case "open":
141
+ await proxy.open_uri(args.URI)
142
+ case "metadata":
143
+ m = _remove_dbus_types(await proxy.metadata)
144
+ if args.json:
145
+ print(json.dumps(m))
146
+ else:
147
+ for k, v in m.items():
148
+ print(f"{k}: {v}")
149
+
150
+
151
+ def main() -> None:
152
+ asyncio.run(_main())
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2026 Stefan Tatschner
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2