././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1651942227.3781245 python-musicpd-0.8.0/0000755000175000017500000000000000000000000014351 5ustar00gberretgberret././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/CHANGES.txt0000644000175000017500000000642000000000000016164 0ustar00gberretgberretpython-musicpd Changes List =========================== 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=1486977948.0 python-musicpd-0.8.0/LICENSE.txt0000644000175000017500000001674300000000000016207 0ustar00gberretgberret 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=1609322916.0 python-musicpd-0.8.0/MANIFEST.in0000644000175000017500000000006300000000000016106 0ustar00gberretgberretinclude CHANGES.txt recursive-include doc/source * ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1651942227.3781245 python-musicpd-0.8.0/PKG-INFO0000644000175000017500000000247300000000000015454 0ustar00gberretgberretMetadata-Version: 2.1 Name: python-musicpd Version: 0.8.0 Summary: An MPD (Music Player Daemon) client library written in pure Python. Home-page: https://kaliko.me/python-musicpd/ Author: Kaliko Jack Author-email: kaliko@azylum.org License: LGPLv3 Download-URL: https://pypi.org/project/python-musicpd/ Description: Python MusicPlayerDaemon 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 Keywords: mpd,Music Player Daemon Platform: UNKNOWN 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1644334197.0 python-musicpd-0.8.0/README.rst0000644000175000017500000000062700000000000016045 0ustar00gberretgberretPython MusicPlayerDaemon 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=1651942227.3781245 python-musicpd-0.8.0/doc/0000755000175000017500000000000000000000000015116 5ustar00gberretgberret././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1651942227.3781245 python-musicpd-0.8.0/doc/source/0000755000175000017500000000000000000000000016416 5ustar00gberretgberret././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/doc/source/commands.rst0000644000175000017500000000057500000000000020760 0ustar00gberretgberret.. SPDX-FileCopyrightText: 2018-2021 kaliko .. SPDX-License-Identifier: GPL-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.6.0: .. literalinclude:: commands.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622192472.0 python-musicpd-0.8.0/doc/source/commands.txt0000644000175000017500000001532700000000000020770 0ustar00gberretgberret== 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/doc/source/conf.py0000644000175000017500000003045500000000000017724 0ustar00gberretgberret# coding: utf-8 # SPDX-FileCopyrightText: 2018-2021 kaliko # SPDX-License-Identifier: GPL-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 = {'https://docs.python.org/': None} # autodoc config autodoc_member_order = 'bysource' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/doc/source/contribute.rst0000644000175000017500000000173300000000000021332 0ustar00gberretgberret.. SPDX-FileCopyrightText: 2018-2021 kaliko .. SPDX-License-Identifier: GPL-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=1651935681.0 python-musicpd-0.8.0/doc/source/doc.rst0000644000175000017500000000046000000000000017715 0ustar00gberretgberret.. SPDX-FileCopyrightText: 2018-2021 kaliko .. SPDX-License-Identifier: GPL-3.0-or-later musicpd namespace ================= .. autodata:: musicpd.CONNECTION_TIMEOUT .. autodata:: musicpd.SOCKET_TIMEOUT .. autoclass:: musicpd.MPDClient :members: .. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/doc/source/index.rst0000644000175000017500000000346100000000000020263 0ustar00gberretgberret.. SPDX-FileCopyrightText: 2018-2021 kaliko .. SPDX-License-Identifier: GPL-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 # And build python3 setup.py build_sphinx # Or call sphinx sphinx-build -d ./doc/build/doctrees doc/source -b html ./doc/build/html Contents ========= .. toctree:: :maxdepth: 2 use.rst doc.rst commands.rst contribute.rst Indices and tables ================== * :ref:`genindex` * :ref:`search` .. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/doc/source/use.rst0000644000175000017500000002360100000000000017746 0ustar00gberretgberret.. SPDX-FileCopyrightText: 2018-2021 kaliko .. SPDX-License-Identifier: GPL-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 print(client.cmd('foo', 42)) # print result of the request "cmd foo 42" # (nb. for actual command, see link to the protocol below) client.disconnect() # disconnect from the server In the example above `cmd` in not an actual MPD command, for a list of supported commands, their arguments (as MPD currently understands them), and the functions used to parse their responses see :ref:`commands`. See the `MPD protocol documentation`_ for more details. .. _environment_variables: Environment variables --------------------- The client honors the following environment variables: * ``MPD_HOST`` MPD host (:abbr:`FQDN (fully qualified domain name)`, socket path or abstract socket) and password. | To define a password set 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`" * ``MPD_PORT`` MPD port, relevant for TCP socket only, ie with :abbr:`FQDN (fully qualified domain name)` defined host * ``MPD_TIMEOUT`` timeout for connecting to MPD and waiting for MPD’s response in seconds * ``XDG_RUNTIME_DIR`` path to look for potential socket: ``${XDG_RUNTIME_DIR}/mpd/socket`` .. _default_settings: Default settings ---------------- * If ``MPD_HOST`` is not set, then look for a socket in ``${XDG_RUNTIME_DIR}/mpd/socket`` * If there is no socket use ``localhost`` * If ``MPD_PORT`` is not set, then use ``6600`` * If ``MPD_TIMEOUT`` is not set, then uses :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 ------ Provide a 2-tuple as argument for command supporting ranges (cf. `MPD protocol documentation`_ for more details). Possible ranges are: "START:END", "START:" and ":" : .. code-block:: python # An intelligent clear # clears played track in the queue, currentsong included pos = client.currentsong().get('pos', 0) # the 2-tuple 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. 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 `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). .. _MPD protocol documentation: http://www.musicpd.org/doc/protocol/ .. vim: spell spelllang=en ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651942166.0 python-musicpd-0.8.0/musicpd.py0000644000175000017500000006721600000000000016403 0ustar00gberretgberret# SPDX-FileCopyrightText: 2012-2022 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: GPL-3.0-or-later """python-musicpd: Python Music Player Daemon client library""" import socket import os from functools import wraps HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" VERSION = '0.8.0' #: Seconds before a connection attempt times out #: (overriden by MPD_TIMEOUT env. var.) CONNECTION_TIMEOUT = 30 #: Socket timeout in second (Default is None for no timeout) SOCKET_TIMEOUT = None 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): pass class ConnectionError(MPDError): pass class ProtocolError(MPDError): pass class CommandError(MPDError): pass class CommandListError(MPDError): pass class PendingCommandError(MPDError): pass class IteratingError(MPDError): pass class Range: def __init__(self, tpl): self.tpl = tpl self._check() def __str__(self): if len(self.tpl) == 0: return ':' if len(self.tpl) == 1: return '{0}:'.format(self.tpl[0]) return '{0[0]}:{0[1]}'.format(self.tpl) def __repr__(self): return 'Range({0})'.format(self.tpl) def _check(self): if not isinstance(self.tpl, tuple): raise CommandError('Wrong type, provide a tuple') if len(self.tpl) not in [0, 1, 2]: raise CommandError('length not in [0, 1, 2]') for index in self.tpl: try: index = int(index) except (TypeError, ValueError) as err: raise CommandError('Not a tuple of int') from err class _NotConnected: def __getattr__(self, attr): return self._dummy def _dummy(self, *args): raise ConnectionError("Not connected") class MPDClient: """MPDClient instance will look for ``MPD_HOST``/``MPD_PORT``/``XDG_RUNTIME_DIR`` environment variables and set instance attribute ``host``, ``port`` and ``pwd`` accordingly. Regarding ``MPD_HOST`` format to expose password refer MPD client manual :manpage:`mpc (1)`. Then :py:obj:`musicpd.MPDClient.connect` will use ``host`` and ``port`` as defaults if not provided as args. Cf. :py:obj:`musicpd.MPDClient.connect` for details. >>> 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 :ivar str host: host used with the current connection :ivar str,int port: port used with the current connection :ivar str pwd: password detected in ``MPD_HOST`` environment variable .. 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 :py:meth:`musicpd.MPDClient().password()` 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 ``MPD_HOST`` environment variable, it will not be used as default value for the :py:meth:`password` method """ 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._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, } self._get_envvars() def _get_envvars(self): """ Retrieve MPD env. var. to overrides "localhost:6600" Use MPD_HOST/MPD_PORT if set else use MPD_HOST=${XDG_RUNTIME_DIR:-/run/}/mpd/socket if file exists """ self.host = 'localhost' self.pwd = None self.port = os.getenv('MPD_PORT', '6600') if os.getenv('MPD_HOST'): # If password is set: MPD_HOST=pass@host if '@' in os.getenv('MPD_HOST'): mpd_host_env = os.getenv('MPD_HOST').split('@', 1) if mpd_host_env[0]: # A password is actually set self.pwd = mpd_host_env[0] if mpd_host_env[1]: self.host = mpd_host_env[1] elif mpd_host_env[1]: # No password set but leading @ is an abstract socket self.host = '@'+mpd_host_env[1] else: # MPD_HOST is a plain host self.host = os.getenv('MPD_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 self.mpd_timeout = os.getenv('MPD_TIMEOUT') if self.mpd_timeout and self.mpd_timeout.isdigit(): self.mpd_timeout = int(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('{0!s}'.format(Range(arg))) else: parts.append('"%s"' % 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 = None 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:] sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(self.mpd_timeout) sock.connect(path) sock.settimeout(self.socket_timeout) return sock def _connect_tcp(self, host, port): try: flags = socket.AI_ADDRCONFIG except AttributeError: flags = 0 err = None for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags): af, socktype, proto, _, sa = res sock = None try: 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: err = socket_err if sock is not None: sock.close() if err is not None: raise ConnectionError(str(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, see below for details) :param port: port number (defaults to 6600) :type port: str or int The connect method honors MPD_HOST/MPD_PORT environment variables. The underlying socket also honors 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`. .. note:: Default host/port If host evaluate to :py:obj:`False` * use ``MPD_HOST`` environment variable if set, extract password if present, * else looks for an existing file in ``${XDG_RUNTIME_DIR:-/run/}/mpd/socket`` * else set host to ``localhost`` If port evaluate to :py:obj:`False` * if ``MPD_PORT`` environment variable is set, use it for port * else use ``6600`` """ 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 ['/', '@']: self._sock = self._connect_unix(host) else: self._sock = self._connect_tcp(host, port) self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape') self._rbfile = self._sock.makefile("rb") self._wfile = self._sock.makefile("w", encoding='utf-8') try: self._hello() except: self.disconnect() raise @property def socket_timeout(self): """Socket timeout in second (defaults to :py:obj:`SOCKET_TIMEOUT`). Use None to disable socket timout.""" return self._socket_timeout @socket_timeout.setter def socket_timeout(self, timeout): 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: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1651942227.3781245 python-musicpd-0.8.0/python_musicpd.egg-info/0000755000175000017500000000000000000000000021110 5ustar00gberretgberret././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651942227.0 python-musicpd-0.8.0/python_musicpd.egg-info/PKG-INFO0000644000175000017500000000247300000000000022213 0ustar00gberretgberretMetadata-Version: 2.1 Name: python-musicpd Version: 0.8.0 Summary: An MPD (Music Player Daemon) client library written in pure Python. Home-page: https://kaliko.me/python-musicpd/ Author: Kaliko Jack Author-email: kaliko@azylum.org License: LGPLv3 Download-URL: https://pypi.org/project/python-musicpd/ Description: Python MusicPlayerDaemon 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 Keywords: mpd,Music Player Daemon Platform: UNKNOWN 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651942227.0 python-musicpd-0.8.0/python_musicpd.egg-info/SOURCES.txt0000644000175000017500000000057400000000000023002 0ustar00gberretgberretCHANGES.txt LICENSE.txt MANIFEST.in README.rst musicpd.py setup.cfg setup.py doc/source/commands.rst doc/source/commands.txt doc/source/conf.py doc/source/contribute.rst doc/source/doc.rst doc/source/index.rst doc/source/use.rst python_musicpd.egg-info/PKG-INFO python_musicpd.egg-info/SOURCES.txt python_musicpd.egg-info/dependency_links.txt python_musicpd.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651942227.0 python-musicpd-0.8.0/python_musicpd.egg-info/dependency_links.txt0000644000175000017500000000000100000000000025156 0ustar00gberretgberret ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651942227.0 python-musicpd-0.8.0/python_musicpd.egg-info/top_level.txt0000644000175000017500000000001000000000000023631 0ustar00gberretgberretmusicpd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1651942227.3781245 python-musicpd-0.8.0/setup.cfg0000644000175000017500000000203600000000000016173 0ustar00gberretgberret[sdist] formats = gztar,zip [metadata] name = python-musicpd description = An MPD (Music Player Daemon) client library written in pure Python. version = attr: musicpd.VERSION long_description = file: README.rst long_description_content_type = text/x-rst license = LGPLv3 license_file = LICENSE.txt author = Kaliko Jack author_email = kaliko@azylum.org url = https://kaliko.me/python-musicpd/ download_url = https://pypi.org/project/python-musicpd/ keywords = mpd, Music Player Daemon 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 [options] python_requires = >=3.6 py_modules = musicpd [bdist_wheel] universal = 0 [build_sphinx] source-dir = doc/source build-dir = build all_files = 1 [pycodestyle] max_line_length = 88 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651935681.0 python-musicpd-0.8.0/setup.py0000755000175000017500000000037200000000000016070 0ustar00gberretgberret#! /usr/bin/env python3 # coding: utf-8 # SPDX-FileCopyrightText: 2012-2021 kaliko # SPDX-License-Identifier: GPL-3.0-or-later from setuptools import setup setup() # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: