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.
Files changed (32) hide show
  1. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/CHANGES.txt +16 -0
  2. python_musicpd-0.9.3/PKG-INFO +34 -0
  3. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/README.rst +1 -1
  4. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/_commands.rst +10 -1
  5. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/commands.txt +1 -1
  6. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/conf.py +8 -8
  7. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/musicpd.py +116 -41
  8. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/use.rst +24 -0
  9. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/musicpd.py +116 -41
  10. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/pyproject.toml +14 -5
  11. python_musicpd-0.9.3/python_musicpd.egg-info/PKG-INFO +34 -0
  12. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/requires.txt +1 -0
  13. python_musicpd-0.9.1/PKG-INFO +0 -199
  14. python_musicpd-0.9.1/python_musicpd.egg-info/PKG-INFO +0 -199
  15. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/LICENSE.txt +0 -0
  16. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/MANIFEST.in +0 -0
  17. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/commands.rst +0 -0
  18. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/contribute.rst +0 -0
  19. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/doc.rst +0 -0
  20. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/client.py +0 -0
  21. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/connect.py +0 -0
  22. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/connect_host.py +0 -0
  23. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/exceptions.py +0 -0
  24. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/findadd.py +0 -0
  25. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples/playback.py +0 -0
  26. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/examples.rst +0 -0
  27. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/index.rst +0 -0
  28. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/doc/source/python-musicpd.svg +0 -0
  29. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/SOURCES.txt +0 -0
  30. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/dependency_links.txt +0 -0
  31. {python_musicpd-0.9.1 → python_musicpd-0.9.3}/python_musicpd.egg-info/top_level.txt +0 -0
  32. {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.6+
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.
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>]...[group <str>]... -> fetch_list
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-2021 kaliko <kaliko@azylum.org>
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 = u'Python MPD Module'
60
+ project = 'Python MPD Module'
61
61
  from datetime import datetime
62
- copyright = u'2012-{}, {}'.format(datetime.now().year, __author__)
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 = u'Python MPD Module v0.0.1'
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', u'Python MPD Module Documentation',
277
- u'kaliko', 'manual'),
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', u'Python MPD Module Documentation',
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', u'Python MPD Module Documentation',
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
- # -*- coding: utf-8 -*-
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.1'
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
- log = logging.getLogger(__name__)
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
- for item in gen:
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 = None
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 = None
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
- log.debug('password detected in MPD_HOST, set client pwd attribute')
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
- log.debug('host detected in MPD_HOST: %s', self.host)
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
- log.debug('host detected in MPD_HOST: %s (abstract socket)', self.host)
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
- log.debug('host detected in MPD_HOST: %s', self.host)
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
- log.debug('host detected in ${XDG_RUNTIME_DIR}/run: %s (unix socket)', self.host)
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
- log.debug('timeout detected in MPD_TIMEOUT: %d', self.mpd_timeout)
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_%s in a command list" %
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
- seen = None
498
- for key, value in self._read_pairs():
499
- if key != seen:
500
- if seen is not None:
501
- raise ProtocolError(f"Expected key '{seen}', got '{key}'")
502
- seen = key
503
- yield value
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
- return None
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 IOError as err:
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 socket.error as socket_err:
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 socket.error as gaierr:
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
- log.debug('opening socket %s', sa)
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 socket.error as socket_err:
680
- log.debug('opening socket %s failed: %s', sa, socket_err)
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 not port:
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
- log.debug('Connecting unix socket %s', host)
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
- log.debug('Connecting tcp socket %s:%s (timeout: %ss)', host, port, self.mpd_timeout)
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
- log.debug('Connected')
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 timout.
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