]> kaliko git repositories - python-musicpd.git/commitdiff
Fixed albumart command
authorKaliko Jack <kaliko@azylum.org>
Fri, 4 Dec 2020 18:12:51 +0000 (19:12 +0100)
committerKaliko Jack <kaliko@azylum.org>
Fri, 4 Dec 2020 18:12:51 +0000 (19:12 +0100)
Fixed test as well

CHANGES.txt
doc/source/use.rst
musicpd.py
test.py

index 57c8d5c53b8a928cc3fed5a6df2f0c5820a372c9..509841f7e52e7daec20c0924482832cf24cda586 100644 (file)
@@ -5,6 +5,7 @@ Changes in 0.#.# UNRELEASED
 ----------------------------
 
 * Update host and port attributes when reconnecting
+* Fixed albumart
 
 Changes in 0.4.4
 ----------------
index 18fdd2968ffce751c39d05d4c1435c968f47d76f..28f94131c453a0532fc98a85918523b17b3c8687 100644 (file)
@@ -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\_<CMD>* and a *fetch\_<CMD>* 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/
index 8c5bfc32c155e7cd1b8179ecdca217185239f591..f63857c4e8c931097f2a1d6e457ced94f96dd988 100644 (file)
@@ -1,5 +1,5 @@
 # python-musicpd: Python MPD client library
-# Copyright (C) 2012-2019  kaliko <kaliko@azylum.org>
+# Copyright (C) 2012-2020  kaliko <kaliko@azylum.org>
 # Copyright (C) 2019       Naglis Jonaitis <naglis@mailbox.org>
 # Copyright (C) 2019       Bart Van Loon <bbb@bbbart.be>
 # Copyright (C) 2008-2010  J. Alexander Treuman <jat@spatialrift.net>
@@ -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 fdd6efa699beb76f1899d423b5e647a96a824da7..ba3132b14689efa63c8164331bf702c74716253a 100755 (executable)
--- 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()