python-musicpd 0.9.1__tar.gz → 0.9.3__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.
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/CHANGES.txt +16 -0
- python_musicpd-0.9.3/PKG-INFO +34 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/README.rst +1 -1
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/_commands.rst +10 -1
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/commands.txt +1 -1
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/conf.py +8 -8
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/musicpd.py +116 -41
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/use.rst +24 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/musicpd.py +116 -41
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/pyproject.toml +14 -5
- python_musicpd-0.9.3/python_musicpd.egg-info/PKG-INFO +34 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/requires.txt +1 -0
- python_musicpd-0.9.1/PKG-INFO +0 -199
- python_musicpd-0.9.1/python_musicpd.egg-info/PKG-INFO +0 -199
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/LICENSE.txt +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/MANIFEST.in +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/commands.rst +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/contribute.rst +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/doc.rst +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/client.py +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/connect.py +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/connect_host.py +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/exceptions.py +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/findadd.py +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/playback.py +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples.rst +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/index.rst +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/python-musicpd.svg +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/SOURCES.txt +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/dependency_links.txt +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/top_level.txt +0 -0
- {python_musicpd-0.9.1 → python_musicpd-0.9.3}/setup.cfg +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
python-musicpd Changes List
|
|
2
2
|
===========================
|
|
3
3
|
|
|
4
|
+
Changes in 0.9.3
|
|
5
|
+
----------------
|
|
6
|
+
|
|
7
|
+
* Add support for the list command grouping (Group marker)
|
|
8
|
+
* Fix deprecation in pyproject.toml
|
|
9
|
+
* Minimum python version raised to 3.11
|
|
10
|
+
|
|
11
|
+
Changes in 0.9.2
|
|
12
|
+
----------------
|
|
13
|
+
|
|
14
|
+
* Add missing build-backend in pyproject.toml
|
|
15
|
+
* Add protocol commands
|
|
16
|
+
* Add stickernamestypes and stickertypes commands
|
|
17
|
+
* Add searchplaylist command
|
|
18
|
+
* Fixed deprecation in pyproject.toml
|
|
19
|
+
|
|
4
20
|
Changes in 0.9.1
|
|
5
21
|
----------------
|
|
6
22
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-musicpd
|
|
3
|
+
Version: 0.9.3
|
|
4
|
+
Summary: An MPD (Music Player Daemon) client library written in pure Python.
|
|
5
|
+
Author-email: Kaliko Jack <kaliko@azylum.org>
|
|
6
|
+
Project-URL: Homepage, https://kaliko.me/python-musicpd/
|
|
7
|
+
Keywords: mpd,Music Player Daemon
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/x-rst
|
|
16
|
+
License-File: LICENSE.txt
|
|
17
|
+
Provides-Extra: sphinx
|
|
18
|
+
Requires-Dist: Sphinx>=5.3.0; extra == "sphinx"
|
|
19
|
+
Requires-Dist: sphinx-rtd-theme>=3.1.0; extra == "sphinx"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
Music Player Daemon client module
|
|
23
|
+
*********************************
|
|
24
|
+
|
|
25
|
+
An MPD (Music Player Daemon) client library written in pure Python.
|
|
26
|
+
|
|
27
|
+
----
|
|
28
|
+
|
|
29
|
+
:Documentation: https://kaliko.gitlab.io/python-musicpd
|
|
30
|
+
:MPD Protocol: https://www.musicpd.org/doc/html/protocol.html
|
|
31
|
+
:Code: https://gitlab.com/kaliko/python-musicpd
|
|
32
|
+
:Dependencies: None
|
|
33
|
+
:Compatibility: Python 3.11+
|
|
34
|
+
:Licence: GNU LGPLv3
|
|
@@ -9,5 +9,5 @@ An MPD (Music Player Daemon) client library written in pure Python.
|
|
|
9
9
|
:MPD Protocol: https://www.musicpd.org/doc/html/protocol.html
|
|
10
10
|
:Code: https://gitlab.com/kaliko/python-musicpd
|
|
11
11
|
:Dependencies: None
|
|
12
|
-
:Compatibility: Python 3.
|
|
12
|
+
:Compatibility: Python 3.11+
|
|
13
13
|
:Licence: GNU LGPLv3
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Below the commands list last updated for v0.9.
|
|
1
|
+
Below the commands list last updated for v0.9.2.
|
|
2
2
|
|
|
3
3
|
Querying MPD’s status
|
|
4
4
|
^^^^^^^^^^^^^^^^^^^^^
|
|
@@ -79,6 +79,7 @@ Protocol documentation: `Stored playlists <https://mpd.readthedocs.io/en/latest/
|
|
|
79
79
|
|
|
80
80
|
* **listplaylist** -> list
|
|
81
81
|
* **listplaylistinfo** -> list[dict]
|
|
82
|
+
* **searchplaylist** -> list[dict]
|
|
82
83
|
* **listplaylists** -> dict
|
|
83
84
|
* **load** -> None
|
|
84
85
|
* **playlistadd** -> None
|
|
@@ -135,6 +136,8 @@ Protocol documentation: `Stickers <https://mpd.readthedocs.io/en/latest/protocol
|
|
|
135
136
|
* **sticker list** -> list
|
|
136
137
|
* **sticker find** -> list[dict]
|
|
137
138
|
* **stickernames** -> list
|
|
139
|
+
* **stickertypes** -> list
|
|
140
|
+
* **stickernamestypes** -> list
|
|
138
141
|
|
|
139
142
|
Connection settings
|
|
140
143
|
^^^^^^^^^^^^^^^^^^^
|
|
@@ -149,6 +152,12 @@ Protocol documentation: `Connection settings <https://mpd.readthedocs.io/en/late
|
|
|
149
152
|
* **tagtypes enable** -> None
|
|
150
153
|
* **tagtypes clear** -> None
|
|
151
154
|
* **tagtypes all** -> None
|
|
155
|
+
* **protocol** -> list
|
|
156
|
+
* **protocol disable** -> None
|
|
157
|
+
* **protocol enable** -> None
|
|
158
|
+
* **protocol clear** -> None
|
|
159
|
+
* **protocol all** -> None
|
|
160
|
+
* **protocol available** -> list
|
|
152
161
|
|
|
153
162
|
Partition Commands
|
|
154
163
|
^^^^^^^^^^^^^^^^^^
|
|
@@ -75,7 +75,7 @@ count group <str> -> fetch_object
|
|
|
75
75
|
getfingerprint <str> -> fetch_object
|
|
76
76
|
find <str> <str> [<str> <str>]... -> fetch_songs
|
|
77
77
|
findadd <str> <str> [<str> <str>] -> fetch_nothing
|
|
78
|
-
list <str> [<str> <str>]...[
|
|
78
|
+
list <str> [<str> <str>]...[<Group>]... -> fetch_list
|
|
79
79
|
listall [<str>] -> fetch_database
|
|
80
80
|
listallinfo [<str>] -> fetch_database
|
|
81
81
|
listfiles <str> -> fetch_database
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
-
# SPDX-FileCopyrightText: 2018-
|
|
2
|
+
# SPDX-FileCopyrightText: 2018-2026 kaliko <kaliko@azylum.org>
|
|
3
3
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
4
4
|
#
|
|
5
5
|
# Python MPD Module documentation build configuration file, created by
|
|
@@ -57,9 +57,9 @@ source_suffix = '.rst'
|
|
|
57
57
|
master_doc = 'index'
|
|
58
58
|
|
|
59
59
|
# General information about the project.
|
|
60
|
-
project =
|
|
60
|
+
project = 'Python MPD Module'
|
|
61
61
|
from datetime import datetime
|
|
62
|
-
copyright =
|
|
62
|
+
copyright = '2012-{}, {}'.format(datetime.now().year, __author__)
|
|
63
63
|
author = __author__
|
|
64
64
|
|
|
65
65
|
# The version info for the project you're documenting, acts as replacement for
|
|
@@ -146,7 +146,7 @@ html_theme = 'classic'
|
|
|
146
146
|
# The name for this set of Sphinx documents.
|
|
147
147
|
# "<project> v<release> documentation" by default.
|
|
148
148
|
#
|
|
149
|
-
# html_title =
|
|
149
|
+
# html_title = 'Python MPD Module v0.0.1'
|
|
150
150
|
|
|
151
151
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
152
152
|
#
|
|
@@ -273,8 +273,8 @@ latex_elements = {
|
|
|
273
273
|
# (source start file, target name, title,
|
|
274
274
|
# author, documentclass [howto, manual, or own class]).
|
|
275
275
|
latex_documents = [
|
|
276
|
-
(master_doc, 'PythonMPDModule.tex',
|
|
277
|
-
|
|
276
|
+
(master_doc, 'PythonMPDModule.tex', 'Python MPD Module Documentation',
|
|
277
|
+
'kaliko', 'manual'),
|
|
278
278
|
]
|
|
279
279
|
|
|
280
280
|
# The name of an image file (relative to this directory) to place at the top of
|
|
@@ -315,7 +315,7 @@ latex_documents = [
|
|
|
315
315
|
# One entry per manual page. List of tuples
|
|
316
316
|
# (source start file, name, description, authors, manual section).
|
|
317
317
|
man_pages = [
|
|
318
|
-
(master_doc, 'pythonmpdmodule',
|
|
318
|
+
(master_doc, 'pythonmpdmodule', 'Python MPD Module Documentation',
|
|
319
319
|
[author], 1)
|
|
320
320
|
]
|
|
321
321
|
|
|
@@ -330,7 +330,7 @@ man_pages = [
|
|
|
330
330
|
# (source start file, target name, title, author,
|
|
331
331
|
# dir menu entry, description, category)
|
|
332
332
|
texinfo_documents = [
|
|
333
|
-
(master_doc, 'PythonMPDModule',
|
|
333
|
+
(master_doc, 'PythonMPDModule', 'Python MPD Module Documentation',
|
|
334
334
|
author, 'PythonMPDModule', 'One line description of project.',
|
|
335
335
|
'Miscellaneous'),
|
|
336
336
|
]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
# SPDX-FileCopyrightText: 2012-2024 kaliko <kaliko@azylum.org>
|
|
1
|
+
# SPDX-FileCopyrightText: 2012-2026 kaliko <kaliko@azylum.org>
|
|
3
2
|
# SPDX-FileCopyrightText: 2021 Wonko der Verständige <wonko@hanstool.org>
|
|
4
3
|
# SPDX-FileCopyrightText: 2019 Naglis Jonaitis <naglis@mailbox.org>
|
|
5
4
|
# SPDX-FileCopyrightText: 2019 Bart Van Loon <bbb@bbbart.be>
|
|
@@ -11,7 +10,6 @@
|
|
|
11
10
|
import logging
|
|
12
11
|
import os
|
|
13
12
|
import socket
|
|
14
|
-
|
|
15
13
|
from functools import wraps
|
|
16
14
|
|
|
17
15
|
HELLO_PREFIX = "OK MPD "
|
|
@@ -19,14 +17,14 @@ ERROR_PREFIX = "ACK "
|
|
|
19
17
|
SUCCESS = "OK"
|
|
20
18
|
NEXT = "list_OK"
|
|
21
19
|
#: Module version
|
|
22
|
-
VERSION = '0.9.
|
|
20
|
+
VERSION = '0.9.3'
|
|
23
21
|
#: Seconds before a connection attempt times out
|
|
24
22
|
#: (overriden by :envvar:`MPD_TIMEOUT` env. var.)
|
|
25
23
|
CONNECTION_TIMEOUT = 30
|
|
26
24
|
#: Socket timeout in second > 0 (Default is :py:obj:`None` for no timeout)
|
|
27
25
|
SOCKET_TIMEOUT = None
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
def iterator_wrapper(func):
|
|
@@ -40,8 +38,7 @@ def iterator_wrapper(func):
|
|
|
40
38
|
|
|
41
39
|
def iterator(gen):
|
|
42
40
|
try:
|
|
43
|
-
|
|
44
|
-
yield item
|
|
41
|
+
yield from gen
|
|
45
42
|
finally:
|
|
46
43
|
instance._iterating = False
|
|
47
44
|
return iterator(generator)
|
|
@@ -97,7 +94,6 @@ class Range:
|
|
|
97
94
|
return str(int(item))
|
|
98
95
|
except (TypeError, ValueError) as err:
|
|
99
96
|
raise CommandError(f'Not an integer: "{item}"') from err
|
|
100
|
-
return item
|
|
101
97
|
|
|
102
98
|
def _check(self):
|
|
103
99
|
if not isinstance(self.tpl, tuple):
|
|
@@ -118,6 +114,22 @@ class Range:
|
|
|
118
114
|
raise CommandError(f'Wrong range: {self.lower} > {self.upper}')
|
|
119
115
|
|
|
120
116
|
|
|
117
|
+
class Group:
|
|
118
|
+
"""Mark a tag used with the ``group`` keyword of the :py:obj:`list` command.
|
|
119
|
+
|
|
120
|
+
>>> client.list('album', '(genre == "Punk")', musicpd.Group('artist'))
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, tag):
|
|
124
|
+
self.tag = tag
|
|
125
|
+
|
|
126
|
+
def __str__(self):
|
|
127
|
+
return f'group {self.tag}'
|
|
128
|
+
|
|
129
|
+
def __repr__(self):
|
|
130
|
+
return f'Group({self.tag!r})'
|
|
131
|
+
|
|
132
|
+
|
|
121
133
|
class _NotConnected:
|
|
122
134
|
|
|
123
135
|
def __getattr__(self, attr):
|
|
@@ -184,6 +196,7 @@ class MPDClient:
|
|
|
184
196
|
|
|
185
197
|
.. note:: This is the version of the protocol spoken, not the real version of the daemon."""
|
|
186
198
|
self._reset()
|
|
199
|
+
# fmt: off
|
|
187
200
|
self._commands = {
|
|
188
201
|
# Querying MPD’s status # querying-mpd-s-status
|
|
189
202
|
"clearerror": self._fetch_nothing,
|
|
@@ -241,6 +254,7 @@ class MPDClient:
|
|
|
241
254
|
# Stored playlists # stored-playlists
|
|
242
255
|
"listplaylist": self._fetch_list,
|
|
243
256
|
"listplaylistinfo": self._fetch_songs,
|
|
257
|
+
"searchplaylist": self._fetch_songs,
|
|
244
258
|
"listplaylists": self._fetch_playlists,
|
|
245
259
|
"load": self._fetch_nothing,
|
|
246
260
|
"playlistadd": self._fetch_nothing,
|
|
@@ -282,6 +296,8 @@ class MPDClient:
|
|
|
282
296
|
"sticker list": self._fetch_list,
|
|
283
297
|
"sticker find": self._fetch_songs,
|
|
284
298
|
"stickernames": self._fetch_list,
|
|
299
|
+
"stickertypes": self._fetch_list,
|
|
300
|
+
"stickernamestypes": self._fetch_list,
|
|
285
301
|
# Connection settings # connection-settings
|
|
286
302
|
"close": None,
|
|
287
303
|
"kill": None,
|
|
@@ -293,6 +309,12 @@ class MPDClient:
|
|
|
293
309
|
"tagtypes enable": self._fetch_nothing,
|
|
294
310
|
"tagtypes clear": self._fetch_nothing,
|
|
295
311
|
"tagtypes all": self._fetch_nothing,
|
|
312
|
+
"protocol": self._fetch_list,
|
|
313
|
+
"protocol disable": self._fetch_nothing,
|
|
314
|
+
"protocol enable": self._fetch_nothing,
|
|
315
|
+
"protocol clear": self._fetch_nothing,
|
|
316
|
+
"protocol all": self._fetch_nothing,
|
|
317
|
+
"protocol available": self._fetch_list,
|
|
296
318
|
# Partition Commands # partition-commands
|
|
297
319
|
"partition": self._fetch_nothing,
|
|
298
320
|
"listpartitions": self._fetch_list,
|
|
@@ -318,21 +340,19 @@ class MPDClient:
|
|
|
318
340
|
"readmessages": self._fetch_messages,
|
|
319
341
|
"sendmessage": self._fetch_nothing,
|
|
320
342
|
}
|
|
343
|
+
# fmt: on
|
|
321
344
|
#: host used with the current connection (:py:obj:`str`)
|
|
322
|
-
self.host =
|
|
345
|
+
self.host = 'localhost'
|
|
323
346
|
#: password detected in :envvar:`MPD_HOST` environment variable (:py:obj:`str`)
|
|
324
347
|
self.pwd = None
|
|
325
348
|
#: port used with the current connection (:py:obj:`int`, :py:obj:`str`)
|
|
326
|
-
self.port =
|
|
349
|
+
self.port = os.getenv('MPD_PORT', '6600')
|
|
327
350
|
self._get_envvars()
|
|
328
351
|
|
|
329
352
|
def _get_envvars(self):
|
|
330
353
|
"""
|
|
331
354
|
Retrieve MPD env. var. to overrides default "localhost:6600"
|
|
332
355
|
"""
|
|
333
|
-
# Set some defaults
|
|
334
|
-
self.host = 'localhost'
|
|
335
|
-
self.port = os.getenv('MPD_PORT', '6600')
|
|
336
356
|
_host = os.getenv('MPD_HOST', '')
|
|
337
357
|
if _host:
|
|
338
358
|
# If password is set: MPD_HOST=pass@host
|
|
@@ -340,30 +360,30 @@ class MPDClient:
|
|
|
340
360
|
mpd_host_env = _host.split('@', 1)
|
|
341
361
|
if mpd_host_env[0]:
|
|
342
362
|
# A password is actually set
|
|
343
|
-
|
|
363
|
+
logger.debug('password detected in MPD_HOST, set client pwd attribute')
|
|
344
364
|
self.pwd = mpd_host_env[0]
|
|
345
365
|
if mpd_host_env[1]:
|
|
346
366
|
self.host = mpd_host_env[1]
|
|
347
|
-
|
|
367
|
+
logger.debug('host detected in MPD_HOST: %s', self.host)
|
|
348
368
|
elif mpd_host_env[1]:
|
|
349
369
|
# No password set but leading @ is an abstract socket
|
|
350
370
|
self.host = '@'+mpd_host_env[1]
|
|
351
|
-
|
|
371
|
+
logger.debug('host detected in MPD_HOST: %s (abstract socket)', self.host)
|
|
352
372
|
else:
|
|
353
373
|
# MPD_HOST is a plain host
|
|
354
374
|
self.host = _host
|
|
355
|
-
|
|
375
|
+
logger.debug('host detected in MPD_HOST: %s', self.host)
|
|
356
376
|
else:
|
|
357
377
|
# Is socket there
|
|
358
378
|
xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR', '/run')
|
|
359
379
|
rundir = os.path.join(xdg_runtime_dir, 'mpd/socket')
|
|
360
380
|
if os.path.exists(rundir):
|
|
361
381
|
self.host = rundir
|
|
362
|
-
|
|
382
|
+
logger.debug('host detected in ${XDG_RUNTIME_DIR}/run: %s (unix socket)', self.host)
|
|
363
383
|
_mpd_timeout = os.getenv('MPD_TIMEOUT', '')
|
|
364
384
|
if _mpd_timeout.isdigit():
|
|
365
385
|
self.mpd_timeout = int(_mpd_timeout)
|
|
366
|
-
|
|
386
|
+
logger.debug('timeout detected in MPD_TIMEOUT: %d', self.mpd_timeout)
|
|
367
387
|
else: # Use CONNECTION_TIMEOUT as default even if MPD_TIMEOUT carries gargage
|
|
368
388
|
self.mpd_timeout = CONNECTION_TIMEOUT
|
|
369
389
|
|
|
@@ -388,8 +408,7 @@ class MPDClient:
|
|
|
388
408
|
|
|
389
409
|
def _send(self, command, args):
|
|
390
410
|
if self._command_list is not None:
|
|
391
|
-
raise CommandListError("Cannot use send_
|
|
392
|
-
command.replace(" ", "_"))
|
|
411
|
+
raise CommandListError("Cannot use send_{} in a command list".format(command.replace(" ", "_")))
|
|
393
412
|
self._write_command(command, args)
|
|
394
413
|
retval = self._commands[command]
|
|
395
414
|
if retval is not None:
|
|
@@ -440,6 +459,8 @@ class MPDClient:
|
|
|
440
459
|
for arg in args:
|
|
441
460
|
if isinstance(arg, tuple):
|
|
442
461
|
parts.append(f'{Range(arg)!s}')
|
|
462
|
+
elif isinstance(arg, Group):
|
|
463
|
+
parts.append(f'group "{escape(str(arg.tag))}"')
|
|
443
464
|
else:
|
|
444
465
|
parts.append(f'"{escape(str(arg))}"')
|
|
445
466
|
if '\n' in ' '.join(parts):
|
|
@@ -494,13 +515,67 @@ class MPDClient:
|
|
|
494
515
|
pair = self._read_pair(separator, binary=binary)
|
|
495
516
|
|
|
496
517
|
def _read_list(self):
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
518
|
+
"""Read a ``list``-style response, flat or grouped.
|
|
519
|
+
|
|
520
|
+
Two response shapes are handled:
|
|
521
|
+
|
|
522
|
+
* **flat** — every line has the same key (e.g. ``Album: Foo`` then
|
|
523
|
+
``Album: Bar``): values are yielded as a plain list.
|
|
524
|
+
* **grouped** — lines carry mixed keys, as returned by the ``list``
|
|
525
|
+
command with the ``group`` keyword. Group tags are only sent when
|
|
526
|
+
their value changes, so each row is rebuilt by propagating the
|
|
527
|
+
current group values.
|
|
528
|
+
|
|
529
|
+
For grouped responses, ``delimiters`` tracks the active tags in order
|
|
530
|
+
of appearance (the group hierarchy, ending with the listed tag). When
|
|
531
|
+
a key already present in the current row is seen, the row is yielded
|
|
532
|
+
and the ``while`` loop pops deeper tags from both the row and
|
|
533
|
+
``delimiters`` until the changed key is reached, carrying the
|
|
534
|
+
shallower group values over to the next row.
|
|
535
|
+
|
|
536
|
+
Example::
|
|
537
|
+
|
|
538
|
+
Artist: Crass
|
|
539
|
+
Album: Stations
|
|
540
|
+
Album: Feeding
|
|
541
|
+
Artist: The Clash
|
|
542
|
+
Album: London Calling
|
|
543
|
+
|
|
544
|
+
yields::
|
|
545
|
+
|
|
546
|
+
[{'artist': 'Crass', 'album': 'Stations'},
|
|
547
|
+
{'artist': 'Crass', 'album': 'Feeding'},
|
|
548
|
+
{'artist': 'The Clash', 'album': 'London Calling'}]
|
|
549
|
+
|
|
550
|
+
.. note::
|
|
551
|
+
Grouping is detected from the response (mixed keys), not from the
|
|
552
|
+
command arguments, so plain and grouped ``list`` calls share the
|
|
553
|
+
same fetch method.
|
|
554
|
+
"""
|
|
555
|
+
pairs = list(self._read_pairs())
|
|
556
|
+
if not pairs:
|
|
557
|
+
return
|
|
558
|
+
if len({key for key, _ in pairs}) == 1:
|
|
559
|
+
for _, value in pairs:
|
|
560
|
+
yield value
|
|
561
|
+
return
|
|
562
|
+
# Grouped list: group tags are only sent when their value changes,
|
|
563
|
+
# hence the dynamic delimiters handling.
|
|
564
|
+
delimiters = []
|
|
565
|
+
obj = {}
|
|
566
|
+
for key, value in pairs:
|
|
567
|
+
key = key.lower()
|
|
568
|
+
if key not in delimiters:
|
|
569
|
+
delimiters.append(key)
|
|
570
|
+
if obj and key in obj:
|
|
571
|
+
yield obj
|
|
572
|
+
obj = obj.copy()
|
|
573
|
+
while delimiters[-1] != key:
|
|
574
|
+
obj.pop(delimiters[-1], None)
|
|
575
|
+
delimiters.pop()
|
|
576
|
+
obj[key] = value
|
|
577
|
+
if obj:
|
|
578
|
+
yield obj
|
|
504
579
|
|
|
505
580
|
def _read_playlist(self):
|
|
506
581
|
for _, value in self._read_pairs(":"):
|
|
@@ -542,7 +617,7 @@ class MPDClient:
|
|
|
542
617
|
def _fetch_item(self):
|
|
543
618
|
pairs = list(self._read_pairs())
|
|
544
619
|
if len(pairs) != 1:
|
|
545
|
-
|
|
620
|
+
raise ProtocolError(f"Expected 1 pair, got {len(pairs)}")
|
|
546
621
|
return pairs[0][1]
|
|
547
622
|
|
|
548
623
|
@iterator_wrapper
|
|
@@ -604,7 +679,7 @@ class MPDClient:
|
|
|
604
679
|
amount = int(obj['binary'])
|
|
605
680
|
try:
|
|
606
681
|
obj['data'] = self._read_binary(amount)
|
|
607
|
-
except
|
|
682
|
+
except OSError as err:
|
|
608
683
|
raise ConnectionError(f'Error reading binary content: {err}') from err
|
|
609
684
|
data_bytes = len(obj['data'])
|
|
610
685
|
if data_bytes != amount: # can we ever get there?
|
|
@@ -650,7 +725,7 @@ class MPDClient:
|
|
|
650
725
|
sock.settimeout(self.mpd_timeout)
|
|
651
726
|
sock.connect(path)
|
|
652
727
|
sock.settimeout(self.socket_timeout)
|
|
653
|
-
except
|
|
728
|
+
except OSError as socket_err:
|
|
654
729
|
raise ConnectionError(socket_err) from socket_err
|
|
655
730
|
return sock
|
|
656
731
|
|
|
@@ -664,20 +739,20 @@ class MPDClient:
|
|
|
664
739
|
gai = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
|
665
740
|
socket.SOCK_STREAM, socket.IPPROTO_TCP,
|
|
666
741
|
flags)
|
|
667
|
-
except
|
|
742
|
+
except OSError as gaierr:
|
|
668
743
|
raise ConnectionError(gaierr) from gaierr
|
|
669
744
|
for res in gai:
|
|
670
745
|
af, socktype, proto, _, sa = res
|
|
671
746
|
sock = None
|
|
672
747
|
try:
|
|
673
|
-
|
|
748
|
+
logger.debug('opening socket %s', sa)
|
|
674
749
|
sock = socket.socket(af, socktype, proto)
|
|
675
750
|
sock.settimeout(self.mpd_timeout)
|
|
676
751
|
sock.connect(sa)
|
|
677
752
|
sock.settimeout(self.socket_timeout)
|
|
678
753
|
return sock
|
|
679
|
-
except
|
|
680
|
-
|
|
754
|
+
except OSError as socket_err:
|
|
755
|
+
logger.debug('opening socket %s failed: %s', sa, socket_err)
|
|
681
756
|
err = socket_err
|
|
682
757
|
if sock is not None:
|
|
683
758
|
sock.close()
|
|
@@ -714,17 +789,17 @@ class MPDClient:
|
|
|
714
789
|
host = self.host
|
|
715
790
|
else:
|
|
716
791
|
self.host = host
|
|
717
|
-
if
|
|
792
|
+
if port is None:
|
|
718
793
|
port = self.port
|
|
719
794
|
else:
|
|
720
795
|
self.port = port
|
|
721
796
|
if self._sock is not None:
|
|
722
797
|
raise ConnectionError("Already connected")
|
|
723
|
-
if host[0] in ['/', '@']:
|
|
724
|
-
|
|
798
|
+
if host and host[0] in ['/', '@']:
|
|
799
|
+
logger.debug('Connecting unix socket %s', host)
|
|
725
800
|
self._sock = self._connect_unix(host)
|
|
726
801
|
else:
|
|
727
|
-
|
|
802
|
+
logger.debug('Connecting tcp socket %s:%s (timeout: %ss)', host, port, self.mpd_timeout)
|
|
728
803
|
self._sock = self._connect_tcp(host, port)
|
|
729
804
|
self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape')
|
|
730
805
|
self._rbfile = self._sock.makefile("rb")
|
|
@@ -734,12 +809,12 @@ class MPDClient:
|
|
|
734
809
|
except:
|
|
735
810
|
self.disconnect()
|
|
736
811
|
raise
|
|
737
|
-
|
|
812
|
+
logger.debug('Connected')
|
|
738
813
|
|
|
739
814
|
@property
|
|
740
815
|
def socket_timeout(self):
|
|
741
816
|
"""Socket timeout in second (defaults to :py:obj:`SOCKET_TIMEOUT`).
|
|
742
|
-
Use :py:obj:`None` to disable socket
|
|
817
|
+
Use :py:obj:`None` to disable socket timeout.
|
|
743
818
|
|
|
744
819
|
:setter: Set the socket timeout (integer > 0)
|
|
745
820
|
:type: int or None
|
|
@@ -157,6 +157,30 @@ Empty start in range (i.e. ":END") are not possible and will raise a CommandErro
|
|
|
157
157
|
|
|
158
158
|
.. note:: Remember the use of a tuple is **optional**. Range can still be specified as a plain string ``"START:END"``.
|
|
159
159
|
|
|
160
|
+
Grouping list results
|
|
161
|
+
---------------------
|
|
162
|
+
|
|
163
|
+
The ``list`` command supports the ``group`` keyword to group results by one or
|
|
164
|
+
more tags (cf. `MPD protocol documentation`_). Use the :py:obj:`musicpd.Group`
|
|
165
|
+
marker as argument to send the ``group`` keyword:
|
|
166
|
+
|
|
167
|
+
.. code-block:: python
|
|
168
|
+
|
|
169
|
+
# list albums grouped by artist, filtered on genre
|
|
170
|
+
albums = client.list('album', '(genre == "Punk")', musicpd.Group('artist'))
|
|
171
|
+
# albums is a list of dicts, group values are propagated on each row:
|
|
172
|
+
# [{'artist': 'Ramones', 'album': 'Leave Home'},
|
|
173
|
+
# {'artist': 'The Clash', 'album': 'The Clash'}, ...]
|
|
174
|
+
|
|
175
|
+
The ``group`` keyword can be repeated to group by several tags:
|
|
176
|
+
|
|
177
|
+
.. code-block:: python
|
|
178
|
+
|
|
179
|
+
client.list('album', musicpd.Group('date'), musicpd.Group('artist'))
|
|
180
|
+
|
|
181
|
+
Without any :py:obj:`musicpd.Group` argument, ``list`` still returns a flat
|
|
182
|
+
list of tag values.
|
|
183
|
+
|
|
160
184
|
Iterators
|
|
161
185
|
----------
|
|
162
186
|
|