python-musicpd 0.9.2__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.2 → python_musicpd-0.9.3}/CHANGES.txt +7 -0
- {python_musicpd-0.9.2/python_musicpd.egg-info → python_musicpd-0.9.3}/PKG-INFO +4 -4
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/README.rst +1 -1
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/commands.txt +1 -1
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/conf.py +8 -8
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/musicpd.py +107 -41
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/use.rst +24 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/musicpd.py +107 -41
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/pyproject.toml +12 -3
- {python_musicpd-0.9.2 → python_musicpd-0.9.3/python_musicpd.egg-info}/PKG-INFO +4 -4
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/python_musicpd.egg-info/requires.txt +1 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/LICENSE.txt +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/MANIFEST.in +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/_commands.rst +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/commands.rst +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/contribute.rst +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/doc.rst +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/client.py +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/connect.py +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/connect_host.py +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/exceptions.py +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/findadd.py +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples/playback.py +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/examples.rst +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/index.rst +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/doc/source/python-musicpd.svg +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/python_musicpd.egg-info/SOURCES.txt +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/python_musicpd.egg-info/dependency_links.txt +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/python_musicpd.egg-info/top_level.txt +0 -0
- {python_musicpd-0.9.2 → python_musicpd-0.9.3}/setup.cfg +0 -0
|
@@ -1,6 +1,13 @@
|
|
|
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
|
+
|
|
4
11
|
Changes in 0.9.2
|
|
5
12
|
----------------
|
|
6
13
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-musicpd
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: An MPD (Music Player Daemon) client library written in pure Python.
|
|
5
5
|
Author-email: Kaliko Jack <kaliko@azylum.org>
|
|
6
|
-
License-Expression: LGPL-3.0-or-later
|
|
7
6
|
Project-URL: Homepage, https://kaliko.me/python-musicpd/
|
|
8
7
|
Keywords: mpd,Music Player Daemon
|
|
9
8
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -12,11 +11,12 @@ Classifier: Natural Language :: English
|
|
|
12
11
|
Classifier: Operating System :: OS Independent
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
13
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.11
|
|
16
15
|
Description-Content-Type: text/x-rst
|
|
17
16
|
License-File: LICENSE.txt
|
|
18
17
|
Provides-Extra: sphinx
|
|
19
18
|
Requires-Dist: Sphinx>=5.3.0; extra == "sphinx"
|
|
19
|
+
Requires-Dist: sphinx-rtd-theme>=3.1.0; extra == "sphinx"
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
22
|
Music Player Daemon client module
|
|
@@ -30,5 +30,5 @@ An MPD (Music Player Daemon) client library written in pure Python.
|
|
|
30
30
|
:MPD Protocol: https://www.musicpd.org/doc/html/protocol.html
|
|
31
31
|
:Code: https://gitlab.com/kaliko/python-musicpd
|
|
32
32
|
:Dependencies: None
|
|
33
|
-
:Compatibility: Python 3.
|
|
33
|
+
:Compatibility: Python 3.11+
|
|
34
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
|
|
@@ -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-2025 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,
|
|
@@ -327,21 +340,19 @@ class MPDClient:
|
|
|
327
340
|
"readmessages": self._fetch_messages,
|
|
328
341
|
"sendmessage": self._fetch_nothing,
|
|
329
342
|
}
|
|
343
|
+
# fmt: on
|
|
330
344
|
#: host used with the current connection (:py:obj:`str`)
|
|
331
|
-
self.host =
|
|
345
|
+
self.host = 'localhost'
|
|
332
346
|
#: password detected in :envvar:`MPD_HOST` environment variable (:py:obj:`str`)
|
|
333
347
|
self.pwd = None
|
|
334
348
|
#: port used with the current connection (:py:obj:`int`, :py:obj:`str`)
|
|
335
|
-
self.port =
|
|
349
|
+
self.port = os.getenv('MPD_PORT', '6600')
|
|
336
350
|
self._get_envvars()
|
|
337
351
|
|
|
338
352
|
def _get_envvars(self):
|
|
339
353
|
"""
|
|
340
354
|
Retrieve MPD env. var. to overrides default "localhost:6600"
|
|
341
355
|
"""
|
|
342
|
-
# Set some defaults
|
|
343
|
-
self.host = 'localhost'
|
|
344
|
-
self.port = os.getenv('MPD_PORT', '6600')
|
|
345
356
|
_host = os.getenv('MPD_HOST', '')
|
|
346
357
|
if _host:
|
|
347
358
|
# If password is set: MPD_HOST=pass@host
|
|
@@ -349,30 +360,30 @@ class MPDClient:
|
|
|
349
360
|
mpd_host_env = _host.split('@', 1)
|
|
350
361
|
if mpd_host_env[0]:
|
|
351
362
|
# A password is actually set
|
|
352
|
-
|
|
363
|
+
logger.debug('password detected in MPD_HOST, set client pwd attribute')
|
|
353
364
|
self.pwd = mpd_host_env[0]
|
|
354
365
|
if mpd_host_env[1]:
|
|
355
366
|
self.host = mpd_host_env[1]
|
|
356
|
-
|
|
367
|
+
logger.debug('host detected in MPD_HOST: %s', self.host)
|
|
357
368
|
elif mpd_host_env[1]:
|
|
358
369
|
# No password set but leading @ is an abstract socket
|
|
359
370
|
self.host = '@'+mpd_host_env[1]
|
|
360
|
-
|
|
371
|
+
logger.debug('host detected in MPD_HOST: %s (abstract socket)', self.host)
|
|
361
372
|
else:
|
|
362
373
|
# MPD_HOST is a plain host
|
|
363
374
|
self.host = _host
|
|
364
|
-
|
|
375
|
+
logger.debug('host detected in MPD_HOST: %s', self.host)
|
|
365
376
|
else:
|
|
366
377
|
# Is socket there
|
|
367
378
|
xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR', '/run')
|
|
368
379
|
rundir = os.path.join(xdg_runtime_dir, 'mpd/socket')
|
|
369
380
|
if os.path.exists(rundir):
|
|
370
381
|
self.host = rundir
|
|
371
|
-
|
|
382
|
+
logger.debug('host detected in ${XDG_RUNTIME_DIR}/run: %s (unix socket)', self.host)
|
|
372
383
|
_mpd_timeout = os.getenv('MPD_TIMEOUT', '')
|
|
373
384
|
if _mpd_timeout.isdigit():
|
|
374
385
|
self.mpd_timeout = int(_mpd_timeout)
|
|
375
|
-
|
|
386
|
+
logger.debug('timeout detected in MPD_TIMEOUT: %d', self.mpd_timeout)
|
|
376
387
|
else: # Use CONNECTION_TIMEOUT as default even if MPD_TIMEOUT carries gargage
|
|
377
388
|
self.mpd_timeout = CONNECTION_TIMEOUT
|
|
378
389
|
|
|
@@ -397,8 +408,7 @@ class MPDClient:
|
|
|
397
408
|
|
|
398
409
|
def _send(self, command, args):
|
|
399
410
|
if self._command_list is not None:
|
|
400
|
-
raise CommandListError("Cannot use send_
|
|
401
|
-
command.replace(" ", "_"))
|
|
411
|
+
raise CommandListError("Cannot use send_{} in a command list".format(command.replace(" ", "_")))
|
|
402
412
|
self._write_command(command, args)
|
|
403
413
|
retval = self._commands[command]
|
|
404
414
|
if retval is not None:
|
|
@@ -449,6 +459,8 @@ class MPDClient:
|
|
|
449
459
|
for arg in args:
|
|
450
460
|
if isinstance(arg, tuple):
|
|
451
461
|
parts.append(f'{Range(arg)!s}')
|
|
462
|
+
elif isinstance(arg, Group):
|
|
463
|
+
parts.append(f'group "{escape(str(arg.tag))}"')
|
|
452
464
|
else:
|
|
453
465
|
parts.append(f'"{escape(str(arg))}"')
|
|
454
466
|
if '\n' in ' '.join(parts):
|
|
@@ -503,13 +515,67 @@ class MPDClient:
|
|
|
503
515
|
pair = self._read_pair(separator, binary=binary)
|
|
504
516
|
|
|
505
517
|
def _read_list(self):
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
513
579
|
|
|
514
580
|
def _read_playlist(self):
|
|
515
581
|
for _, value in self._read_pairs(":"):
|
|
@@ -551,7 +617,7 @@ class MPDClient:
|
|
|
551
617
|
def _fetch_item(self):
|
|
552
618
|
pairs = list(self._read_pairs())
|
|
553
619
|
if len(pairs) != 1:
|
|
554
|
-
|
|
620
|
+
raise ProtocolError(f"Expected 1 pair, got {len(pairs)}")
|
|
555
621
|
return pairs[0][1]
|
|
556
622
|
|
|
557
623
|
@iterator_wrapper
|
|
@@ -613,7 +679,7 @@ class MPDClient:
|
|
|
613
679
|
amount = int(obj['binary'])
|
|
614
680
|
try:
|
|
615
681
|
obj['data'] = self._read_binary(amount)
|
|
616
|
-
except
|
|
682
|
+
except OSError as err:
|
|
617
683
|
raise ConnectionError(f'Error reading binary content: {err}') from err
|
|
618
684
|
data_bytes = len(obj['data'])
|
|
619
685
|
if data_bytes != amount: # can we ever get there?
|
|
@@ -659,7 +725,7 @@ class MPDClient:
|
|
|
659
725
|
sock.settimeout(self.mpd_timeout)
|
|
660
726
|
sock.connect(path)
|
|
661
727
|
sock.settimeout(self.socket_timeout)
|
|
662
|
-
except
|
|
728
|
+
except OSError as socket_err:
|
|
663
729
|
raise ConnectionError(socket_err) from socket_err
|
|
664
730
|
return sock
|
|
665
731
|
|
|
@@ -673,20 +739,20 @@ class MPDClient:
|
|
|
673
739
|
gai = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
|
674
740
|
socket.SOCK_STREAM, socket.IPPROTO_TCP,
|
|
675
741
|
flags)
|
|
676
|
-
except
|
|
742
|
+
except OSError as gaierr:
|
|
677
743
|
raise ConnectionError(gaierr) from gaierr
|
|
678
744
|
for res in gai:
|
|
679
745
|
af, socktype, proto, _, sa = res
|
|
680
746
|
sock = None
|
|
681
747
|
try:
|
|
682
|
-
|
|
748
|
+
logger.debug('opening socket %s', sa)
|
|
683
749
|
sock = socket.socket(af, socktype, proto)
|
|
684
750
|
sock.settimeout(self.mpd_timeout)
|
|
685
751
|
sock.connect(sa)
|
|
686
752
|
sock.settimeout(self.socket_timeout)
|
|
687
753
|
return sock
|
|
688
|
-
except
|
|
689
|
-
|
|
754
|
+
except OSError as socket_err:
|
|
755
|
+
logger.debug('opening socket %s failed: %s', sa, socket_err)
|
|
690
756
|
err = socket_err
|
|
691
757
|
if sock is not None:
|
|
692
758
|
sock.close()
|
|
@@ -723,17 +789,17 @@ class MPDClient:
|
|
|
723
789
|
host = self.host
|
|
724
790
|
else:
|
|
725
791
|
self.host = host
|
|
726
|
-
if
|
|
792
|
+
if port is None:
|
|
727
793
|
port = self.port
|
|
728
794
|
else:
|
|
729
795
|
self.port = port
|
|
730
796
|
if self._sock is not None:
|
|
731
797
|
raise ConnectionError("Already connected")
|
|
732
|
-
if host[0] in ['/', '@']:
|
|
733
|
-
|
|
798
|
+
if host and host[0] in ['/', '@']:
|
|
799
|
+
logger.debug('Connecting unix socket %s', host)
|
|
734
800
|
self._sock = self._connect_unix(host)
|
|
735
801
|
else:
|
|
736
|
-
|
|
802
|
+
logger.debug('Connecting tcp socket %s:%s (timeout: %ss)', host, port, self.mpd_timeout)
|
|
737
803
|
self._sock = self._connect_tcp(host, port)
|
|
738
804
|
self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape')
|
|
739
805
|
self._rbfile = self._sock.makefile("rb")
|
|
@@ -743,12 +809,12 @@ class MPDClient:
|
|
|
743
809
|
except:
|
|
744
810
|
self.disconnect()
|
|
745
811
|
raise
|
|
746
|
-
|
|
812
|
+
logger.debug('Connected')
|
|
747
813
|
|
|
748
814
|
@property
|
|
749
815
|
def socket_timeout(self):
|
|
750
816
|
"""Socket timeout in second (defaults to :py:obj:`SOCKET_TIMEOUT`).
|
|
751
|
-
Use :py:obj:`None` to disable socket
|
|
817
|
+
Use :py:obj:`None` to disable socket timeout.
|
|
752
818
|
|
|
753
819
|
:setter: Set the socket timeout (integer > 0)
|
|
754
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
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
# SPDX-FileCopyrightText: 2012-2025 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,
|
|
@@ -327,21 +340,19 @@ class MPDClient:
|
|
|
327
340
|
"readmessages": self._fetch_messages,
|
|
328
341
|
"sendmessage": self._fetch_nothing,
|
|
329
342
|
}
|
|
343
|
+
# fmt: on
|
|
330
344
|
#: host used with the current connection (:py:obj:`str`)
|
|
331
|
-
self.host =
|
|
345
|
+
self.host = 'localhost'
|
|
332
346
|
#: password detected in :envvar:`MPD_HOST` environment variable (:py:obj:`str`)
|
|
333
347
|
self.pwd = None
|
|
334
348
|
#: port used with the current connection (:py:obj:`int`, :py:obj:`str`)
|
|
335
|
-
self.port =
|
|
349
|
+
self.port = os.getenv('MPD_PORT', '6600')
|
|
336
350
|
self._get_envvars()
|
|
337
351
|
|
|
338
352
|
def _get_envvars(self):
|
|
339
353
|
"""
|
|
340
354
|
Retrieve MPD env. var. to overrides default "localhost:6600"
|
|
341
355
|
"""
|
|
342
|
-
# Set some defaults
|
|
343
|
-
self.host = 'localhost'
|
|
344
|
-
self.port = os.getenv('MPD_PORT', '6600')
|
|
345
356
|
_host = os.getenv('MPD_HOST', '')
|
|
346
357
|
if _host:
|
|
347
358
|
# If password is set: MPD_HOST=pass@host
|
|
@@ -349,30 +360,30 @@ class MPDClient:
|
|
|
349
360
|
mpd_host_env = _host.split('@', 1)
|
|
350
361
|
if mpd_host_env[0]:
|
|
351
362
|
# A password is actually set
|
|
352
|
-
|
|
363
|
+
logger.debug('password detected in MPD_HOST, set client pwd attribute')
|
|
353
364
|
self.pwd = mpd_host_env[0]
|
|
354
365
|
if mpd_host_env[1]:
|
|
355
366
|
self.host = mpd_host_env[1]
|
|
356
|
-
|
|
367
|
+
logger.debug('host detected in MPD_HOST: %s', self.host)
|
|
357
368
|
elif mpd_host_env[1]:
|
|
358
369
|
# No password set but leading @ is an abstract socket
|
|
359
370
|
self.host = '@'+mpd_host_env[1]
|
|
360
|
-
|
|
371
|
+
logger.debug('host detected in MPD_HOST: %s (abstract socket)', self.host)
|
|
361
372
|
else:
|
|
362
373
|
# MPD_HOST is a plain host
|
|
363
374
|
self.host = _host
|
|
364
|
-
|
|
375
|
+
logger.debug('host detected in MPD_HOST: %s', self.host)
|
|
365
376
|
else:
|
|
366
377
|
# Is socket there
|
|
367
378
|
xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR', '/run')
|
|
368
379
|
rundir = os.path.join(xdg_runtime_dir, 'mpd/socket')
|
|
369
380
|
if os.path.exists(rundir):
|
|
370
381
|
self.host = rundir
|
|
371
|
-
|
|
382
|
+
logger.debug('host detected in ${XDG_RUNTIME_DIR}/run: %s (unix socket)', self.host)
|
|
372
383
|
_mpd_timeout = os.getenv('MPD_TIMEOUT', '')
|
|
373
384
|
if _mpd_timeout.isdigit():
|
|
374
385
|
self.mpd_timeout = int(_mpd_timeout)
|
|
375
|
-
|
|
386
|
+
logger.debug('timeout detected in MPD_TIMEOUT: %d', self.mpd_timeout)
|
|
376
387
|
else: # Use CONNECTION_TIMEOUT as default even if MPD_TIMEOUT carries gargage
|
|
377
388
|
self.mpd_timeout = CONNECTION_TIMEOUT
|
|
378
389
|
|
|
@@ -397,8 +408,7 @@ class MPDClient:
|
|
|
397
408
|
|
|
398
409
|
def _send(self, command, args):
|
|
399
410
|
if self._command_list is not None:
|
|
400
|
-
raise CommandListError("Cannot use send_
|
|
401
|
-
command.replace(" ", "_"))
|
|
411
|
+
raise CommandListError("Cannot use send_{} in a command list".format(command.replace(" ", "_")))
|
|
402
412
|
self._write_command(command, args)
|
|
403
413
|
retval = self._commands[command]
|
|
404
414
|
if retval is not None:
|
|
@@ -449,6 +459,8 @@ class MPDClient:
|
|
|
449
459
|
for arg in args:
|
|
450
460
|
if isinstance(arg, tuple):
|
|
451
461
|
parts.append(f'{Range(arg)!s}')
|
|
462
|
+
elif isinstance(arg, Group):
|
|
463
|
+
parts.append(f'group "{escape(str(arg.tag))}"')
|
|
452
464
|
else:
|
|
453
465
|
parts.append(f'"{escape(str(arg))}"')
|
|
454
466
|
if '\n' in ' '.join(parts):
|
|
@@ -503,13 +515,67 @@ class MPDClient:
|
|
|
503
515
|
pair = self._read_pair(separator, binary=binary)
|
|
504
516
|
|
|
505
517
|
def _read_list(self):
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
513
579
|
|
|
514
580
|
def _read_playlist(self):
|
|
515
581
|
for _, value in self._read_pairs(":"):
|
|
@@ -551,7 +617,7 @@ class MPDClient:
|
|
|
551
617
|
def _fetch_item(self):
|
|
552
618
|
pairs = list(self._read_pairs())
|
|
553
619
|
if len(pairs) != 1:
|
|
554
|
-
|
|
620
|
+
raise ProtocolError(f"Expected 1 pair, got {len(pairs)}")
|
|
555
621
|
return pairs[0][1]
|
|
556
622
|
|
|
557
623
|
@iterator_wrapper
|
|
@@ -613,7 +679,7 @@ class MPDClient:
|
|
|
613
679
|
amount = int(obj['binary'])
|
|
614
680
|
try:
|
|
615
681
|
obj['data'] = self._read_binary(amount)
|
|
616
|
-
except
|
|
682
|
+
except OSError as err:
|
|
617
683
|
raise ConnectionError(f'Error reading binary content: {err}') from err
|
|
618
684
|
data_bytes = len(obj['data'])
|
|
619
685
|
if data_bytes != amount: # can we ever get there?
|
|
@@ -659,7 +725,7 @@ class MPDClient:
|
|
|
659
725
|
sock.settimeout(self.mpd_timeout)
|
|
660
726
|
sock.connect(path)
|
|
661
727
|
sock.settimeout(self.socket_timeout)
|
|
662
|
-
except
|
|
728
|
+
except OSError as socket_err:
|
|
663
729
|
raise ConnectionError(socket_err) from socket_err
|
|
664
730
|
return sock
|
|
665
731
|
|
|
@@ -673,20 +739,20 @@ class MPDClient:
|
|
|
673
739
|
gai = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
|
674
740
|
socket.SOCK_STREAM, socket.IPPROTO_TCP,
|
|
675
741
|
flags)
|
|
676
|
-
except
|
|
742
|
+
except OSError as gaierr:
|
|
677
743
|
raise ConnectionError(gaierr) from gaierr
|
|
678
744
|
for res in gai:
|
|
679
745
|
af, socktype, proto, _, sa = res
|
|
680
746
|
sock = None
|
|
681
747
|
try:
|
|
682
|
-
|
|
748
|
+
logger.debug('opening socket %s', sa)
|
|
683
749
|
sock = socket.socket(af, socktype, proto)
|
|
684
750
|
sock.settimeout(self.mpd_timeout)
|
|
685
751
|
sock.connect(sa)
|
|
686
752
|
sock.settimeout(self.socket_timeout)
|
|
687
753
|
return sock
|
|
688
|
-
except
|
|
689
|
-
|
|
754
|
+
except OSError as socket_err:
|
|
755
|
+
logger.debug('opening socket %s failed: %s', sa, socket_err)
|
|
690
756
|
err = socket_err
|
|
691
757
|
if sock is not None:
|
|
692
758
|
sock.close()
|
|
@@ -723,17 +789,17 @@ class MPDClient:
|
|
|
723
789
|
host = self.host
|
|
724
790
|
else:
|
|
725
791
|
self.host = host
|
|
726
|
-
if
|
|
792
|
+
if port is None:
|
|
727
793
|
port = self.port
|
|
728
794
|
else:
|
|
729
795
|
self.port = port
|
|
730
796
|
if self._sock is not None:
|
|
731
797
|
raise ConnectionError("Already connected")
|
|
732
|
-
if host[0] in ['/', '@']:
|
|
733
|
-
|
|
798
|
+
if host and host[0] in ['/', '@']:
|
|
799
|
+
logger.debug('Connecting unix socket %s', host)
|
|
734
800
|
self._sock = self._connect_unix(host)
|
|
735
801
|
else:
|
|
736
|
-
|
|
802
|
+
logger.debug('Connecting tcp socket %s:%s (timeout: %ss)', host, port, self.mpd_timeout)
|
|
737
803
|
self._sock = self._connect_tcp(host, port)
|
|
738
804
|
self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape')
|
|
739
805
|
self._rbfile = self._sock.makefile("rb")
|
|
@@ -743,12 +809,12 @@ class MPDClient:
|
|
|
743
809
|
except:
|
|
744
810
|
self.disconnect()
|
|
745
811
|
raise
|
|
746
|
-
|
|
812
|
+
logger.debug('Connected')
|
|
747
813
|
|
|
748
814
|
@property
|
|
749
815
|
def socket_timeout(self):
|
|
750
816
|
"""Socket timeout in second (defaults to :py:obj:`SOCKET_TIMEOUT`).
|
|
751
|
-
Use :py:obj:`None` to disable socket
|
|
817
|
+
Use :py:obj:`None` to disable socket timeout.
|
|
752
818
|
|
|
753
819
|
:setter: Set the socket timeout (integer > 0)
|
|
754
820
|
:type: int or None
|
|
@@ -7,9 +7,9 @@ description = "An MPD (Music Player Daemon) client library written in pure Pytho
|
|
|
7
7
|
authors = [
|
|
8
8
|
{ name="Kaliko Jack", email="kaliko@azylum.org" },
|
|
9
9
|
]
|
|
10
|
-
license = "
|
|
10
|
+
license-files = ["LICENSE.txt"]
|
|
11
11
|
readme = "README.rst"
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 5 - Production/Stable",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
@@ -21,7 +21,10 @@ classifiers = [
|
|
|
21
21
|
dynamic = ["version"]
|
|
22
22
|
|
|
23
23
|
[project.optional-dependencies]
|
|
24
|
-
sphinx = [
|
|
24
|
+
sphinx = [
|
|
25
|
+
"Sphinx>=5.3.0",
|
|
26
|
+
"sphinx-rtd-theme>=3.1.0",
|
|
27
|
+
]
|
|
25
28
|
|
|
26
29
|
[project.urls]
|
|
27
30
|
"Homepage" = "https://kaliko.me/python-musicpd/"
|
|
@@ -35,3 +38,9 @@ py-modules = ["musicpd"]
|
|
|
35
38
|
|
|
36
39
|
[tool.setuptools.dynamic]
|
|
37
40
|
version = {attr = "musicpd.VERSION"}
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
test = [
|
|
44
|
+
"pytest>=7.0.1",
|
|
45
|
+
"pytest-cov>=4.0.0",
|
|
46
|
+
]
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-musicpd
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: An MPD (Music Player Daemon) client library written in pure Python.
|
|
5
5
|
Author-email: Kaliko Jack <kaliko@azylum.org>
|
|
6
|
-
License-Expression: LGPL-3.0-or-later
|
|
7
6
|
Project-URL: Homepage, https://kaliko.me/python-musicpd/
|
|
8
7
|
Keywords: mpd,Music Player Daemon
|
|
9
8
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -12,11 +11,12 @@ Classifier: Natural Language :: English
|
|
|
12
11
|
Classifier: Operating System :: OS Independent
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
13
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.11
|
|
16
15
|
Description-Content-Type: text/x-rst
|
|
17
16
|
License-File: LICENSE.txt
|
|
18
17
|
Provides-Extra: sphinx
|
|
19
18
|
Requires-Dist: Sphinx>=5.3.0; extra == "sphinx"
|
|
19
|
+
Requires-Dist: sphinx-rtd-theme>=3.1.0; extra == "sphinx"
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
22
|
Music Player Daemon client module
|
|
@@ -30,5 +30,5 @@ An MPD (Music Player Daemon) client library written in pure Python.
|
|
|
30
30
|
:MPD Protocol: https://www.musicpd.org/doc/html/protocol.html
|
|
31
31
|
:Code: https://gitlab.com/kaliko/python-musicpd
|
|
32
32
|
:Dependencies: None
|
|
33
|
-
:Compatibility: Python 3.
|
|
33
|
+
:Compatibility: Python 3.11+
|
|
34
34
|
:Licence: GNU LGPLv3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|