From 7cb6ba6bfc1dfae2be3ef0047dfeaabc87b81df7 Mon Sep 17 00:00:00 2001 From: Kaliko Jack Date: Fri, 4 Dec 2020 19:12:51 +0100 Subject: [PATCH] Fixed albumart command Fixed test as well --- CHANGES.txt | 1 + doc/source/use.rst | 39 +++++++++++++++++++++++++++++++++++ musicpd.py | 51 +++++++++++++++++++++++++++++++++++----------- test.py | 30 ++++++++++++++++++++++++--- 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 57c8d5c..509841f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,6 +5,7 @@ Changes in 0.#.# UNRELEASED ---------------------------- * Update host and port attributes when reconnecting +* Fixed albumart Changes in 0.4.4 ---------------- diff --git a/doc/source/use.rst b/doc/source/use.rst index 18fdd29..28f9413 100644 --- a/doc/source/use.rst +++ b/doc/source/use.rst @@ -1,6 +1,9 @@ Using the client library ========================= +Introduction +------------ + The client library can be used as follows: .. code-block:: python @@ -21,6 +24,9 @@ them), and the functions used to parse their responses see :ref:`commands`. See the `MPD protocol documentation`_ for more details. +Command lists +------------- + Command lists are also supported using `command_list_ok_begin()` and `command_list_end()` : @@ -31,6 +37,9 @@ Command lists are also supported using `command_list_ok_begin()` and client.status() # insert the status command into the list results = client.command_list_end() # results will be a list with the results +Ranges +------ + Provide a 2-tuple as argument for command supporting ranges (cf. `MPD protocol documentation`_ for more details). Possible ranges are: "START:END", "START:" and ":" : @@ -54,6 +63,8 @@ as a single colon as argument (i.e. sending just ":"): Empty start in range (i.e. ":END") are not possible and will raise a CommandError. +Iterators +---------- Commands may also return iterators instead of lists if `iterate` is set to `True`: @@ -64,6 +75,9 @@ Commands may also return iterators instead of lists if `iterate` is set to for song in client.playlistinfo(): print song['file'] +Idle prefixed commands +---------------------- + Each command have a *send\_* and a *fetch\_* variant, which allows to send a MPD command and then fetch the result later. This is useful for the idle command: @@ -86,5 +100,30 @@ This is useful for the idle command: >>> gobject.io_add_watch(client, gobject.IO_IN, callback) >>> gobject.MainLoop().run() +Fetching binary content (cover art) +----------------------------------- + +Fetching album covers is possible with albumart, here is an example: + +.. code-block:: python + + >>> fetched_cover_file = '/tmp/cover' + >>> cli = musicpd.MPDClient() + >>> cli.connect() + >>> track = "Steve Reich/1978-Music for 18 Musicians" + >>> with open(fetched_cover_file, 'wb') as cover: + >>> aart = cli.albumart(track, 0) + >>> received = int(aart.get('binary')) + >>> size = int(aart.get('size')) + >>> cover.write(aart.get('data')) + >>> while received < size: + >>> aart = cli.albumart(track, received) + >>> cover.write(aart.get('data')) + >>> received += int(aart.get('binary')) + >>> if received != size: + >>> print('something went wrong', file=sys.stderr) + >>> cli.disconnect() + +Refer to `MPD protocol documentation`_ for the meaning of `binary`, `size` and `data`. .. _MPD protocol documentation: http://www.musicpd.org/doc/protocol/ diff --git a/musicpd.py b/musicpd.py index 8c5bfc3..f63857c 100644 --- a/musicpd.py +++ b/musicpd.py @@ -1,5 +1,5 @@ # python-musicpd: Python MPD client library -# Copyright (C) 2012-2019 kaliko +# Copyright (C) 2012-2020 kaliko # Copyright (C) 2019 Naglis Jonaitis # Copyright (C) 2019 Bart Van Loon # Copyright (C) 2008-2010 J. Alexander Treuman @@ -24,7 +24,6 @@ import os from functools import wraps - HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" @@ -391,8 +390,22 @@ class MPDClient: parts.append('"%s"' % escape(str(arg))) self._write_line(" ".join(parts)) - def _read_line(self): - line = self._rfile.readline() + def _read_binary(self, amount): + chunk = bytearray() + while amount > 0: + result = self._rbfile.recv(amount) + if len(result) == 0: + self.disconnect() + raise ConnectionError("Connection lost while reading binary content") + chunk.extend(result) + amount -= len(result) + return bytes(chunk) + + def _read_line(self, binary=False): + if binary: + line = self._rbfile.readline().decode('utf-8') + else: + line = self._rfile.readline() if not line.endswith("\n"): self.disconnect() raise ConnectionError("Connection lost while reading line") @@ -409,8 +422,8 @@ class MPDClient: return return line - def _read_pair(self, separator): - line = self._read_line() + def _read_pair(self, separator, binary=False): + line = self._read_line(binary=binary) if line is None: return pair = line.split(separator, 1) @@ -418,11 +431,11 @@ class MPDClient: raise ProtocolError("Could not parse pair: '%s'" % line) return pair - def _read_pairs(self, separator=": "): - pair = self._read_pair(separator) + def _read_pairs(self, separator=": ", binary=False): + pair = self._read_pair(separator, binary=binary) while pair: yield pair - pair = self._read_pair(separator) + pair = self._read_pair(separator, binary=binary) def _read_list(self): seen = None @@ -524,13 +537,23 @@ class MPDClient: def _fetch_composite(self): obj = {} - for key, value in self._read_pairs(): + for key, value in self._read_pairs(binary=True): key = key.lower() obj[key] = value if key == 'binary': break - by = self._read_line() - obj['data'] = by.encode(errors='surrogateescape') + amount = int(obj['binary']) + try: + obj['data'] = self._read_binary(amount) + except IOError as err: + raise ConnectionError('Error reading binary content: %s' % err) + if len(obj['data']) != amount: + raise ConnectionError('Error reading binary content: ' + 'Expects %sB, got %s' % (amount, len(obj['data']))) + # Fetches trailing new line + self._read_line(binary=True) + # Fetches SUCCESS code + self._read_line(binary=True) return obj @iterator_wrapper @@ -553,6 +576,7 @@ class MPDClient: self._command_list = None self._sock = None self._rfile = _NotConnected() + self._rbfile = _NotConnected() self._wfile = _NotConnected() def _connect_unix(self, path): @@ -633,6 +657,7 @@ class MPDClient: else: self._sock = self._connect_tcp(host, port) self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape') + self._rbfile = self._sock.makefile("rb") self._wfile = self._sock.makefile("w", encoding='utf-8') try: self._hello() @@ -647,6 +672,8 @@ class MPDClient: """ if hasattr(self._rfile, 'close'): self._rfile.close() + if hasattr(self._rbfile, 'close'): + self._rbfile.close() if hasattr(self._wfile, 'close'): self._wfile.close() if hasattr(self._sock, 'close'): diff --git a/test.py b/test.py index fdd6efa..ba3132b 100755 --- a/test.py +++ b/test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# pylint: disable=missing-docstring """ Test suite highly borrowed^Wsteal from python-mpd2 [0] project. @@ -220,6 +221,27 @@ class TestMPDClient(unittest.TestCase): self.client._rfile.readline.side_effect = itertools.chain( lines, itertools.repeat('')) + def MPDWillReturnBinary(self, lines): + data = bytearray(b''.join(lines)) + print(data) + + def recv(amount): + val = bytearray() + while amount > 0: + amount -= 1 + _ = data.pop(0) + print(hex(_)) + val.append(_) + return val + + def readline(): + val = bytearray() + while not val.endswith(b'\x0a'): + val.append(data.pop(0)) + return val + self.client._rbfile.readline.side_effect = readline + self.client._rbfile.recv.side_effect = recv + def assertMPDReceived(self, *lines): self.client._wfile.write.assert_called_with(*lines) @@ -419,14 +441,16 @@ class TestMPDClient(unittest.TestCase): self.assertEqual(['foo=bar'], res) def test_albumart(self): + # here is a 34 bytes long data data = bytes('\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01' '\x00\x01\x00\x00\xff\xdb\x00C\x00\x05\x03\x04', encoding='utf8') - data_str = data.decode(encoding='utf-8', errors='surrogateescape') - self.MPDWillReturn('size: 36474\n', 'binary: 8192\n', - data_str+'\n', 'OK\n') + read_lines = [b'size: 42\nbinary: 34\n', data, b'\nOK\n'] + self.MPDWillReturnBinary(read_lines) + # Reading albumart / offset 0 should return the data res = self.client.albumart('muse/Raised Fist/2002-Dedication/', 0) self.assertEqual(res.get('data'), data) + if __name__ == '__main__': unittest.main() -- 2.39.2