././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/0000755000175000017500000000000014606431054014054 5ustar00kalikokaliko././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/CHANGES.txt0000644000175000017500000000740114606431026015666 0ustar00kalikokalikopython-musicpd Changes List =========================== Changes in 0.9.0 ---------------- * Use right SPDX identifier for license headers * mpd_version attribute init to empty string instead of None * Fixed send_noidle (introduced with e8daa719) * Improved Range object to deal with window parameter * Add logging * Switch to pyproject.toml (setuptools build system) * The connect sequence raises ConnectionError only on error, previously getaddrinfo or unix socket connection error raised an OSError * Improved documentation, add examples Changes in 0.8.0 ---------------- * Need python 3.6 minimum (modernize code) * Add context management for MPDClient instance (Thanks Wonko der Verständige) * Switch to SPDX for license header Changes in 0.7.0 ---------------- * Add socket timeout (disabled by default) * MPD_TIMEOUT is set for both tcp and unix sockets * Raises an exception if command contains '\n' Changes in 0.6.0 ---------------- * Add binarylimit command * Add getvol command * Honor MPD_TIMEOUT environment variable * Default connection timeout is now 30s (previously 5s) * Add abstract unix socket support * Add tests, improved coverage * Switch back to gztar (even though official python doc mention xztar, PEP 527 considers only gztar and zip) Changes in 0.5.1 ---------------- * Clean up packaging (remove useless files and html doc) * Remove bztar from sdist * Switch to xztar sdist Changes in 0.5.0 ---------------- * Add readpicture command * Add getfingerprint command * Add delpartition command * Add moveoutput command * Update host and port attributes when reconnecting * Fixed albumart Changes in 0.4.4 ---------------- * Add tcp socket timeout, exposed in musicpd.CONNECTION_TIMEOUT, defaults to 5s (Thanks to Bart Van Loon) * Add partition commands (Thanks Naglis Jonaitis) * Add listfiles command * Add tagtypes (disable|enable|clear|all) commands * Add albumart command Changes in 0.4.3 ---------------- * Use setuptools * Add sphinx documentation * Add sensible defaults and honor environment variables Use MPD_HOST/MPD_PORT (honor password setting in MPD_HOST) If MPD_HOST is not set, tries to find a socket in ${XDG_RUNTIME_DIR:-/run}/mpd/socket Changes in 0.4.2 ---------------- * Add unittest (requires mock) * Add mounts and neighbors commands (mount, unmount, listmounts and listneighbors) * Add rangeid command * Add tag editing commands (addtagid and cleartagid) * Add missing priority commands (prio and prioid) * Add config command Changes in 0.4.1 ---------------- * Fixed noidle command Changes in 0.4.0 ---------------- * support range Changes in 0.3.1 UNRELEASED --------------------------- * renamed musicpd * python3 port * no python2 support * added searchadd and searchaddpl commands * added seekcur command * added client to client commands * added readcomments and toggleoutput commands Changes in 0.3.0 ---------------- * added replay_gain_mode and replay_gain_status commands * added mixrampdb and mixrampdelay commands * added findadd and rescan commands * added decoders command * changed license to LGPL * added sticker commands * correctly handle errors in command lists (fixes a longstanding bug) * raise IteratingError instead of breaking horribly when called wrong * added fileno() to export socket FD (for polling with select et al.) * asynchronous API (use send_ to queue, fetch_ to retrieve) * support for connecting to unix domain sockets * added consume and single commands * added idle and noidle commands * added listplaylists command Changes in 0.2.1 ---------------- * connect() no longer broken on Windows Changes in 0.2.0 ---------------- * support for IPv6 and multi-homed hostnames * connect() will fail if already connected * commands may now raise ConnectionError * addid and update may now return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/LICENSE.txt0000644000175000017500000001674314606431026015711 0ustar00kalikokaliko GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/MANIFEST.in0000644000175000017500000000010614606431026015606 0ustar00kalikokalikoinclude CHANGES.txt graft doc/source global-exclude *~ *.py[cod] *.so ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/PKG-INFO0000644000175000017500000002373614606431054015164 0ustar00kalikokalikoMetadata-Version: 2.1 Name: python-musicpd Version: 0.9.0 Summary: An MPD (Music Player Daemon) client library written in pure Python. Author-email: Kaliko Jack License: GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. Project-URL: Homepage, https://kaliko.me/python-musicpd/ Keywords: mpd,Music Player Daemon Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Description-Content-Type: text/x-rst License-File: LICENSE.txt Provides-Extra: sphinx Requires-Dist: Sphinx>=5.3.0; extra == "sphinx" Music Player Daemon client module ********************************* An MPD (Music Player Daemon) client library written in pure Python. ---- :Documentation: https://kaliko.gitlab.io/python-musicpd :MPD Protocol: https://www.musicpd.org/doc/html/protocol.html :Code: https://gitlab.com/kaliko/python-musicpd :Dependencies: None :Compatibility: Python 3.6+ :Licence: GNU LGPLv3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/README.rst0000644000175000017500000000061414606431026015543 0ustar00kalikokalikoMusic Player Daemon client module ********************************* An MPD (Music Player Daemon) client library written in pure Python. ---- :Documentation: https://kaliko.gitlab.io/python-musicpd :MPD Protocol: https://www.musicpd.org/doc/html/protocol.html :Code: https://gitlab.com/kaliko/python-musicpd :Dependencies: None :Compatibility: Python 3.6+ :Licence: GNU LGPLv3 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/doc/0000755000175000017500000000000014606431054014621 5ustar00kalikokaliko././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/doc/source/0000755000175000017500000000000014606431054016121 5ustar00kalikokaliko././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/commands.rst0000644000175000017500000000057414606431026020461 0ustar00kalikokaliko.. SPDX-FileCopyrightText: 2018-2023 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later .. _commands: Available commands ================== Get current available commands: .. code-block:: python import musicpd print(' '.join([cmd for cmd in musicpd.MPDClient()._commands.keys()])) List, last updated for v0.8.0: .. literalinclude:: commands.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/commands.txt0000644000175000017500000001575114606431026020473 0ustar00kalikokaliko== Status Commands clearerror -> fetch_nothing currentsong -> fetch_object idle [] -> fetch_list noidle -> None status -> fetch_object stats -> fetch_object == Playback Option Commands consume -> fetch_nothing crossfade -> fetch_nothing mixrampdb -> fetch_nothing mixrampdelay -> fetch_nothing random -> fetch_nothing repeat -> fetch_nothing setvol -> fetch_nothing getvol -> fetch_object single -> fetch_nothing replay_gain_mode -> fetch_nothing replay_gain_status -> fetch_item volume -> fetch_nothing == Playback Control Commands next -> fetch_nothing pause [] -> fetch_nothing play [] -> fetch_nothing playid [] -> fetch_nothing previous -> fetch_nothing seek -> fetch_nothing seekid -> fetch_nothing seekcur -> fetch_nothing stop -> fetch_nothing == Queue Commands add -> fetch_nothing addid [] -> fetch_item clear -> fetch_nothing delete | -> fetch_nothing deleteid -> fetch_nothing move | -> fetch_nothing moveid -> fetch_nothing playlist -> fetch_playlist playlistfind -> fetch_songs playlistid [] -> fetch_songs playlistinfo [|] -> fetch_songs playlistsearch -> fetch_songs plchanges -> fetch_songs plchangesposid -> fetch_changes prio | -> fetch_nothing, prioid -> fetch_nothing, rangeid -> fetch_nothing, shuffle [] -> fetch_nothing swap -> fetch_nothing swapid -> fetch_nothing addtagid -> fetch_nothing cleartagid [] -> fetch_nothing == Stored Playlist Commands listplaylist -> fetch_list listplaylistinfo -> fetch_songs listplaylists -> fetch_playlists load [] -> fetch_nothing playlistadd -> fetch_nothing playlistclear -> fetch_nothing playlistdelete -> fetch_nothing playlistmove -> fetch_nothing rename -> fetch_nothing rm -> fetch_nothing save -> fetch_nothing == Database Commands albumart -> fetch_composite count -> fetch_object count group -> fetch_object getfingerprint -> fetch_object find [ ]... -> fetch_songs findadd [ ] -> fetch_nothing list [ ]...[group ]... -> fetch_list listall [] -> fetch_database listallinfo [] -> fetch_database listfiles -> fetch_database lsinfo [] -> fetch_database readcomments [] -> fetch_object readpicture -> fetch_composite search [ ]... -> fetch_song searchadd [ ]... -> fetch_nothing searchaddpl [ ]... -> fetch_nothing update [] -> fetch_item rescan [] -> fetch_item == Mounts and neighbors == mount -> fetch_nothing unmount -> fetch_nothing listmounts -> fetch_mounts listneighbors -> fetch_neighbors == Sticker Commands sticker get -> fetch_item sticker set -> fetch_nothing sticker delete [] -> fetch_nothing sticker list -> fetch_list sticker find -> fetch_songs == Connection Commands close -> None kill -> None password -> fetch_nothing ping -> fetch_nothing binarylimit -> fetch_nothing tagtypes -> fetch_list tagtypes disable []... -> fetch_nothing tagtypes enable []... -> fetch_nothing tagtypes clear -> fetch_nothing tagtypes all -> fetch_nothing == Partition Commands partition -> fetch_nothing listpartitions -> fetch_list newpartition -> fetch_nothing delpartition -> fetch_nothing moveoutput -> fetch_nothing == Audio Output Commands disableoutput -> fetch_nothing enableoutput -> fetch_nothing toggleoutput -> fetch_nothing outputs -> fetch_outputs outputset -> fetch_nothing == Reflection Commands config -> fetch_object commands -> fetch_list notcommands -> fetch_list urlhandlers -> fetch_list decoders -> fetch_plugins == Client to Client subscribe -> self._fetch_nothing, unsubscribe -> self._fetch_nothing, channels -> self._fetch_list, readmessages -> self._fetch_messages, sendmessage -> self._fetch_nothing, ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/conf.py0000644000175000017500000003047414606431026017427 0ustar00kalikokaliko# coding: utf-8 # SPDX-FileCopyrightText: 2018-2021 kaliko # SPDX-License-Identifier: LGPL-3.0-or-later # # Python MPD Module documentation build configuration file, created by # sphinx-quickstart on Mon Mar 12 14:37:32 2018. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('../../')) from musicpd import VERSION __author__ = 'kaliko' # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', #'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.todo', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Python MPD Module' from datetime import datetime copyright = u'2012-{}, {}'.format(datetime.now().year, __author__) author = __author__ # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = VERSION # The full version, including alpha/beta/rc tags. release = VERSION # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'classic' #html_theme = "sphinx_rtd_theme" #html_theme = 'bootstrap' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] #html_theme_path = ['/usr/lib/python3/dist-packages/sphinx_bootstrap_theme'] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'Python MPD Module v0.0.1' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #chtml_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'PythonMPDModuledoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'PythonMPDModule.tex', u'Python MPD Module Documentation', u'kaliko', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pythonmpdmodule', u'Python MPD Module Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'PythonMPDModule', u'Python MPD Module Documentation', author, 'PythonMPDModule', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. # epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. # # epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. # # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. # # epub_tocdepth = 3 # Allow duplicate toc entries. # # epub_tocdup = True # Choose between 'default' and 'includehidden'. # # epub_tocscope = 'default' # Fix unsupported image types using the Pillow. # # epub_fix_images = False # Scale large images. # # epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. # # epub_show_urls = 'inline' # If false, no index is generated. # # epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} # autodoc config autodoc_member_order = 'bysource' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/contribute.rst0000644000175000017500000000173414606431026021035 0ustar00kalikokaliko.. SPDX-FileCopyrightText: 2018-2021 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later Contributing ============= Use git, `learn if needed`_. Git Workflow ------------- * ``master`` branch holds latest stable|release code * ``dev`` branch holds current development code * Work on a dedicated branch starting off ``dev`` * I like fast forward merges, **please rebase on** ``dev`` branch before submitting changes * Advertise your work (cf. Note) .. NOTE:: **Git merge Workflow** |br| I currently don't care about a specific workflow concerning changes submission. |br| Don't bother with gitlab merge request or gh pull request. |br| All we need is a public git repo and a ref to fetch from as long you rebased on ``dev`` branch. Coding ------- * follow pep8 * write unittest * actually test your code (unit and functional testing) .. _`learn if needed`: https://git-scm.com/book/ .. |br| raw:: html
.. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/doc.rst0000644000175000017500000000035414606431026017421 0ustar00kalikokaliko.. SPDX-FileCopyrightText: 2018-2023 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later musicpd namespace ================= .. automodule:: musicpd :members: :no-undoc-members: .. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/doc/source/examples/0000755000175000017500000000000014606431054017737 5ustar00kalikokaliko././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples/client.py0000644000175000017500000000547014606431026021574 0ustar00kalikokaliko"""Plain client class """ import logging import select import sys import musicpd class MyClient(musicpd.MPDClient): """Plain client inheriting from MPDClient""" def __init__(self): # Set logging to debug level logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(module)-8s %(message)s') self.log = logging.getLogger(__name__) super().__init__() # Set host/port/password after init to overrides defaults # self.host = 'example.org' # self.port = 4242 # self.pwd = 'secret' def connect(self): """Overriding explicitly MPDClient.connect()""" try: super().connect(host=self.host, port=self.port) if hasattr(self, 'pwd') and self.pwd: self.password(self.pwd) except musicpd.ConnectionError as err: # Catch socket error self.log.error('Failed to connect: %s', err) sys.exit(42) def _wait_for_changes(self, callback): select_timeout = 10 # second while True: self.send_idle() # use send_ API to avoid blocking on read _read, _, _ = select.select([self], [], [], select_timeout) if _read: # tries to read response ret = self.fetch_idle() # do something callback(ret) else: # cancels idle self.noidle() def callback(self, *args): """Method launch on MPD event, cf. monitor method""" self.log.info('%s', args) def monitor(self): """Continuously monitor MPD activity. Launch callback method on event. """ try: self._wait_for_changes(self.callback) except (OSError, musicpd.MPDError) as err: self.log.error('%s: Something went wrong: %s', type(err).__name__, err) if __name__ == '__main__': cli = MyClient() # You can overrides host here or in init #cli.host = 'example.org' # Connect MPD server try: cli.connect() except musicpd.ConnectionError as err: cli.log.error(err) # Monitor MPD changes, blocking/timeout idle approach try: cli.socket_timeout = 20 # seconds ret = cli.idle() cli.log.info('Leaving idle, got: %s', ret) except TimeoutError as err: cli.log.info('Nothing occured the last %ss', cli.socket_timeout) # Reset connection try: cli.socket_timeout = None cli.disconnect() cli.connect() except musicpd.ConnectionError as err: cli.log.error(err) # Monitor MPD changes, non blocking idle approach try: cli.monitor() except KeyboardInterrupt as err: cli.log.info(type(err).__name__) cli.send_noidle() cli.disconnect() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples/connect.py0000644000175000017500000000157614606431026021752 0ustar00kalikokalikoimport logging import musicpd # Set logging to debug level # it should log messages showing where defaults come from logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') log = logging.getLogger() client = musicpd.MPDClient() # use MPD_HOST/MPD_PORT env var if set else # test ${XDG_RUNTIME_DIR}/mpd/socket for existence # fallback to localhost:6600 # connect support host/port argument as well client.connect() status = client.status() if status.get('state') == 'play': current_song_id = status.get('songid') current_song = client.playlistid(current_song_id)[0] log.info(f'Playing : {current_song.get("file")}') next_song_id = status.get('nextsongid', None) if next_song_id: next_song = client.playlistid(next_song_id)[0] log.info(f'Next song : {next_song.get("file")}') else: log.info('Not playing') client.disconnect() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples/connect_host.py0000644000175000017500000000057214606431026023002 0ustar00kalikokalikoimport sys import logging import musicpd # Set logging to debug level logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') client = musicpd.MPDClient() try: client.connect(host='example.lan') client.password('secret') client.status() except musicpd.MPDError as err: print(f'An error occured: {err}') finally: client.disconnect() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples/exceptions.py0000644000175000017500000000304614606431026022474 0ustar00kalikokaliko"""client class dealing with all Exceptions """ import logging import musicpd # Wrap Exception decorator def wrapext(func): """Decorator to wrap errors in musicpd.MPDError""" errors=(OSError, TimeoutError) into = musicpd.MPDError def w_func(*args, **kwargs): try: return func(*args, **kwargs) except errors as err: strerr = str(err) if hasattr(err, 'strerror'): if err.strerror: strerr = err.strerror raise into(strerr) from err return w_func class MyClient(musicpd.MPDClient): """Plain client inheriting from MPDClient""" def __init__(self): # Set logging to debug level logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(module)-10s %(message)s') self.log = logging.getLogger(__name__) super().__init__() @wrapext def __getattr__(self, cmd): """Wrapper around MPDClient calls for abstract overriding""" self.log.debug('cmd: %s', cmd) return super().__getattr__(cmd) if __name__ == '__main__': cli = MyClient() # You can overrides host here or in init #cli.host = 'example.org' # Connect MPD server try: cli.connect() cli.currentsong() cli.stats() except musicpd.MPDError as error: cli.log.fatal(error) finally: cli.log.info('Disconnecting') try: # Tries to close the socket anyway cli.disconnect() except OSError: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples/findadd.py0000644000175000017500000000032414606431026021700 0ustar00kalikokalikoimport musicpd # Using a context manager # (use env var if you need to override default host) with musicpd.MPDClient() as client: client.clear() client.findadd("(artist == 'Monkey3')") client.play() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples/playback.py0000644000175000017500000000025514606431026022100 0ustar00kalikokalikoimport musicpd # Using a context manager # (use env var if you need to override default host) with musicpd.MPDClient() as client: client.play() client.setvol('80') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/examples.rst0000644000175000017500000000241014606431026020465 0ustar00kalikokaliko.. SPDX-FileCopyrightText: 2018-2023 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later .. _examples: Examples ======== Plain examples -------------- Connect, if playing, get currently playing track, the next one: .. literalinclude:: examples/connect.py :language: python :linenos: Connect a specific password protected host: .. literalinclude:: examples/connect_host.py :language: python :linenos: Start playing current queue and set the volume: .. literalinclude:: examples/playback.py :language: python :linenos: Clear the queue, search artist, queue what's found and play: .. literalinclude:: examples/findadd.py :language: python :linenos: Object Oriented example ----------------------- A plain client monitoring changes on MPD. .. literalinclude:: examples/client.py :language: python :linenos: .. _exceptions_example: Dealing with Exceptions ----------------------- Musicpd module will raise it's own :py:obj:`MPDError` exceptions **and** python :py:obj:`OSError`. Then you can wrap :py:obj:`OSError` in :py:obj:`MPDError` exceptions to have to deal with a single type of exceptions in your code: .. literalinclude:: examples/exceptions.py :language: python :linenos: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/index.rst0000644000175000017500000000342414606431026017764 0ustar00kalikokaliko.. SPDX-FileCopyrightText: 2018-2023 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later .. include:: ../../README.rst Installation ============= **Latest stable release** from `PyPi `_: .. code:: bash pip install python-musicpd **Latest development version** using pip + git: .. code:: bash pip install git+https://gitlab.com/kaliko/python-musicpd.git@dev Library overview ================ Here is a snippet allowing to list the last modified artists in the media library: .. code:: python3 #!/usr/bin/env python3 # coding: utf-8 import musicpd def main(): cli = musicpd.MPDClient() cli.connect() # Gets files count in the library nb_files = int(cli.stats()['songs']) # Gets the last 100 files modified files = cli.search('file', '', 'sort', 'Last-Modified', 'window', (nb_files-100,)) # Print last modified artists in media library print(' / '.join({f.get('albumartist') for f in files})) cli.disconnect() # Script starts here if __name__ == '__main__': main() Build documentation =================== .. code:: bash # Get the source git clone https://gitlab.com/kaliko/python-musicpd.git && cd python-musicpd # Installs sphinx if needed python3 -m venv venv && . ./venv/bin/activate pip install sphinx # Call sphinx sphinx-build -d ./doc/build/doctrees doc/source -b html ./doc/build/html Contents ========= .. toctree:: :maxdepth: 2 self use.rst doc.rst commands.rst examples.rst contribute.rst Indices and tables ================== * :ref:`genindex` * :ref:`search` .. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/doc/source/use.rst0000644000175000017500000003041014606431026017444 0ustar00kalikokaliko.. SPDX-FileCopyrightText: 2018-2023 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later Using the client library ========================= Introduction ------------ The client library can be used as follows: .. code-block:: python client = musicpd.MPDClient() # create client object client.connect() # use MPD_HOST/MPD_PORT if set else # test ${XDG_RUNTIME_DIR}/mpd/socket for existence # fallback to localhost:6600 # connect support host/port argument as well print(client.mpd_version) # print the MPD protocol version client.setvol('42') # sets the volume client.disconnect() # disconnect from the server The MPD command protocol exchanges line-based text records. The client emits a command with optional arguments. In the example above the client sends a `setvol` command with the string argument `42`. MPD commands are exposed as :py:class:`musicpd.MPDClient` methods. Methods **arguments are python strings**. Some commands are composed of more than one word (ie "**tagtypes [disable|enable|all]**"), for these use a `snake case`_ style to access the method. Then **"tagtypes enable"** command is called with **"tagtypes_enable"**. Remember MPD protocol is text based, then all MPD command arguments are UTF-8 strings. In the example above, an integer can be used as argument for the `setvol` command, but it is then evaluated as a string when the command is written to the socket. To avoid confusion use regular string instead of relying on object string representation. :py:class:`musicpd.MPDClient` methods returns different kinds of objects depending on the command. Could be :py:obj:`None`, a single object as a :py:obj:`str` or a :py:obj:`dict`, a list of :py:obj:`dict`. Then :py:class:`musicpd.MPDClient` **methods signatures** are not hard coded within this module since the protocol is handled on the server side. Please refer to the protocol and MPD commands in `MPD protocol documentation`_ to learn how to call commands and what kind of arguments they expect. Some examples are provided for the most common cases, see :ref:`examples`. For a list of currently supported commands in this python module see :ref:`commands`. .. _environment_variables: Environment variables --------------------- :py:class:`musicpd.MPDClient` honors the following environment variables: .. envvar:: MPD_HOST MPD host (:abbr:`FQDN (fully qualified domain name)`, IP, socket path or abstract socket) and password. | To define a **password** set :envvar:`MPD_HOST` to "*password@host*" (password only "*password@*") | For **abstract socket** use "@" as prefix : "*@socket*" and then with a password "*pass@@socket*" | Regular **unix socket** are set with an absolute path: "*/run/mpd/socket*" .. envvar:: MPD_PORT MPD port, relevant for TCP socket only .. envvar:: MPD_TIMEOUT socket timeout when connecting to MPD and waiting for MPD’s response (in seconds) .. envvar:: XDG_RUNTIME_DIR path to look for potential socket .. _default_settings: Default settings ---------------- Default host: * use :envvar:`MPD_HOST` environment variable if set, extract password if present, * else use :envvar:`XDG_RUNTIME_DIR` to looks for an existing file in ``${XDG_RUNTIME_DIR}/mpd/socket``, :envvar:`XDG_RUNTIME_DIR` defaults to ``/run`` if not set. * else set host to ``localhost`` Default port: * use :envvar:`MPD_PORT` environment variable if set * else use ``6600`` Default timeout: * use :envvar:`MPD_TIMEOUT` if set * else use :py:obj:`musicpd.CONNECTION_TIMEOUT` Context manager --------------- Calling MPDClient in a context manager :py:obj:`musicpd.MPDClient.connect` is transparently called with :ref:`default setting` (use :ref:`environment variables` to override defaults). Leaving the context manager :py:obj:`musicpd.MPDClient.disconnect` is called. .. code-block:: python import os os.environ['MPD_HOST'] = 'mpdhost' with MPDClient() as c: c.status() c.next() Command lists ------------- Command lists are also supported using `command_list_ok_begin()` and `command_list_end()` : .. code-block:: python client.command_list_ok_begin() # start a command list client.update() # insert the update command into the list client.status() # insert the status command into the list results = client.command_list_end() # results will be a list with the results Ranges ------ Some commands (e.g. delete) allow specifying a range in the form `"START:END"` (cf. `MPD protocol documentation`_ for more details). Possible ranges are: `"START:END"`, `"START:"` and `":"` : Instead of giving the plain string as `"START:END"`, you **can** provide a :py:obj:`tuple` as `(START,END)`. The module is then ensuring the format is correct and raises an :py:obj:`musicpd.CommandError` exception otherwise. Empty start or end can be specified as en empty string ``''`` or :py:obj:`None`. .. code-block:: python # An intelligent clear # clears played track in the queue, currentsong included pos = client.currentsong().get('pos', 0) # the range object accepts str, no need to convert to int client.delete((0, pos)) # missing end interpreted as highest value possible, pay attention still need a tuple. client.delete((pos,)) # purge queue from current to the end A notable case is the *rangeid* command allowing an empty range specified as a single colon as argument (i.e. sending just ``":"``): .. code-block:: python # sending "rangeid :" to clear the range, play everything client.rangeid(()) # send an empty tuple Empty start in range (i.e. ":END") are not possible and will raise a CommandError. .. note:: Remember the use of a tuple is **optional**. Range can still be specified as a plain string ``"START:END"``. Iterators ---------- Commands may also return iterators instead of lists if `iterate` is set to `True`: .. code-block:: python client.iterate = True 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 (non-blocking call). This is useful for the idle command: .. code-block:: python >>> client.send_idle() # do something else or use function like select() # http://docs.python.org/howto/sockets.html#non-blocking-sockets # ex. select([client], [], []) >>> events = client.fetch_idle() # more complex use for example, with glib/gobject: >>> def callback(source, condition): >>> changes = client.fetch_idle() >>> print changes >>> return False # removes the IO watcher >>> client.send_idle() >>> gobject.io_add_watch(client, gobject.IO_IN, callback) >>> gobject.MainLoop().run() See also use of :ref:`socket timeout` with idle command. Fetching binary content (cover art) ----------------------------------- Fetching album covers is possible with albumart, here is an example: .. code-block:: python >>> cli = musicpd.MPDClient() >>> cli.connect() >>> track = "Steve Reich/1978-Music for 18 Musicians" >>> aart = cli.albumart(track, 0) >>> received = int(aart.get('binary')) >>> size = int(aart.get('size')) >>> with open('/tmp/cover', 'wb') as cover: >>> # aart = {'size': 42, 'binary': 2051, data: bytes(...)} >>> 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() A :py:obj:`musicpd.CommandError` is raised if the album does not expose a cover. You can also use `readpicture` command to fetch embedded picture: .. code-block:: python >>> cli = musicpd.MPDClient() >>> cli.connect() >>> track = 'muse/Amon Tobin/2011-ISAM/01-Amon Tobin - Journeyman.mp3' >>> rpict = cli.readpicture(track, 0) >>> if not rpict: >>> print('No embedded picture found', file=sys.stderr) >>> sys.exit(1) >>> size = int(rpict['size']) >>> done = int(rpict['binary']) >>> with open('/tmp/cover', 'wb') as cover: >>> cover.write(rpict['data']) >>> while size > done: >>> rpict = cli.readpicture(track, done) >>> done += int(rpict['binary']) >>> print(f'writing {rpict["binary"]}, done {100*done/size:03.0f}%') >>> cover.write(rpict['data']) >>> cli.disconnect() Refer to `MPD protocol documentation`_ for the meaning of `binary`, `size` and `data`. .. _socket_timeout: Socket timeout -------------- .. note:: When the timeout is reached it raises a :py:obj:`socket.timeout` exception. An :py:obj:`OSError` subclass. A timeout is used for the initial MPD connection (``connect`` command), then the socket is put in blocking mode with no timeout. Its value is set in :py:obj:`musicpd.CONNECTION_TIMEOUT` at module level and :py:obj:`musicpd.MPDClient.mpd_timeout` in MPDClient instances . However it is possible to set socket timeout for all command setting :py:obj:`musicpd.MPDClient.socket_timeout` attribute to a value in second. Having ``socket_timeout`` enabled can help to detect "half-open connection". For instance loosing connectivity without the server explicitly closing the connection (switching network interface ethernet/wifi, router down, etc…). **Nota bene**: with ``socket_timeout`` enabled each command sent to MPD might timeout. A couple of seconds should be enough for commands to complete except for the special case of ``idle`` command which by definition *“ waits until there is a noteworthy change in one or more of MPD’s subsystems.”* (cf. `MPD protocol documentation`_). Here is a solution to use ``idle`` command with ``socket_timeout``: .. code-block:: python import musicpd import select import socket cli = musicpd.MPDClient() try: cli.socket_timeout = 10 # seconds select_timeout = 5 # second cli.connect() while True: cli.send_idle() # use send_ API to avoid blocking on read _read, _, _ = select.select([cli], [], [], select_timeout) if _read: # tries to read response ret = cli.fetch_idle() print(', '.join(ret)) # Do something else: # cancels idle cli.noidle() except socket.timeout as err: print(f'{err} (timeout {cli.socket_timeout})') except (OSError, musicpd.MPDError) as err: print(f'{err!r}') if cli._sock is not None: cli.disconnect() except KeyboardInterrupt: pass Some explanations: * First launch a non blocking ``idle`` command. This call do not wait for a response to avoid socket timeout waiting for an MPD event. * ``select`` waits for something to read on the socket (the idle response in this case), returns after ``select_timeout`` seconds anyway. * In case there is something to read read it using ``fetch_idle`` * Nothing to read, cancel idle with ``noidle`` All three commands in the while loop (send_idle, fetch_idle, noidle) are not triggering a socket timeout unless the connection is actually lost (actually it could also be that MPD took too much time to answer, but MPD taking more than a couple of seconds for these commands should never occur). .. _exceptions: Exceptions ---------- The :py:obj:`connect` method raises :py:obj:`ConnectionError` only (an :py:obj:`MPDError` exception) but then, calling other MPD commands, the module can raise :py:obj:`MPDError` or an :py:obj:`OSError` depending on the error and where it occurs. Then using musicpd module both :py:obj:`musicpd.MPDError` and :py:obj:`OSError` exceptions families are expected, see :ref:`examples` for a way to deal with this. .. _MPD protocol documentation: http://www.musicpd.org/doc/protocol/ .. _snake case: https://en.wikipedia.org/wiki/Snake_case .. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/musicpd.py0000644000175000017500000007401714606431026016102 0ustar00kalikokaliko# -*- coding: utf-8 -*- # SPDX-FileCopyrightText: 2012-2024 kaliko # SPDX-FileCopyrightText: 2021 Wonko der Verständige # SPDX-FileCopyrightText: 2019 Naglis Jonaitis # SPDX-FileCopyrightText: 2019 Bart Van Loon # SPDX-FileCopyrightText: 2008-2010 J. Alexander Treuman # SPDX-License-Identifier: LGPL-3.0-or-later """Python Music Player Daemon client library""" import logging import os import socket from functools import wraps HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" #: Module version VERSION = '0.9.0' #: Seconds before a connection attempt times out #: (overriden by :envvar:`MPD_TIMEOUT` env. var.) CONNECTION_TIMEOUT = 30 #: Socket timeout in second > 0 (Default is :py:obj:`None` for no timeout) SOCKET_TIMEOUT = None log = logging.getLogger(__name__) def iterator_wrapper(func): """Decorator handling iterate option""" @wraps(func) def decorated_function(instance, *args, **kwargs): generator = func(instance, *args, **kwargs) if not instance.iterate: return list(generator) instance._iterating = True def iterator(gen): try: for item in gen: yield item finally: instance._iterating = False return iterator(generator) return decorated_function class MPDError(Exception): """Main musicpd Exception""" class ConnectionError(MPDError): """Fatal Connection Error, cannot recover from it.""" class ProtocolError(MPDError): """Fatal Protocol Error, cannot recover from it""" class CommandError(MPDError): """Malformed command, socket should be fine, can reuse it""" class CommandListError(MPDError): """""" class PendingCommandError(MPDError): """""" class IteratingError(MPDError): """""" class Range: def __init__(self, tpl): self.tpl = tpl self.lower = '' self.upper = '' self._check() def __str__(self): return f'{self.lower}:{self.upper}' def __repr__(self): return f'Range({self.tpl})' def _check_element(self, item): if item is None or item == '': return '' try: return str(int(item)) except (TypeError, ValueError) as err: raise CommandError(f'Not an integer: "{item}"') from err return item def _check(self): if not isinstance(self.tpl, tuple): raise CommandError('Wrong type, provide a tuple') if len(self.tpl) == 0: return if len(self.tpl) == 1: self.lower = self._check_element(self.tpl[0]) return if len(self.tpl) != 2: raise CommandError('Range wrong size (0, 1 or 2 allowed)') self.lower = self._check_element(self.tpl[0]) self.upper = self._check_element(self.tpl[1]) if self.lower == '' and self.upper != '': raise CommandError(f'Integer expected to start the range: {self.tpl}') if self.upper.isdigit() and self.lower.isdigit(): if int(self.lower) > int(self.upper): raise CommandError(f'Wrong range: {self.lower} > {self.upper}') class _NotConnected: def __getattr__(self, attr): return self._dummy def _dummy(self, *args): raise ConnectionError("Not connected") class MPDClient: """MPDClient instance will look for :envvar:`MPD_HOST`/:envvar:`MPD_PORT`/:envvar:`XDG_RUNTIME_DIR` environment variables and set instance attribute :py:attr:`host`, :py:attr:`port` and :py:obj:`pwd` accordingly. Then :py:obj:`musicpd.MPDClient.connect` will use :py:obj:`host` and :py:obj:`port` as defaults if not provided as args. Regarding :envvar:`MPD_HOST` format to expose password refer this module documentation or MPD client manual :manpage:`mpc (1)`. >>> from os import environ >>> environ['MPD_HOST'] = 'pass@mpdhost' >>> cli = musicpd.MPDClient() >>> cli.pwd == environ['MPD_HOST'].split('@')[0] True >>> cli.host == environ['MPD_HOST'].split('@')[1] True >>> cli.connect() # will use host/port as set in MPD_HOST/MPD_PORT .. note:: default host: * use :envvar:`MPD_HOST` environment variable if set, extract password if present, * else use :envvar:`XDG_RUNTIME_DIR` to looks for an existing file in ``${XDG_RUNTIME_DIR:-/run/}/mpd/socket`` * else set host to ``localhost`` default port: * use :envvar:`MPD_PORT` environment variable is set * else use ``6600`` .. warning:: **Instance attribute host/port/pwd** While :py:attr:`musicpd.MPDClient.host` and :py:attr:`musicpd.MPDClient.port` keep track of current connection host and port, :py:attr:`musicpd.MPDClient.pwd` is set once with password extracted from environment variable. Calling MPS's password method with a new password won't update :py:attr:`musicpd.MPDClient.pwd` value. Moreover, :py:attr:`musicpd.MPDClient.pwd` is only an helper attribute exposing password extracted from :envvar:`MPD_HOST` environment variable, it will not be used as default value for the MPD's password command. """ def __init__(self): self.iterate = False #: Socket timeout value in seconds self._socket_timeout = SOCKET_TIMEOUT #: Current connection timeout value, defaults to #: :py:obj:`CONNECTION_TIMEOUT` or env. var. ``MPD_TIMEOUT`` if provided self.mpd_timeout = None self.mpd_version = '' """Protocol version as exposed by the server as a :py:obj:`str` .. note:: This is the version of the protocol spoken, not the real version of the daemon.""" self._reset() self._commands = { # Status Commands "clearerror": self._fetch_nothing, "currentsong": self._fetch_object, "idle": self._fetch_list, #"noidle": None, "status": self._fetch_object, "stats": self._fetch_object, # Playback Option Commands "consume": self._fetch_nothing, "crossfade": self._fetch_nothing, "mixrampdb": self._fetch_nothing, "mixrampdelay": self._fetch_nothing, "random": self._fetch_nothing, "repeat": self._fetch_nothing, "setvol": self._fetch_nothing, "getvol": self._fetch_object, "single": self._fetch_nothing, "replay_gain_mode": self._fetch_nothing, "replay_gain_status": self._fetch_item, "volume": self._fetch_nothing, # Playback Control Commands "next": self._fetch_nothing, "pause": self._fetch_nothing, "play": self._fetch_nothing, "playid": self._fetch_nothing, "previous": self._fetch_nothing, "seek": self._fetch_nothing, "seekid": self._fetch_nothing, "seekcur": self._fetch_nothing, "stop": self._fetch_nothing, # Queue Commands "add": self._fetch_nothing, "addid": self._fetch_item, "clear": self._fetch_nothing, "delete": self._fetch_nothing, "deleteid": self._fetch_nothing, "move": self._fetch_nothing, "moveid": self._fetch_nothing, "playlist": self._fetch_playlist, "playlistfind": self._fetch_songs, "playlistid": self._fetch_songs, "playlistinfo": self._fetch_songs, "playlistsearch": self._fetch_songs, "plchanges": self._fetch_songs, "plchangesposid": self._fetch_changes, "prio": self._fetch_nothing, "prioid": self._fetch_nothing, "rangeid": self._fetch_nothing, "shuffle": self._fetch_nothing, "swap": self._fetch_nothing, "swapid": self._fetch_nothing, "addtagid": self._fetch_nothing, "cleartagid": self._fetch_nothing, # Stored Playlist Commands "listplaylist": self._fetch_list, "listplaylistinfo": self._fetch_songs, "listplaylists": self._fetch_playlists, "load": self._fetch_nothing, "playlistadd": self._fetch_nothing, "playlistclear": self._fetch_nothing, "playlistdelete": self._fetch_nothing, "playlistmove": self._fetch_nothing, "rename": self._fetch_nothing, "rm": self._fetch_nothing, "save": self._fetch_nothing, # Database Commands "albumart": self._fetch_composite, "count": self._fetch_object, "getfingerprint": self._fetch_object, "find": self._fetch_songs, "findadd": self._fetch_nothing, "list": self._fetch_list, "listall": self._fetch_database, "listallinfo": self._fetch_database, "listfiles": self._fetch_database, "lsinfo": self._fetch_database, "readcomments": self._fetch_object, "readpicture": self._fetch_composite, "search": self._fetch_songs, "searchadd": self._fetch_nothing, "searchaddpl": self._fetch_nothing, "update": self._fetch_item, "rescan": self._fetch_item, # Mounts and neighbors "mount": self._fetch_nothing, "unmount": self._fetch_nothing, "listmounts": self._fetch_mounts, "listneighbors": self._fetch_neighbors, # Sticker Commands "sticker get": self._fetch_item, "sticker set": self._fetch_nothing, "sticker delete": self._fetch_nothing, "sticker list": self._fetch_list, "sticker find": self._fetch_songs, # Connection Commands "close": None, "kill": None, "password": self._fetch_nothing, "ping": self._fetch_nothing, "binarylimit": self._fetch_nothing, "tagtypes": self._fetch_list, "tagtypes disable": self._fetch_nothing, "tagtypes enable": self._fetch_nothing, "tagtypes clear": self._fetch_nothing, "tagtypes all": self._fetch_nothing, # Partition Commands "partition": self._fetch_nothing, "listpartitions": self._fetch_list, "newpartition": self._fetch_nothing, "delpartition": self._fetch_nothing, "moveoutput": self._fetch_nothing, # Audio Output Commands "disableoutput": self._fetch_nothing, "enableoutput": self._fetch_nothing, "toggleoutput": self._fetch_nothing, "outputs": self._fetch_outputs, "outputset": self._fetch_nothing, # Reflection Commands "config": self._fetch_object, "commands": self._fetch_list, "notcommands": self._fetch_list, "urlhandlers": self._fetch_list, "decoders": self._fetch_plugins, # Client to Client "subscribe": self._fetch_nothing, "unsubscribe": self._fetch_nothing, "channels": self._fetch_list, "readmessages": self._fetch_messages, "sendmessage": self._fetch_nothing, } #: host used with the current connection (:py:obj:`str`) self.host = None #: password detected in :envvar:`MPD_HOST` environment variable (:py:obj:`str`) self.pwd = None #: port used with the current connection (:py:obj:`int`, :py:obj:`str`) self.port = None self._get_envvars() def _get_envvars(self): """ Retrieve MPD env. var. to overrides default "localhost:6600" """ # Set some defaults self.host = 'localhost' self.port = os.getenv('MPD_PORT', '6600') _host = os.getenv('MPD_HOST', '') if _host: # If password is set: MPD_HOST=pass@host if '@' in _host: mpd_host_env = _host.split('@', 1) if mpd_host_env[0]: # A password is actually set log.debug('password detected in MPD_HOST, set client pwd attribute') self.pwd = mpd_host_env[0] if mpd_host_env[1]: self.host = mpd_host_env[1] log.debug('host detected in MPD_HOST: %s', self.host) elif mpd_host_env[1]: # No password set but leading @ is an abstract socket self.host = '@'+mpd_host_env[1] log.debug('host detected in MPD_HOST: %s (abstract socket)', self.host) else: # MPD_HOST is a plain host self.host = _host log.debug('host detected in MPD_HOST: %s', self.host) else: # Is socket there xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR', '/run') rundir = os.path.join(xdg_runtime_dir, 'mpd/socket') if os.path.exists(rundir): self.host = rundir log.debug('host detected in ${XDG_RUNTIME_DIR}/run: %s (unix socket)', self.host) _mpd_timeout = os.getenv('MPD_TIMEOUT', '') if _mpd_timeout.isdigit(): self.mpd_timeout = int(_mpd_timeout) log.debug('timeout detected in MPD_TIMEOUT: %d', self.mpd_timeout) else: # Use CONNECTION_TIMEOUT as default even if MPD_TIMEOUT carries gargage self.mpd_timeout = CONNECTION_TIMEOUT def __getattr__(self, attr): if attr == 'send_noidle': # have send_noidle to cancel idle as well as noidle return self.noidle if attr.startswith("send_"): command = attr.replace("send_", "", 1) wrapper = self._send elif attr.startswith("fetch_"): command = attr.replace("fetch_", "", 1) wrapper = self._fetch else: command = attr wrapper = self._execute if command not in self._commands: command = command.replace("_", " ") if command not in self._commands: cls = self.__class__.__name__ raise AttributeError(f"'{cls}' object has no attribute '{attr}'") return lambda *args: wrapper(command, args) def _send(self, command, args): if self._command_list is not None: raise CommandListError("Cannot use send_%s in a command list" % command.replace(" ", "_")) self._write_command(command, args) retval = self._commands[command] if retval is not None: self._pending.append(command) def _fetch(self, command, args=None): # pylint: disable=unused-argument cmd_fmt = command.replace(" ", "_") if self._command_list is not None: raise CommandListError(f"Cannot use fetch_{cmd_fmt} in a command list") if self._iterating: raise IteratingError(f"Cannot use fetch_{cmd_fmt} while iterating") if not self._pending: raise PendingCommandError("No pending commands to fetch") if self._pending[0] != command: raise PendingCommandError(f"'{command}' is not the currently pending command") del self._pending[0] retval = self._commands[command] if callable(retval): return retval() return retval def _execute(self, command, args): # pylint: disable=unused-argument if self._iterating: raise IteratingError(f"Cannot execute '{command}' while iterating") if self._pending: raise PendingCommandError(f"Cannot execute '{command}' with pending commands") retval = self._commands[command] if self._command_list is not None: if not callable(retval): raise CommandListError(f"'{command}' not allowed in command list") self._write_command(command, args) self._command_list.append(retval) else: self._write_command(command, args) if callable(retval): return retval() return retval return None def _write_line(self, line): self._wfile.write(f"{line!s}\n") self._wfile.flush() def _write_command(self, command, args=None): if args is None: args = [] parts = [command] for arg in args: if isinstance(arg, tuple): parts.append(f'{Range(arg)!s}') else: parts.append(f'"{escape(str(arg))}"') if '\n' in ' '.join(parts): raise CommandError('new line found in the command!') self._write_line(" ".join(parts)) def _read_binary(self, amount): chunk = bytearray() while amount > 0: result = self._rbfile.read(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") line = line.rstrip("\n") if line.startswith(ERROR_PREFIX): error = line[len(ERROR_PREFIX):].strip() raise CommandError(error) if self._command_list is not None: if line == NEXT: return None if line == SUCCESS: raise ProtocolError(f"Got unexpected '{SUCCESS}'") elif line == SUCCESS: return None return line def _read_pair(self, separator, binary=False): line = self._read_line(binary=binary) if line is None: return None pair = line.split(separator, 1) if len(pair) < 2: raise ProtocolError(f"Could not parse pair: '{line}'") return pair def _read_pairs(self, separator=": ", binary=False): pair = self._read_pair(separator, binary=binary) while pair: yield pair pair = self._read_pair(separator, binary=binary) def _read_list(self): seen = None for key, value in self._read_pairs(): if key != seen: if seen is not None: raise ProtocolError(f"Expected key '{seen}', got '{key}'") seen = key yield value def _read_playlist(self): for _, value in self._read_pairs(":"): yield value def _read_objects(self, delimiters=None): obj = {} if delimiters is None: delimiters = [] for key, value in self._read_pairs(): key = key.lower() if obj: if key in delimiters: yield obj obj = {} elif key in obj: if not isinstance(obj[key], list): obj[key] = [obj[key], value] else: obj[key].append(value) continue obj[key] = value if obj: yield obj def _read_command_list(self): try: for retval in self._command_list: yield retval() finally: self._command_list = None self._fetch_nothing() def _fetch_nothing(self): line = self._read_line() if line is not None: raise ProtocolError(f"Got unexpected return value: '{line}'") def _fetch_item(self): pairs = list(self._read_pairs()) if len(pairs) != 1: return None return pairs[0][1] @iterator_wrapper def _fetch_list(self): return self._read_list() @iterator_wrapper def _fetch_playlist(self): return self._read_playlist() def _fetch_object(self): objs = list(self._read_objects()) if not objs: return {} return objs[0] @iterator_wrapper def _fetch_objects(self, delimiters): return self._read_objects(delimiters) def _fetch_changes(self): return self._fetch_objects(["cpos"]) def _fetch_songs(self): return self._fetch_objects(["file"]) def _fetch_playlists(self): return self._fetch_objects(["playlist"]) def _fetch_database(self): return self._fetch_objects(["file", "directory", "playlist"]) def _fetch_outputs(self): return self._fetch_objects(["outputid"]) def _fetch_plugins(self): return self._fetch_objects(["plugin"]) def _fetch_messages(self): return self._fetch_objects(["channel"]) def _fetch_mounts(self): return self._fetch_objects(["mount"]) def _fetch_neighbors(self): return self._fetch_objects(["neighbor"]) def _fetch_composite(self): obj = {} for key, value in self._read_pairs(binary=True): key = key.lower() obj[key] = value if key == 'binary': break if not obj: # If the song file was recognized, but there is no picture, the # response is successful, but is otherwise empty. return obj amount = int(obj['binary']) try: obj['data'] = self._read_binary(amount) except IOError as err: raise ConnectionError(f'Error reading binary content: {err}') from err data_bytes = len(obj['data']) if data_bytes != amount: # can we ever get there? raise ConnectionError('Error reading binary content: ' f'Expects {amount}B, got {data_bytes}') # Fetches trailing new line self._read_line(binary=True) # Fetches SUCCESS code self._read_line(binary=True) return obj @iterator_wrapper def _fetch_command_list(self): return self._read_command_list() def _hello(self): line = self._rfile.readline() if not line.endswith("\n"): raise ConnectionError("Connection lost while reading MPD hello") line = line.rstrip("\n") if not line.startswith(HELLO_PREFIX): raise ProtocolError(f"Got invalid MPD hello: '{line}'") self.mpd_version = line[len(HELLO_PREFIX):].strip() def _reset(self): self.mpd_version = '' self._iterating = False self._pending = [] self._command_list = None self._sock = None self._rfile = _NotConnected() self._rbfile = _NotConnected() self._wfile = _NotConnected() def _connect_unix(self, path): if not hasattr(socket, "AF_UNIX"): raise ConnectionError("Unix domain sockets not supported on this platform") # abstract socket if path.startswith('@'): path = '\0'+path[1:] try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(self.mpd_timeout) sock.connect(path) sock.settimeout(self.socket_timeout) except socket.error as socket_err: raise ConnectionError(socket_err) from socket_err return sock def _connect_tcp(self, host, port): try: flags = socket.AI_ADDRCONFIG except AttributeError: flags = 0 err = None try: gai = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags) except socket.error as gaierr: raise ConnectionError(gaierr) from gaierr for res in gai: af, socktype, proto, _, sa = res sock = None try: log.debug('opening socket %s', sa) sock = socket.socket(af, socktype, proto) sock.settimeout(self.mpd_timeout) sock.connect(sa) sock.settimeout(self.socket_timeout) return sock except socket.error as socket_err: log.debug('opening socket %s failed: %s', sa, socket_err) err = socket_err if sock is not None: sock.close() if err is not None: raise ConnectionError(err) raise ConnectionError("getaddrinfo returns an empty list") def noidle(self): # noidle's special case if not self._pending or self._pending[0] != 'idle': raise CommandError('cannot send noidle if send_idle was not called') del self._pending[0] self._write_command("noidle") return self._fetch_list() def connect(self, host=None, port=None): """Connects the MPD server :param str host: hostname, IP or FQDN (defaults to *localhost* or socket) :param port: port number (defaults to *6600*) :type port: str or int If host/port are :py:obj:`None` the socket uses :py:attr:`host`/:py:attr:`port` attributes as defaults. Cf. :py:obj:`MPDClient` for the logic behind default host/port. The underlying socket also honors :envvar:`MPD_TIMEOUT` environment variable and defaults to :py:obj:`musicpd.CONNECTION_TIMEOUT` (connect command only). If you want to have a timeout for each command once you got connected, set its value in :py:obj:`MPDClient.socket_timeout` (in second) or at module level in :py:obj:`musicpd.SOCKET_TIMEOUT`. """ if not host: host = self.host else: self.host = host if not port: port = self.port else: self.port = port if self._sock is not None: raise ConnectionError("Already connected") if host[0] in ['/', '@']: log.debug('Connecting unix socket %s', host) self._sock = self._connect_unix(host) else: log.debug('Connecting tcp socket %s:%s (timeout: %ss)', host, port, self.mpd_timeout) 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() except: self.disconnect() raise log.debug('Connected') @property def socket_timeout(self): """Socket timeout in second (defaults to :py:obj:`SOCKET_TIMEOUT`). Use :py:obj:`None` to disable socket timout. :setter: Set the socket timeout (integer > 0) :type: int or None """ return self._socket_timeout @socket_timeout.setter def socket_timeout(self, timeout): if timeout is not None: if int(timeout) <= 0: raise ValueError('socket_timeout expects a non zero positive integer') self._socket_timeout = int(timeout) else: self._socket_timeout = timeout if getattr(self._sock, 'settimeout', False): self._sock.settimeout(self._socket_timeout) def disconnect(self): """Closes the MPD connection. The client closes the actual socket, it does not use the 'close' request from MPD protocol (as suggested in documentation). """ 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'): self._sock.close() self._reset() def __enter__(self): self.connect() return self def __exit__(self, exception_type, exception_value, exception_traceback): self.disconnect() def fileno(self): """Return the socket’s file descriptor (a small integer). This is useful with :py:obj:`select.select`. """ if self._sock is None: raise ConnectionError("Not connected") return self._sock.fileno() def command_list_ok_begin(self): if self._command_list is not None: raise CommandListError("Already in command list") if self._iterating: raise IteratingError("Cannot begin command list while iterating") if self._pending: raise PendingCommandError("Cannot begin command list with pending commands") self._write_command("command_list_ok_begin") self._command_list = [] def command_list_end(self): if self._command_list is None: raise CommandListError("Not in command list") if self._iterating: raise IteratingError("Already iterating over a command list") self._write_command("command_list_end") return self._fetch_command_list() def escape(text): return text.replace("\\", "\\\\").replace('"', '\\"') # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992790.0 python-musicpd-0.9.0/pyproject.toml0000644000175000017500000000211314606431026016764 0ustar00kalikokaliko# SPDX-FileCopyrightText: 2023 kaliko # SPDX-License-Identifier: LGPL-3.0-or-later [project] name = "python-musicpd" keywords = ["mpd", "Music Player Daemon"] description = "An MPD (Music Player Daemon) client library written in pure Python." authors = [ { name="Kaliko Jack", email="kaliko@azylum.org" }, ] license = {file = "LICENSE.txt"} readme = "README.rst" requires-python = ">=3.6" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["version"] [project.optional-dependencies] sphinx = ["Sphinx>=5.3.0"] [project.urls] "Homepage" = "https://kaliko.me/python-musicpd/" [build-system] requires = ["setuptools>=61.0.0"] [tool.setuptools] py-modules = ["musicpd"] [tool.setuptools.dynamic] version = {attr = "musicpd.VERSION"} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/python_musicpd.egg-info/0000755000175000017500000000000014606431054020613 5ustar00kalikokaliko././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992811.0 python-musicpd-0.9.0/python_musicpd.egg-info/PKG-INFO0000644000175000017500000002373614606431053021722 0ustar00kalikokalikoMetadata-Version: 2.1 Name: python-musicpd Version: 0.9.0 Summary: An MPD (Music Player Daemon) client library written in pure Python. Author-email: Kaliko Jack License: GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. Project-URL: Homepage, https://kaliko.me/python-musicpd/ Keywords: mpd,Music Player Daemon Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Description-Content-Type: text/x-rst License-File: LICENSE.txt Provides-Extra: sphinx Requires-Dist: Sphinx>=5.3.0; extra == "sphinx" Music Player Daemon client module ********************************* An MPD (Music Player Daemon) client library written in pure Python. ---- :Documentation: https://kaliko.gitlab.io/python-musicpd :MPD Protocol: https://www.musicpd.org/doc/html/protocol.html :Code: https://gitlab.com/kaliko/python-musicpd :Dependencies: None :Compatibility: Python 3.6+ :Licence: GNU LGPLv3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992811.0 python-musicpd-0.9.0/python_musicpd.egg-info/SOURCES.txt0000644000175000017500000000116714606431053022503 0ustar00kalikokalikoCHANGES.txt LICENSE.txt MANIFEST.in README.rst musicpd.py pyproject.toml doc/source/commands.rst doc/source/commands.txt doc/source/conf.py doc/source/contribute.rst doc/source/doc.rst doc/source/examples.rst doc/source/index.rst doc/source/use.rst doc/source/examples/client.py doc/source/examples/connect.py doc/source/examples/connect_host.py doc/source/examples/exceptions.py doc/source/examples/findadd.py doc/source/examples/playback.py python_musicpd.egg-info/PKG-INFO python_musicpd.egg-info/SOURCES.txt python_musicpd.egg-info/dependency_links.txt python_musicpd.egg-info/requires.txt python_musicpd.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992811.0 python-musicpd-0.9.0/python_musicpd.egg-info/dependency_links.txt0000644000175000017500000000000114606431053024660 0ustar00kalikokaliko ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992811.0 python-musicpd-0.9.0/python_musicpd.egg-info/requires.txt0000644000175000017500000000003014606431053023203 0ustar00kalikokaliko [sphinx] Sphinx>=5.3.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712992811.0 python-musicpd-0.9.0/python_musicpd.egg-info/top_level.txt0000644000175000017500000000001014606431053023333 0ustar00kalikokalikomusicpd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712992811.8039522 python-musicpd-0.9.0/setup.cfg0000644000175000017500000000004614606431054015675 0ustar00kalikokaliko[egg_info] tag_build = tag_date = 0