././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 011451 x ustar 00 0000000 0000000 27 mtime=1578764319.882595
musicbrainzngs-0.7.1/ 0000755 0000765 0000024 00000000000 00000000000 015501 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578763476.0
musicbrainzngs-0.7.1/CHANGES 0000644 0000765 0000024 00000012767 00000000000 016511 0 ustar 00alastair staff 0000000 0000000 0.7.1 (2020-01-11):
* include README file in pypi
0.7 (2020-01-09):
* removed support for PUIDs and Echoprint (Alastair Porter, #237)
* removed the 'artists' include for work lookup (Alastair Porter, #231 & #227)
* allow the 'work-level-rels' include for recording lookups (Shen-Ta Hsieh, #213)
* added support for 'target-credit' elements (Itay Brandes, #162 & #217)
* update valid search fields (Alastair Porter, #239)
* use https by default with musicbrainz.org (Frederik “Freso” S. Olesen, #197)
0.6 (2016-04-11):
* don't require authentication when getting public collections (#87)
* allow submit_ratings() and submit_tags() to submit for all supported entities (Ian McEwen, #145)
* allow 'tags' and 'user-tags' includes on releases (Jérémie Detrey, #150)
* set the parser when the webservice format is changed
* read the error message from musicbrainz and return it in
a raised exception
* send authenticaion headers when required (Ryan Helinski, #153)
* added get_series_by_id(), search_areas(), search_series() (Ian McEwen, #148)
* updated options for get_releases_by_discid() to support 'media-format'
and discid-less requests (Ian McEwen, #148)
* parse work attributes (Wieland Hoffmann, #151)
* added various methods to retrieve data from the Cover Art Archive (Alastair Porter & Wieland Hoffmann, #115)
* added support for pregap tracks (Rui Gonçalves, #154 & #165)
* return 'offset-list' and 'offset-count' for get_releases_by_discid()
(Johannes Dewender, #169)
* added support for search and browse of events (Shadab Zafar, #168)
* added support for 'data-track-list' elements (Jérémie Detrey, #180)
* added support for get and search instruments
* added support to read all collection types (#175)
* added support for search and browse of places (#176)
* allow single strings to be used as includes for browse requests (#172)
* allow single strings to be used at tag submission (#172)
* added support for browse artist by work and work by artist
* added support for 'track-count' elements in 'medium-list's returned by search
* added support to read xml attributes in 'attribute-list' elements (#142)
0.5 (2014-02-06):
* added get_url_by_id() and browse_urls() (Ian McEwen, #117)
* added get_area_by_id() and get_place_by_id() (Ian McEwen, #119 + #132)
* added support for custom parsers with set_parser() (Ryan Helinski, #129)
* added support for different WS formats with set_format() (Johannes Dewender, #131)
* added support for URL MBIDs (Ian McEwen, #132)
* added support for link type UUIDs (Ian McEwen, #127 + #132)
* support fuzzy disc lookup by TOC (Johannes Dewender, #105)
* add -count element for browse and search requests (Johannes Dewender, #135)
* deprecated puid and echoprint support (Johannes Dewender, #106)
* updated valid includes and browse includes (Ian McEwen, #118)
* updated valid search fields and release group types (Ian McEwen, #132)
* browsing for get_releases_in_collection() (Johannes Dewender, #88 + #128)
* allow browsing releases by track_artist (Johannes Dewender, #107)
* fix list submission for isrcs (Johannes Dewender, #113)
* fix debug logging and many unparsed entities (Johannes Dewender, #134)
* don't install tests with setup.py (Johannes Dewender, #112)
* add ISC license (compat.py) to COPYING (Wieland Hoffmann, #111 and #110)
* parse the video element of recordings (Wieland Hoffmann, #136)
* parse track ids (Wieland Hoffmann)
* fixed undefined name in submit_barcodes (Simon Chopin, #109)
The github repository and RTD doc urls were renamed to python-musicbrainzngs
(formerly python-musicbrainz-ngs).
0.4 (2013-05-15):
Thanks to Johannes Dewender for all his work in this release!
* Improve documentation
* Fix get_recordings_by_puid/isrc
* Update search fields
* Parse CDStubs in release results
* Correct release_type/release_status checking
* Allow iso-8859-1 passwords
* Convert single isrcs to list when submitting
* Parse ISRC results
* Escape forward slashes in search queries (Adrian Sampson)
* Package documentation and examples in release (Alastair Porter)
0.3 (2013-03-11):
* Lots of bug fixes! also:
* Catch network errors when reading data (Adrian Sampson, #78)
* Get and search annotations (Wieland Hoffmann)
* Better alias support (Sam Doshi, #83, #86)
* Parse track artist-credit if present (Galen Hazelwood, #75)
* Show relevancy scores on search results (Alastair Porter, #37)
* Perform searches in lower case (Adrian Sampson, #36)
* Use AND instead of OR by default in searches (Johannes Dewender)
* Parse artist disambiguation field (Paul Bailey, #48)
* Send zero-length body requests correctly (Adrian Sampson)
* Fix bug in get methods when includes, release status, or release type
are included (Alastair Porter, reported by palli81)
* Support python 2 and python 3
* Update valid includes for some entity queries
* Add usage examples
0.2 (2012-03-06):
* ISRC submission support (Wieland Hoffmann)
* Various submission bug fixes (Wieland Hoffmann)
* Retry the query if the connection is reset (Adrian Sampson)
* Rename some methods to make the API more consistent (Alastair Porter)
* Use test methods from Python 2.6 (Alastair Porter)
0.1: Initial release
Contributions by Alastair Porter, Adrian Sampson, Michael Marineau,
Thomas Vander Stichele, Ian McEwen
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1381268676.0
musicbrainzngs-0.7.1/COPYING 0000644 0000765 0000024 00000004104 00000000000 016533 0 ustar 00alastair staff 0000000 0000000 Copyright 2011 Alastair Porter, Adrian Sampson, and others.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The license for the file `musicbrainzngs/compat.py` is
Copyright (c) 2012 Kenneth Reitz.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1405848834.0
musicbrainzngs-0.7.1/MANIFEST.in 0000644 0000765 0000024 00000000502 00000000000 017234 0 ustar 00alastair staff 0000000 0000000 include COPYING README.rst CHANGES query.py
recursive-include test *.py
recursive-include test/data *.xml
include test/data/artist test/data/label test/data/release
include test/data/release-group test/data/work
recursive-include docs *.rst
include docs/conf.py docs/Makefile docs/make.bat
recursive-include examples *.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8822947
musicbrainzngs-0.7.1/PKG-INFO 0000644 0000765 0000024 00000007404 00000000000 016603 0 ustar 00alastair staff 0000000 0000000 Metadata-Version: 2.1
Name: musicbrainzngs
Version: 0.7.1
Summary: Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices
Home-page: https://python-musicbrainzngs.readthedocs.io/
Author: Alastair Porter
Author-email: alastair@porter.net.nz
License: BSD 2-clause
Description: Musicbrainz NGS bindings
########################
This library implements webservice bindings for the Musicbrainz NGS site, also known as /ws/2
and the `Cover Art Archive `_.
For more information on the musicbrainz webservice see ``_.
Usage
*****
.. code:: python
# Import the module
import musicbrainzngs
# If you plan to submit data, authenticate
musicbrainzngs.auth("user", "password")
# Tell musicbrainz what your app is, and how to contact you
# (this step is required, as per the webservice access rules
# at http://wiki.musicbrainz.org/XML_Web_Service/Rate_Limiting )
musicbrainzngs.set_useragent("Example music app", "0.1", "http://example.com/music")
# If you are connecting to a different server
musicbrainzngs.set_hostname("beta.musicbrainz.org")
See the ``query.py`` file for more examples.
More documentation is available at
`Read the Docs `_.
Contribute
**********
If you want to contribute to this repository, please read `the
contribution guidelines
`_ first.
Authors
*******
These bindings were written by `Alastair Porter `_.
Contributions have been made by:
* `Adrian Sampson `_
* `Corey Farwell `_
* `Galen Hazelwood `_
* `Greg Ward `_
* `Ian McEwen `_
* `Jérémie Detrey `_
* `Johannes Dewender `_
* `Michael Marineau `_
* `Patrick Speiser `_
* `Pavan Chander `_
* `Paul Bailey `_
* `Rui Gonçalves `_
* `Ryan Helinski `_
* `Sam Doshi `_
* `Shadab Zafar `_
* `Simon Chopin `_
* `Thomas Vander Stichele `_
* `Wieland Hoffmann `_
License
*******
This library is released under the simplified BSD license except for the file
``musicbrainzngs/compat.py`` which is licensed under the ISC license.
See COPYING for details.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: BSD License
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Description-Content-Type: text/x-rst
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1456054330.0
musicbrainzngs-0.7.1/README.rst 0000644 0000765 0000024 00000004617 00000000000 017200 0 ustar 00alastair staff 0000000 0000000 Musicbrainz NGS bindings
########################
This library implements webservice bindings for the Musicbrainz NGS site, also known as /ws/2
and the `Cover Art Archive `_.
For more information on the musicbrainz webservice see ``_.
Usage
*****
.. code:: python
# Import the module
import musicbrainzngs
# If you plan to submit data, authenticate
musicbrainzngs.auth("user", "password")
# Tell musicbrainz what your app is, and how to contact you
# (this step is required, as per the webservice access rules
# at http://wiki.musicbrainz.org/XML_Web_Service/Rate_Limiting )
musicbrainzngs.set_useragent("Example music app", "0.1", "http://example.com/music")
# If you are connecting to a different server
musicbrainzngs.set_hostname("beta.musicbrainz.org")
See the ``query.py`` file for more examples.
More documentation is available at
`Read the Docs `_.
Contribute
**********
If you want to contribute to this repository, please read `the
contribution guidelines
`_ first.
Authors
*******
These bindings were written by `Alastair Porter `_.
Contributions have been made by:
* `Adrian Sampson `_
* `Corey Farwell `_
* `Galen Hazelwood `_
* `Greg Ward `_
* `Ian McEwen `_
* `Jérémie Detrey `_
* `Johannes Dewender `_
* `Michael Marineau `_
* `Patrick Speiser `_
* `Pavan Chander `_
* `Paul Bailey `_
* `Rui Gonçalves `_
* `Ryan Helinski `_
* `Sam Doshi `_
* `Shadab Zafar `_
* `Simon Chopin `_
* `Thomas Vander Stichele `_
* `Wieland Hoffmann `_
License
*******
This library is released under the simplified BSD license except for the file
``musicbrainzngs/compat.py`` which is licensed under the ISC license.
See COPYING for details.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8395243
musicbrainzngs-0.7.1/docs/ 0000755 0000765 0000024 00000000000 00000000000 016431 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1377116296.0
musicbrainzngs-0.7.1/docs/Makefile 0000644 0000765 0000024 00000013057 00000000000 020077 0 ustar 00alastair staff 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = $(shell command -v sphinx-build || command -v sphinx-build2)
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/doctrees
-rm -rf $(BUILDDIR)/html/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/musicbrainzngs.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/musicbrainzngs.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/musicbrainzngs"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/musicbrainzngs"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1460373295.0
musicbrainzngs-0.7.1/docs/api.rst 0000644 0000765 0000024 00000013255 00000000000 017742 0 ustar 00alastair staff 0000000 0000000 API
~~~
.. module:: musicbrainzngs
This is a shallow python binding of the MusicBrainz web service
so you should read
:musicbrainz:`Development/XML Web Service/Version 2`
to understand how that web service works in general.
All requests that fetch data return the data in the form of a :class:`dict`.
Attributes and elements both map to keys in the dict.
List entities are of type :class:`list`.
This part will give an overview of available functions.
Have a look at :doc:`usage` for examples on how to use them.
General
-------
.. autofunction:: auth
.. autofunction:: set_rate_limit
.. autofunction:: set_useragent
.. autofunction:: set_hostname
.. autofunction:: set_caa_hostname
.. autofunction:: set_parser
.. autofunction:: set_format
Getting Data
------------
All of these functions will fetch a MusicBrainz entity or a list of entities
as a dict.
You can specify a list of `includes` to get more data
and you can filter on `release_status` and `release_type`.
See :const:`musicbrainz.VALID_RELEASE_STATUSES`
and :const:`musicbrainz.VALID_RELEASE_TYPES`.
The valid includes are listed for each function.
.. autofunction:: get_area_by_id
.. autofunction:: get_artist_by_id
.. autofunction:: get_event_by_id
.. autofunction:: get_instrument_by_id
.. autofunction:: get_label_by_id
.. autofunction:: get_place_by_id
.. autofunction:: get_recording_by_id
.. autofunction:: get_recordings_by_isrc
.. autofunction:: get_release_group_by_id
.. autofunction:: get_release_by_id
.. autofunction:: get_releases_by_discid
.. autofunction:: get_series_by_id
.. autofunction:: get_work_by_id
.. autofunction:: get_works_by_iswc
.. autofunction:: get_url_by_id
.. autofunction:: get_collections
.. autofunction:: get_releases_in_collection
.. autodata:: musicbrainzngs.musicbrainz.VALID_RELEASE_TYPES
.. autodata:: musicbrainzngs.musicbrainz.VALID_RELEASE_STATUSES
.. _caa_api:
Cover Art
---------
.. autofunction:: get_image_list
.. autofunction:: get_release_group_image_list
.. autofunction:: get_image
.. autofunction:: get_image_front
.. autofunction:: get_release_group_image_front
.. autofunction:: get_image_back
.. _search_api:
Searching
---------
For all of these search functions you can use any of the allowed search fields
as parameter names.
The documentation of what these fields do is on
:musicbrainz:`Development/XML Web Service/Version 2/Search`.
You can also set the `query` parameter to any lucene query you like.
When you use any of the search fields as parameters,
special characters are escaped in the `query`.
By default the elements are concatenated with spaces in between,
so lucene essentially does a fuzzy search.
That search might include results that don't match the complete query,
though these will be ranked lower than the ones that do.
If you want all query elements to match for all results,
you have to set `strict=True`.
By default the web service returns 25 results per request and you can set
a `limit` of up to 100.
You have to use the `offset` parameter to set how many results you have
already seen so the web service doesn't give you the same results again.
.. autofunction:: search_annotations
.. autofunction:: search_areas
.. autofunction:: search_artists
.. autofunction:: search_events
.. autofunction:: search_instruments
.. autofunction:: search_labels
.. autofunction:: search_places
.. autofunction:: search_recordings
.. autofunction:: search_release_groups
.. autofunction:: search_releases
.. autofunction:: search_series
.. autofunction:: search_works
Browsing
--------
You can browse entitities of a certain type linked to one specific entity.
That is you can browse all recordings by an artist, for example.
These functions can be used to to include more than the maximum of 25 linked
entities returned by the functions in `Getting Data`_.
You can set a `limit` as high as 100. The default is still 25.
Similar to the functions in `Searching`_, you have to specify
an `offset` to see the results you haven't seen yet.
You have to provide exactly one MusicBrainz ID to these functions.
.. autofunction:: browse_artists
.. autofunction:: browse_events
.. autofunction:: browse_labels
.. autofunction:: browse_places
.. autofunction:: browse_recordings
.. autofunction:: browse_release_groups
.. autofunction:: browse_releases
.. autofunction:: browse_urls
.. _api_submitting:
Submitting
----------
These are the only functions that write to the MusicBrainz database.
They take one or more dicts with multiple entities as keys,
which take certain values or a list of values.
You have to use :func:`auth` before using any of these functions.
.. autofunction:: submit_barcodes
.. autofunction:: submit_isrcs
.. autofunction:: submit_tags
.. autofunction:: submit_ratings
.. autofunction:: add_releases_to_collection
.. autofunction:: remove_releases_from_collection
Exceptions
----------
These are the main exceptions that are raised by functions in musicbrainzngs.
You might want to catch some of these at an appropriate point in your code.
Some of these might have subclasses that are not listed here.
.. autoclass:: MusicBrainzError
.. autoclass:: UsageError
:show-inheritance:
.. autoclass:: WebServiceError
:show-inheritance:
.. autoclass:: AuthenticationError
:show-inheritance:
.. autoclass:: NetworkError
:show-inheritance:
.. autoclass:: ResponseError
:show-inheritance:
Logging
-------
`musicbrainzngs` logs debug and informational messages using Python's
:mod:`logging` module.
All logging is done in the logger with the name `musicbrainzngs`.
You can enable this output in your application with::
import logging
logging.basicConfig(level=logging.DEBUG)
# optionally restrict musicbrainzngs output to INFO messages
logging.getLogger("musicbrainzngs").setLevel(logging.INFO)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1531332200.0
musicbrainzngs-0.7.1/docs/conf.py 0000644 0000765 0000024 00000021241 00000000000 017730 0 ustar 00alastair staff 0000000 0000000 # -*- coding: utf-8 -*-
#
# musicbrainzngs documentation build configuration file, created by
# sphinx-quickstart2 on Thu Apr 26 15:56:46 2012.
#
# 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.
import sys, os
# 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.
sys.path.insert(0, os.path.abspath('..'))
import musicbrainzngs
from musicbrainzngs.musicbrainz import _version
# -- 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.extlinks',
'sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
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'musicbrainzngs'
copyright = u'2012, Alastair Porter et al'
# 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.
#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.
exclude_patterns = ['_build']
# 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 = []
extlinks = {
'musicbrainz': ('https://musicbrainz.org/doc/%s', ''),
}
intersphinx_mapping = {
'python': ('http://python.readthedocs.io/en/latest/', None),
'python2': ('http://python.readthedocs.io/en/v2.7.2/', None),
'discid': ('http://python-discid.readthedocs.io/en/latest/', None),
}
# -- 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 = 'default'
# force default theme on readthedocs
html_style = "/default.css"
# 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 = {
"footerbgcolor": "#e7e7e7",
"footertextcolor": "#444444",
"sidebarbgcolor": "#ffffff",
"sidebartextcolor": "#000000",
"sidebarlinkcolor": "002bba",
"relbarbgcolor": "#5c5789",
"relbartextcolor": "#000000",
"bgcolor": "#ffffff",
"textcolor": "#000000",
"linkcolor": "#002bba",
"headbgcolor": "#ffba58",
"headtextcolor": "#515151",
"codebgcolor": "#dddddd",
"codetextcolor": "#000000"
}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# 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 (within the static path) to use as 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".
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
html_last_updated_fmt = '%b %d, %Y'
# 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 = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# 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
# Output file base name for HTML help builder.
htmlhelp_basename = 'musicbrainzngsdoc'
# -- 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': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'musicbrainzngs.tex', u'musicbrainzngs Documentation',
u'Alastair Porter et. al', '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 = []
# 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 = [
('index', 'musicbrainzngs', u'musicbrainzngs Documentation',
[u'Alastair Porter et. al'], 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 = [
('index', 'musicbrainzngs', u'musicbrainzngs Documentation',
u'Alastair Porter et. al', 'musicbrainzngs', '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'
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1377116296.0
musicbrainzngs-0.7.1/docs/index.rst 0000644 0000765 0000024 00000001201 00000000000 020264 0 ustar 00alastair staff 0000000 0000000 musicbrainzngs |release|
========================
`musicbrainzngs` implements Python bindings of the `MusicBrainz Web Service`_
(WS/2, NGS).
With this library you can retrieve all kinds of music metadata
from the `MusicBrainz`_ database.
`musicbrainzngs` is released under a simplified BSD style license.
.. _`MusicBrainz`: http://musicbrainz.org
.. _`MusicBrainz Web Service`: http://musicbrainz.org/doc/Development/XML%20Web%20Service/Version%202
Contents
--------
.. toctree::
installation
usage
api
.. currentmodule:: musicbrainzngs.musicbrainz
Indices and tables
------------------
* :ref:`genindex`
* :ref:`search`
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1381268850.0
musicbrainzngs-0.7.1/docs/installation.rst 0000644 0000765 0000024 00000001552 00000000000 021667 0 ustar 00alastair staff 0000000 0000000 Installation
~~~~~~~~~~~~
Package manager
---------------
If you want the latest stable version of musicbrainzngs, the first place to
check is your systems package manager. Being a relatively new library, you
might not be able to find it packaged by your distribution and need to use one
of the alternate installation methods.
PyPI
----
Musicbrainzngs is available on the Python Package Index. This makes installing
it with `pip `_ as easy as::
pip install musicbrainzngs
Git
---
If you want the latest code or even feel like contributing, the code is
available on `GitHub `_.
You can easily clone the code with git::
git clone git://github.com/alastair/python-musicbrainzngs.git
Now you can start hacking on the code or install it system-wide::
python setup.py install
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1346195945.0
musicbrainzngs-0.7.1/docs/make.bat 0000644 0000765 0000024 00000011771 00000000000 020045 0 ustar 00alastair staff 0000000 0000000 @ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build2
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^` where ^ is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\musicbrainzngs.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\musicbrainzngs.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1470679958.0
musicbrainzngs-0.7.1/docs/usage.rst 0000644 0000765 0000024 00000020346 00000000000 020274 0 ustar 00alastair staff 0000000 0000000 Usage
~~~~~
In general you need to set a useragent for your application,
start searches to get to know corresponding MusicBrainz IDs
and then retrieve information about these entities.
The data is returned in form of a :class:`dict`.
If you also want to submit data,
then you must authenticate as a MusicBrainz user.
This part of the documentation will give you usage examples.
For an overview of available functions you can have a look at
the :doc:`api`.
Identification
--------------
To access the MusicBrainz webservice through this library, you `need to
identify your application
`_
by setting the useragent header made in HTTP requests to one that is unique to
your application.
To ease this, the convenience function :meth:`musicbrainzngs.set_useragent` is
provided which automatically sets the useragent based on information about the
application name, version and contact information to the format `recommended by
MusicBrainz
`_.
If a request is made without setting the useragent beforehand, a
:exc:`musicbrainzngs.UsageError` will be raised.
Authentication
--------------
Certain calls to the webservice require user authentication prior to the call
itself. The affected functions state this requirement in their documentation.
The user and password used for authentication are the same as for the
MusicBrainz website itself and can be set with the :meth:`musicbrainzngs.auth`
method. After calling this function, the credentials will be saved and
automaticall used by all functions requiring them.
If a method requiring authentication is called without authenticating, a
:exc:`musicbrainzngs.UsageError` will be raised.
If the credentials provided are wrong and the server returns a status code of
401, a :exc:`musicbrainzngs.AuthenticationError` will be raised.
Getting Data
------------
Regular MusicBrainz Data
^^^^^^^^^^^^^^^^^^^^^^^^
You can get MusicBrainz entities as a :class:`dict`
when retrieving them with some form of identifier.
An example using :func:`musicbrainzngs.get_artist_by_id`::
artist_id = "c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6"
try:
result = musicbrainzngs.get_artist_by_id(artist_id)
except WebServiceError as exc:
print("Something went wrong with the request: %s" % exc)
else:
artist = result["artist"]
print("name:\t\t%s" % artist["name"])
print("sort name:\t%s" % artist["sort-name"])
You can get more information about entities connected to the artist
with adding `includes` and you filter releases and release_groups::
result = musicbrainzngs.get_artist_by_id(artist_id,
includes=["release-groups"], release_type=["album", "ep"])
for release_group in result["artist"]["release-group-list"]:
print("{title} ({type})".format(title=release_group["title"],
type=release_group["type"]))
.. tip:: Compilations are also of primary type "album".
You have to filter these out manually if you don't want them.
.. note:: You can only get at most 25 release groups using this method.
If you want to fetch all release groups you will have to
`browse `_.
Cover Art Data
^^^^^^^^^^^^^^
This library includes a few methods to access data from the `Cover Art Archive
`_ which has a `documented API
`_.
Both :func:`musicbrainzngs.get_image_list` and
:func:`musicbrainzngs.get_release_group_image_list` return the deserialized
cover art listing for a `release
`_
or `release group
`_.
To find out whether a release
has an approved front image, you could use the following example code::
release_id = "46a48e90-819b-4bed-81fa-5ca8aa33fbf3"
data = musicbrainzngs.get_cover_art_list("46a48e90-819b-4bed-81fa-5ca8aa33fbf3")
for image in data["images"]:
if "Front" in image["types"] and image["approved"]:
print "%s is an approved front image!" % image["thumbnails"]["large"]
break
To retrieve an image itself, use :func:`musicbrainzngs.get_image`. A
few convenience functions like :func:`musicbrainzngs.get_image_front`
are provided to allow easy access to often requested images.
.. warning:: There is no upper bound for the size of images uploaded to the
Cover Art Archive and downloading an image will return the binary data in
memory. Consider using the :py:mod:`tempfile` module or similar
techniques to save images to disk as soon as possible.
Searching
---------
When you don't know the MusicBrainz IDs yet, you have to start a search.
Using :func:`musicbrainzngs.search_artists`::
result = musicbrainzngs.search_artists(artist="xx", type="group",
country="GB")
for artist in result['artist-list']:
print(u"{id}: {name}".format(id=artist['id'], name=artist["name"]))
.. tip:: Musicbrainzngs returns unicode strings.
It's up to you to make sure Python (2) doesn't try to convert these
to ascii again. In the example we force a unicode literal for print.
Python 3 works without fixes like these.
You can also use the query without specifying the search fields::
musicbrainzngs.search_release_groups("the clash london calling")
The query and the search fields can also be used at the same time.
Browsing
--------
When you want to fetch a list of entities greater than 25,
you have to use one of the browse functions.
Not only can you specify a `limit` as high as 100,
but you can also specify an `offset` to get the complete list
in multiple requests.
An example would be using :func:`musicbrainzngs.browse_release_groups`
to get all releases for a label::
label = "71247f6b-fd24-4a56-89a2-23512f006f0c"
limit = 100
offset = 0
releases = []
page = 1
print("fetching page number %d.." % page)
result = musicbrainzngs.browse_releases(label=label, includes=["labels"],
release_type=["album"], limit=limit)
page_releases = result['release-list']
releases += page_releases
# release-count is only available starting with musicbrainzngs 0.5
if "release-count" in result:
count = result['release-count']
print("")
while len(page_releases) >= limit:
offset += limit
page += 1
print("fetching page number %d.." % page)
result = musicbrainzngs.browse_releases(label=label, includes=["labels"],
release_type=["album"], limit=limit, offset=offset)
page_releases = result['release-list']
releases += page_releases
print("")
for release in releases:
for label_info in release['label-info-list']:
catnum = label_info.get('catalog-number')
if label_info['label']['id'] == label and catnum:
print("{catnum:>17}: {date:10} {title}".format(catnum=catnum,
date=release['date'], title=release['title']))
print("\n%d releases on %d pages" % (len(releases), page))
.. tip:: You should always try to filter in the query, when possible,
rather than fetching everything and filtering afterwards.
This will make your application faster
since web service requests are throttled.
In the example we filter by `release_type`.
Submitting
----------
You can also submit data using musicbrainzngs.
Please use :func:`musicbrainzngs.set_hostname` to set the host to
test.musicbrainz.org when testing the submission part of your application.
`Authentication`_ is necessary to submit any data to MusicBrainz.
An example using :func:`musicbrainzngs.submit_barcodes` looks like this::
musicbrainzngs.set_hostname("test.musicbrainz.org")
musicbrainzngs.auth("test", "mb")
barcodes = {
"174a5513-73d1-3c9d-a316-3c1c179e35f8": "5099749534728",
"838952af-600d-3f51-84d5-941d15880400": "602517737280"
}
musicbrainzngs.submit_barcodes(barcodes)
See :ref:`api_submitting` in the API for other possibilites.
More Examples
-------------
You can find some examples for using `musicbrainzngs` in the
`examples directory `_.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8427253
musicbrainzngs-0.7.1/examples/ 0000755 0000765 0000024 00000000000 00000000000 017317 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1381268792.0
musicbrainzngs-0.7.1/examples/browse.py 0000755 0000765 0000024 00000006313 00000000000 021200 0 ustar 00alastair staff 0000000 0000000 #!/usr/bin/env python
"""View and modify your MusicBrainz collections.
To show a list of your collections:
$ ./collection.py USERNAME
Password for USERNAME:
All collections for this user:
My Collection by USERNAME (4137a646-a104-4031-b549-da4e1f36a463)
To show the releases in a collection:
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
Password for USERNAME:
Releases in My Collection:
None Shall Pass (b0885908-cbe2-4e51-95d8-c4f3b9721ad6)
...
To add a release to a collection or remove one:
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
--add 0d432d8b-8865-4ae9-8479-3a197620a37b
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
--remove 0d432d8b-8865-4ae9-8479-3a197620a37b
"""
from __future__ import print_function
from __future__ import unicode_literals
import musicbrainzngs
import getpass
from optparse import OptionParser
musicbrainzngs.set_useragent(
"python-musicbrainzngs-example",
"0.1",
"https://github.com/alastair/python-musicbrainzngs/",
)
def show_collections():
"""Fetch and display the current user's collections.
"""
result = musicbrainzngs.get_collections()
print('All collections for this user:')
for collection in result['collection-list']:
print('{name} by {editor} ({mbid})'.format(
name=collection['name'], editor=collection['editor'],
mbid=collection['id']
))
def show_collection(collection_id):
"""Show the list of releases in a given collection.
"""
result = musicbrainzngs.get_releases_in_collection(collection_id)
collection = result['collection']
print('Releases in {}:'.format(collection['name']))
for release in collection['release-list']:
print('{title} ({mbid})'.format(
title=release['title'], mbid=release['id']
))
if __name__ == '__main__':
parser = OptionParser(usage="%prog [options] USERNAME [COLLECTION-ID]")
parser.add_option('-a', '--add', metavar="RELEASE-ID",
help="add a release to the collection")
parser.add_option('-r', '--remove', metavar="RELEASE-ID",
help="remove a release from the collection")
options, args = parser.parse_args()
if not args:
parser.error('no username specified')
username = args.pop(0)
# Input the password.
password = getpass.getpass('Password for {}: '.format(username))
# Call musicbrainzngs.auth() before making any API calls that
# require authentication.
musicbrainzngs.auth(username, password)
if args:
# Actions for a specific collction.
collection_id = args[0]
if options.add:
# Add a release to the collection.
musicbrainzngs.add_releases_to_collection(
collection_id, [options.add]
)
elif options.remove:
# Remove a release from the collection.
musicbrainzngs.remove_releases_from_collection(
collection_id, [options.remove]
)
else:
# Print out the collection's contents.
show_collection(collection_id)
else:
# Show all collections.
show_collections()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1460373295.0
musicbrainzngs-0.7.1/examples/collection.py 0000755 0000765 0000024 00000015777 00000000000 022050 0 ustar 00alastair staff 0000000 0000000 #!/usr/bin/env python
"""View and modify your MusicBrainz collections.
To show a list of your collections:
$ ./collection.py USERNAME
Password for USERNAME:
All collections for this user:
My Collection by USERNAME (4137a646-a104-4031-b549-da4e1f36a463)
To show the releases in a collection:
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
Password for USERNAME:
Releases in My Collection:
None Shall Pass (b0885908-cbe2-4e51-95d8-c4f3b9721ad6)
...
To add a release to a collection or remove one:
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
--add 0d432d8b-8865-4ae9-8479-3a197620a37b
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
--remove 0d432d8b-8865-4ae9-8479-3a197620a37b
"""
from __future__ import print_function
from __future__ import unicode_literals
import musicbrainzngs
import getpass
from optparse import OptionParser
import sys
try:
user_input = raw_input
except NameError:
user_input = input
musicbrainzngs.set_useragent(
"python-musicbrainzngs-example",
"0.1",
"https://github.com/alastair/python-musicbrainzngs/",
)
def show_collections():
"""Fetch and display the current user's collections.
"""
result = musicbrainzngs.get_collections()
print('All collections for this user:')
for collection in result['collection-list']:
# entity-type only available starting with musicbrainzngs 0.6
if "entity-type" in collection:
print('"{name}" by {editor} ({cat}, {count} {entity}s)\n\t{mbid}'
.format(
name=collection['name'], editor=collection['editor'],
cat=collection['type'], entity=collection['entity-type'],
count=collection[collection['entity-type']+'-count'],
mbid=collection['id']
))
else:
print('"{name}" by {editor}\n\t{mbid}'.format(
name=collection['name'], editor=collection['editor'],
mbid=collection['id']
))
def show_collection(collection_id, ctype):
"""Show a given collection.
"""
if ctype == "release":
result = musicbrainzngs.get_releases_in_collection(
collection_id, limit=0)
elif ctype == "artist":
result = musicbrainzngs.get_artists_in_collection(
collection_id, limit=0)
elif ctype == "event":
result = musicbrainzngs.get_events_in_collection(
collection_id, limit=0)
elif ctype == "place":
result = musicbrainzngs.get_places_in_collection(
collection_id, limit=0)
elif ctype == "recording":
result = musicbrainzngs.get_recordings_in_collection(
collection_id, limit=0)
elif ctype == "work":
result = musicbrainzngs.get_works_in_collection(
collection_id, limit=0)
collection = result['collection']
# entity-type only available starting with musicbrainzngs 0.6
if "entity-type" in collection:
print('{mbid}\n"{name}" by {editor} ({cat}, {entity})'.format(
name=collection['name'], editor=collection['editor'],
cat=collection['type'], entity=collection['entity-type'],
mbid=collection['id']
))
else:
print('{mbid}\n"{name}" by {editor}'.format(
name=collection['name'], editor=collection['editor'],
mbid=collection['id']
))
print('')
# release count is only available starting with musicbrainzngs 0.5
if "release-count" in collection:
print('{} releases'.format(collection['release-count']))
if "artist-count" in collection:
print('{} artists'.format(collection['artist-count']))
if "event-count" in collection:
print('{} events'.format(collection['release-count']))
if "place-count" in collection:
print('{} places'.format(collection['place-count']))
if "recording-count" in collection:
print('{} recordings'.format(collection['recording-count']))
if "work-count" in collection:
print('{} works'.format(collection['work-count']))
print('')
if "release-list" in collection:
show_releases(collection)
else:
pass # TODO
def show_releases(collection):
result = musicbrainzngs.get_releases_in_collection(collection_id, limit=25)
release_list = result['collection']['release-list']
print('Releases:')
releases_fetched = 0
while len(release_list) > 0:
print("")
releases_fetched += len(release_list)
for release in release_list:
print('{title} ({mbid})'.format(
title=release['title'], mbid=release['id']
))
if user_input("Would you like to display more releases? [y/N] ") != "y":
break;
# fetch next batch of releases
result = musicbrainzngs.get_releases_in_collection(collection_id,
limit=25, offset=releases_fetched)
collection = result['collection']
release_list = collection['release-list']
print("")
print("Number of fetched releases: %d" % releases_fetched)
if __name__ == '__main__':
parser = OptionParser(usage="%prog [options] USERNAME [COLLECTION-ID]")
parser.add_option('-a', '--add', metavar="RELEASE-ID",
help="add a release to the collection")
parser.add_option('-r', '--remove', metavar="RELEASE-ID",
help="remove a release from the collection")
parser.add_option('-t', '--type', metavar="TYPE", default="release",
help="type of the collection (default: release)")
options, args = parser.parse_args()
if not args:
parser.error('no username specified')
username = args.pop(0)
# Input the password.
password = getpass.getpass('Password for {}: '.format(username))
# Call musicbrainzngs.auth() before making any API calls that
# require authentication.
musicbrainzngs.auth(username, password)
if args:
# Actions for a specific collction.
collection_id = args[0]
if options.add:
if option.type == "release":
musicbrainzngs.add_releases_to_collection(
collection_id, [options.add]
)
else:
sys.exit("only release collections can be modified ATM")
elif options.remove:
if option.type == "release":
musicbrainzngs.remove_releases_from_collection(
collection_id, [options.remove]
)
else:
sys.exit("only release collections can be modified ATM")
else:
# Print out the collection's contents.
print("")
show_collection(collection_id, options.type)
else:
# Show all collections.
print("")
show_collections()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1439637069.0
musicbrainzngs-0.7.1/examples/find_disc.py 0000755 0000765 0000024 00000005107 00000000000 021621 0 ustar 00alastair staff 0000000 0000000 #!/usr/bin/env python
"""A script that looks for a release in the MusicBrainz database by disc ID
$ ./find_disc.py kKOqMEuRDSeW_.K49SUEJXensLY-
disc:
Sectors: 295099
London Calling
MusicBrainz ID: 174a5513-73d1-3c9d-a316-3c1c179e35f8
EAN/UPC: 5099749534728
cat#: 495347 2
...
"""
from __future__ import unicode_literals
import musicbrainzngs
import sys
musicbrainzngs.set_useragent(
"python-musicbrainzngs-example",
"0.1",
"https://github.com/alastair/python-musicbrainzngs/",
)
def show_release_details(rel):
"""Print some details about a release dictionary to stdout.
"""
print("\t{}".format(rel['title']))
print("\t\tMusicBrainz ID: {}".format(rel['id']))
if rel.get('barcode'):
print("\t\tEAN/UPC: {}".format(rel['barcode']))
for info in rel['label-info-list']:
if info.get('catalog-number'):
print("\t\tcat#: {}".format(info['catalog-number']))
def show_offsets(offset_list):
offsets = None
for offset in offset_list:
if offsets == None:
offsets = str(offset)
else:
offsets += " " + str(offset)
print("\toffsets: {}".format(offsets))
if __name__ == '__main__':
args = sys.argv[1:]
if len(args) != 1:
sys.exit("usage: {} DISC_ID".format(sys.argv[0]))
discid = args[0]
try:
# the "labels" include enables the cat#s we display
result = musicbrainzngs.get_releases_by_discid(discid,
includes=["labels"])
except musicbrainzngs.ResponseError as err:
if err.cause.code == 404:
sys.exit("disc not found")
else:
sys.exit("received bad response from the MB server")
# The result can either be a "disc" or a "cdstub"
if result.get('disc'):
print("disc:")
print("\tSectors: {}".format(result['disc']['sectors']))
# offset-list only available starting with musicbrainzngs 0.6
if "offset-list" in result['disc']:
show_offsets(result['disc']['offset-list'])
print("\tTracks: {}".format(result['disc']['offset-count']))
for release in result['disc']['release-list']:
show_release_details(release)
print("")
elif result.get('cdstub'):
print("cdstub:")
print("\tArtist: {}".format(result['cdstub']['artist']))
print("\tTitle: {}".format(result['cdstub']['title']))
if result['cdstub'].get('barcode'):
print("\tBarcode: {}".format(result['cdstub']['barcode']))
else:
sys.exit("no valid results")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1381268813.0
musicbrainzngs-0.7.1/examples/isrcsubmit.py 0000755 0000765 0000024 00000006313 00000000000 022063 0 ustar 00alastair staff 0000000 0000000 #!/usr/bin/env python
"""View and modify your MusicBrainz collections.
To show a list of your collections:
$ ./collection.py USERNAME
Password for USERNAME:
All collections for this user:
My Collection by USERNAME (4137a646-a104-4031-b549-da4e1f36a463)
To show the releases in a collection:
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
Password for USERNAME:
Releases in My Collection:
None Shall Pass (b0885908-cbe2-4e51-95d8-c4f3b9721ad6)
...
To add a release to a collection or remove one:
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
--add 0d432d8b-8865-4ae9-8479-3a197620a37b
$ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463
--remove 0d432d8b-8865-4ae9-8479-3a197620a37b
"""
from __future__ import print_function
from __future__ import unicode_literals
import musicbrainzngs
import getpass
from optparse import OptionParser
musicbrainzngs.set_useragent(
"python-musicbrainzngs-example",
"0.1",
"https://github.com/alastair/python-musicbrainzngs/",
)
def show_collections():
"""Fetch and display the current user's collections.
"""
result = musicbrainzngs.get_collections()
print('All collections for this user:')
for collection in result['collection-list']:
print('{name} by {editor} ({mbid})'.format(
name=collection['name'], editor=collection['editor'],
mbid=collection['id']
))
def show_collection(collection_id):
"""Show the list of releases in a given collection.
"""
result = musicbrainzngs.get_releases_in_collection(collection_id)
collection = result['collection']
print('Releases in {}:'.format(collection['name']))
for release in collection['release-list']:
print('{title} ({mbid})'.format(
title=release['title'], mbid=release['id']
))
if __name__ == '__main__':
parser = OptionParser(usage="%prog [options] USERNAME [COLLECTION-ID]")
parser.add_option('-a', '--add', metavar="RELEASE-ID",
help="add a release to the collection")
parser.add_option('-r', '--remove', metavar="RELEASE-ID",
help="remove a release from the collection")
options, args = parser.parse_args()
if not args:
parser.error('no username specified')
username = args.pop(0)
# Input the password.
password = getpass.getpass('Password for {}: '.format(username))
# Call musicbrainzngs.auth() before making any API calls that
# require authentication.
musicbrainzngs.auth(username, password)
if args:
# Actions for a specific collction.
collection_id = args[0]
if options.add:
# Add a release to the collection.
musicbrainzngs.add_releases_to_collection(
collection_id, [options.add]
)
elif options.remove:
# Remove a release from the collection.
musicbrainzngs.remove_releases_from_collection(
collection_id, [options.remove]
)
else:
# Print out the collection's contents.
show_collection(collection_id)
else:
# Show all collections.
show_collections()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1381268818.0
musicbrainzngs-0.7.1/examples/releasesearch.py 0000755 0000765 0000024 00000003650 00000000000 022506 0 ustar 00alastair staff 0000000 0000000 #!/usr/bin/env python
"""A simple script that searches for a release in the MusicBrainz
database and prints out a few details about the first 5 matching release.
$ ./releasesearch.py "the beatles" revolver
Revolver, by The Beatles
Released 1966-08-08 (Official)
MusicBrainz ID: b4b04cbf-118a-3944-9545-38a0a88ff1a2
"""
from __future__ import print_function
from __future__ import unicode_literals
import musicbrainzngs
import sys
musicbrainzngs.set_useragent(
"python-musicbrainzngs-example",
"0.1",
"https://github.com/alastair/python-musicbrainzngs/",
)
def show_release_details(rel):
"""Print some details about a release dictionary to stdout.
"""
# "artist-credit-phrase" is a flat string of the credited artists
# joined with " + " or whatever is given by the server.
# You can also work with the "artist-credit" list manually.
print("{}, by {}".format(rel['title'], rel["artist-credit-phrase"]))
if 'date' in rel:
print("Released {} ({})".format(rel['date'], rel['status']))
print("MusicBrainz ID: {}".format(rel['id']))
if __name__ == '__main__':
args = sys.argv[1:]
if len(args) != 2:
sys.exit("usage: {} ARTIST ALBUM".format(sys.argv[0]))
artist, album = args
# Keyword arguments to the "search_*" functions limit keywords to
# specific fields. The "limit" keyword argument is special (like as
# "offset", not shown here) and specifies the number of results to
# return.
result = musicbrainzngs.search_releases(artist=artist, release=album,
limit=5)
# On success, result is a dictionary with a single key:
# "release-list", which is a list of dictionaries.
if not result['release-list']:
sys.exit("no release found")
for (idx, release) in enumerate(result['release-list']):
print("match #{}:".format(idx+1))
show_release_details(release)
print()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8460085
musicbrainzngs-0.7.1/musicbrainzngs/ 0000755 0000765 0000024 00000000000 00000000000 020537 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1405848834.0
musicbrainzngs-0.7.1/musicbrainzngs/__init__.py 0000644 0000765 0000024 00000000112 00000000000 022642 0 ustar 00alastair staff 0000000 0000000 from musicbrainzngs.musicbrainz import *
from musicbrainzngs.caa import *
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1568138663.0
musicbrainzngs-0.7.1/musicbrainzngs/caa.py 0000644 0000765 0000024 00000014651 00000000000 021644 0 ustar 00alastair staff 0000000 0000000 # This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Wieland Hoffmann, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
__all__ = [
'set_caa_hostname', 'get_image_list', 'get_release_group_image_list',
'get_release_group_image_front', 'get_image_front', 'get_image_back',
'get_image'
]
import json
from musicbrainzngs import compat
from musicbrainzngs import musicbrainz
from musicbrainzngs.util import _unicode
hostname = "coverartarchive.org"
https = True
def set_caa_hostname(new_hostname, use_https=False):
"""Set the base hostname for Cover Art Archive requests.
Defaults to 'coverartarchive.org', accessing over https.
For backwards compatibility, `use_https` is False by default.
:param str new_hostname: The hostname (and port) of the CAA server to connect to
:param bool use_https: `True` if the host should be accessed using https. Default is `False`
"""
global hostname
global https
hostname = new_hostname
https = use_https
def _caa_request(mbid, imageid=None, size=None, entitytype="release"):
""" Make a CAA request.
:param imageid: ``front``, ``back`` or a number from the listing obtained
with :meth:`get_image_list`.
:type imageid: str
:param size: "250", "500", "1200"
:type size: str or None
:param entitytype: ``release`` or ``release-group``
:type entitytype: str
"""
# Construct the full URL for the request, including hostname and
# query string.
path = [entitytype, mbid]
if imageid and size:
path.append("%s-%s" % (imageid, size))
elif imageid:
path.append(imageid)
url = compat.urlunparse((
'https' if https else 'http',
hostname,
'/%s' % '/'.join(path),
'',
'',
''
))
musicbrainz._log.debug("GET request for %s" % (url, ))
# Set up HTTP request handler and URL opener.
httpHandler = compat.HTTPHandler(debuglevel=0)
handlers = [httpHandler]
opener = compat.build_opener(*handlers)
# Make request.
req = musicbrainz._MusicbrainzHttpRequest("GET", url, None)
# Useragent isn't needed for CAA, but we'll add it if it exists
if musicbrainz._useragent != "":
req.add_header('User-Agent', musicbrainz._useragent)
musicbrainz._log.debug("requesting with UA %s" % musicbrainz._useragent)
resp = musicbrainz._safe_read(opener, req, None)
# TODO: The content type declared by the CAA for JSON files is
# 'applicaiton/octet-stream'. This is not useful to detect whether the
# content is JSON, so default to decoding JSON if no imageid was supplied.
# http://tickets.musicbrainz.org/browse/CAA-75
if imageid:
# If we asked for an image, return the image
return resp
else:
# Otherwise it's json
data = _unicode(resp)
return json.loads(data)
def get_image_list(releaseid):
"""Get the list of cover art associated with a release.
The return value is the deserialized response of the `JSON listing
`_
returned by the Cover Art Archive API.
If an error occurs then a :class:`~musicbrainzngs.ResponseError` will
be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
"""
return _caa_request(releaseid)
def get_release_group_image_list(releasegroupid):
"""Get the list of cover art associated with a release group.
The return value is the deserialized response of the `JSON listing
`_
returned by the Cover Art Archive API.
If an error occurs then a :class:`~musicbrainzngs.ResponseError` will
be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
"""
return _caa_request(releasegroupid, entitytype="release-group")
def get_release_group_image_front(releasegroupid, size=None):
"""Download the front cover art for a release group.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releasegroupid, "front", size=size,
entitytype="release-group")
def get_image_front(releaseid, size=None):
"""Download the front cover art for a release.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releaseid, "front", size=size)
def get_image_back(releaseid, size=None):
"""Download the back cover art for a release.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releaseid, "back", size=size)
def get_image(mbid, coverid, size=None, entitytype="release"):
"""Download cover art for a release. The coverart file to download
is specified by the `coverid` argument.
If `size` is not specified, download the largest copy present, which can be
very large.
If an error occurs then a :class:`~musicbrainzngs.ResponseError`
will be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID or `coverid` is invalid
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
:param coverid: ``front``, ``back`` or a number from the listing obtained with
:meth:`get_image_list`
:type coverid: int or str
:param size: "250", "500", "1200" or None. If it is None, the largest
available picture will be downloaded. If the image originally
uploaded to the Cover Art Archive was smaller than the
requested size, only the original image will be returned.
:type size: str or None
:param entitytype: The type of entity for which to download the cover art.
This is either ``release`` or ``release-group``.
:type entitytype: str
:return: The binary image data
:type: str
"""
if isinstance(coverid, int):
coverid = "%d" % (coverid, )
if isinstance(size, int):
size = "%d" % (size, )
return _caa_request(mbid, coverid, size=size, entitytype=entitytype)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1553537326.0
musicbrainzngs-0.7.1/musicbrainzngs/compat.py 0000644 0000765 0000024 00000003267 00000000000 022404 0 ustar 00alastair staff 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright (c) 2012 Kenneth Reitz.
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
pythoncompat
"""
import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
# ---------
# Specifics
# ---------
if is_py2:
from StringIO import StringIO
from urllib2 import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\
HTTPHandler, build_opener, HTTPError, URLError
from httplib import BadStatusLine, HTTPException
from urlparse import urlunparse
from urllib import urlencode, quote_plus
bytes = str
unicode = unicode
basestring = basestring
elif is_py3:
from io import StringIO
from urllib.request import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\
HTTPHandler, build_opener
from urllib.error import HTTPError, URLError
from http.client import HTTPException, BadStatusLine
from urllib.parse import urlunparse, urlencode, quote_plus
unicode = str
bytes = bytes
basestring = (str,bytes)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1536178072.0
musicbrainzngs-0.7.1/musicbrainzngs/mbxml.py 0000644 0000765 0000024 00000066767 00000000000 022256 0 ustar 00alastair staff 0000000 0000000 # This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Adrian Sampson, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
import re
import xml.etree.ElementTree as ET
import logging
from . import util
def fixtag(tag, namespaces):
# given a decorated tag (of the form {uri}tag), return prefixed
# tag and namespace declaration, if any
if isinstance(tag, ET.QName):
tag = tag.text
namespace_uri, tag = tag[1:].split("}", 1)
prefix = namespaces.get(namespace_uri)
if prefix is None:
prefix = "ns%d" % len(namespaces)
namespaces[namespace_uri] = prefix
if prefix == "xml":
xmlns = None
else:
xmlns = ("xmlns:%s" % prefix, namespace_uri)
else:
xmlns = None
return "%s:%s" % (prefix, tag), xmlns
NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2",
"http://musicbrainz.org/ns/ext#-2.0": "ext"}
_log = logging.getLogger("musicbrainzngs")
def get_error_message(error):
""" Given an error XML message from the webservice containing
xy, return a list
of [x, y]"""
try:
tree = util.bytes_to_elementtree(error)
root = tree.getroot()
errors = []
if root.tag == "error":
for ch in root:
if ch.tag == "text":
errors.append(ch.text)
return errors
except ET.ParseError:
return None
def make_artist_credit(artists):
names = []
for artist in artists:
if isinstance(artist, dict):
if "name" in artist:
names.append(artist.get("name", ""))
else:
names.append(artist.get("artist", {}).get("name", ""))
else:
names.append(artist)
return "".join(names)
def parse_elements(valid_els, inner_els, element):
""" Extract single level subelements from an element.
For example, given the element:
Text
and a list valid_els that contains "subelement",
return a dict {'subelement': 'Text'}
Delegate the parsing of multi-level subelements to another function.
For example, given the element:
FooBar
and a dictionary {'subelement': parse_subelement},
call parse_subelement() and
return a dict {'subelement': }
if parse_subelement returns a tuple of the form
(True, {'subelement-key': })
then merge the second element of the tuple into the
result (which may have a key other than 'subelement' or
more than 1 key)
"""
result = {}
for sub in element:
t = fixtag(sub.tag, NS_MAP)[0]
if ":" in t:
t = t.split(":")[1]
if t in valid_els:
result[t] = sub.text or ""
elif t in inner_els.keys():
inner_result = inner_els[t](sub)
if isinstance(inner_result, tuple) and inner_result[0]:
result.update(inner_result[1])
else:
result[t] = inner_result
# add counts for lists when available
m = re.match(r'([a-z0-9-]+)-list', t)
if m and "count" in sub.attrib:
result["%s-count" % m.group(1)] = int(sub.attrib["count"])
else:
_log.info("in <%s>, uncaught <%s>",
fixtag(element.tag, NS_MAP)[0], t)
return result
def parse_attributes(attributes, element):
""" Extract attributes from an element.
For example, given the element:
and a list attributes that contains "type",
return a dict {'type': 'Group'}
"""
result = {}
for attr in element.attrib:
if "{" in attr:
a = fixtag(attr, NS_MAP)[0]
else:
a = attr
if a in attributes:
result[a] = element.attrib[attr]
else:
_log.info("in <%s>, uncaught attribute %s", fixtag(element.tag, NS_MAP)[0], attr)
return result
def parse_message(message):
tree = util.bytes_to_elementtree(message)
root = tree.getroot()
result = {}
valid_elements = {"area": parse_area,
"artist": parse_artist,
"instrument": parse_instrument,
"label": parse_label,
"place": parse_place,
"event": parse_event,
"release": parse_release,
"release-group": parse_release_group,
"series": parse_series,
"recording": parse_recording,
"work": parse_work,
"url": parse_url,
"disc": parse_disc,
"cdstub": parse_cdstub,
"isrc": parse_isrc,
"annotation-list": parse_annotation_list,
"area-list": parse_area_list,
"artist-list": parse_artist_list,
"label-list": parse_label_list,
"place-list": parse_place_list,
"event-list": parse_event_list,
"instrument-list": parse_instrument_list,
"release-list": parse_release_list,
"release-group-list": parse_release_group_list,
"series-list": parse_series_list,
"recording-list": parse_recording_list,
"work-list": parse_work_list,
"url-list": parse_url_list,
"collection-list": parse_collection_list,
"collection": parse_collection,
"message": parse_response_message
}
result.update(parse_elements([], valid_elements, root))
return result
def parse_response_message(message):
return parse_elements(["text"], {}, message)
def parse_collection_list(cl):
return [parse_collection(c) for c in cl]
def parse_collection(collection):
result = {}
attribs = ["id", "type", "entity-type"]
elements = ["name", "editor"]
inner_els = {"release-list": parse_release_list,
"artist-list": parse_artist_list,
"event-list": parse_event_list,
"place-list": parse_place_list,
"recording-list": parse_recording_list,
"work-list": parse_work_list}
result.update(parse_attributes(attribs, collection))
result.update(parse_elements(elements, inner_els, collection))
return result
def parse_annotation_list(al):
return [parse_annotation(a) for a in al]
def parse_annotation(annotation):
result = {}
attribs = ["type", "ext:score"]
elements = ["entity", "name", "text"]
result.update(parse_attributes(attribs, annotation))
result.update(parse_elements(elements, {}, annotation))
return result
def parse_lifespan(lifespan):
parts = parse_elements(["begin", "end", "ended"], {}, lifespan)
return parts
def parse_area_list(al):
return [parse_area(a) for a in al]
def parse_area(area):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "disambiguation"]
inner_els = {"life-span": parse_lifespan,
"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation,
"iso-3166-1-code-list": parse_element_list,
"iso-3166-2-code-list": parse_element_list,
"iso-3166-3-code-list": parse_element_list}
result.update(parse_attributes(attribs, area))
result.update(parse_elements(elements, inner_els, area))
return result
def parse_artist_list(al):
return [parse_artist(a) for a in al]
def parse_artist(artist):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "country", "user-rating",
"disambiguation", "gender", "ipi"]
inner_els = {"area": parse_area,
"begin-area": parse_area,
"end-area": parse_area,
"life-span": parse_lifespan,
"recording-list": parse_recording_list,
"relation-list": parse_relation_list,
"release-list": parse_release_list,
"release-group-list": parse_release_group_list,
"work-list": parse_work_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"ipi-list": parse_element_list,
"isni-list": parse_element_list,
"alias-list": parse_alias_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, artist))
result.update(parse_elements(elements, inner_els, artist))
return result
def parse_coordinates(c):
return parse_elements(['latitude', 'longitude'], {}, c)
def parse_place_list(pl):
return [parse_place(p) for p in pl]
def parse_place(place):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "address",
"ipi", "disambiguation"]
inner_els = {"area": parse_area,
"coordinates": parse_coordinates,
"life-span": parse_lifespan,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, place))
result.update(parse_elements(elements, inner_els, place))
return result
def parse_event_list(el):
return [parse_event(e) for e in el]
def parse_event(event):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "time", "setlist", "cancelled", "disambiguation", "user-rating"]
inner_els = {"life-span": parse_lifespan,
"relation-list": parse_relation_list,
"alias-list": parse_alias_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating}
result.update(parse_attributes(attribs, event))
result.update(parse_elements(elements, inner_els, event))
return result
def parse_instrument(instrument):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "description", "disambiguation"]
inner_els = {"relation-list": parse_relation_list,
"tag-list": parse_tag_list,
"alias-list": parse_alias_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, instrument))
result.update(parse_elements(elements, inner_els, instrument))
return result
def parse_label_list(ll):
return [parse_label(l) for l in ll]
def parse_label(label):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "country", "label-code", "user-rating",
"ipi", "disambiguation"]
inner_els = {"area": parse_area,
"life-span": parse_lifespan,
"release-list": parse_release_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"ipi-list": parse_element_list,
"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, label))
result.update(parse_elements(elements, inner_els, label))
return result
def parse_relation_target(tgt):
attributes = parse_attributes(['id'], tgt)
if 'id' in attributes:
return (True, {'target-id': attributes['id']})
else:
return (True, {'target-id': tgt.text})
def parse_relation_list(rl):
attribs = ["target-type"]
ttype = parse_attributes(attribs, rl)
key = "%s-relation-list" % ttype["target-type"]
return (True, {key: [parse_relation(r) for r in rl]})
def parse_relation(relation):
result = {}
attribs = ["type", "type-id"]
elements = ["target", "direction", "begin", "end", "ended", "ordering-key"]
inner_els = {"area": parse_area,
"artist": parse_artist,
"instrument": parse_instrument,
"label": parse_label,
"place": parse_place,
"event": parse_event,
"recording": parse_recording,
"release": parse_release,
"release-group": parse_release_group,
"series": parse_series,
"attribute-list": parse_element_list,
"work": parse_work,
"target": parse_relation_target
}
result.update(parse_attributes(attribs, relation))
result.update(parse_elements(elements, inner_els, relation))
# We parse attribute-list again to get attributes that have both
# text and attribute values
result.update(parse_elements(['target-credit'], {"attribute-list": parse_relation_attribute_list}, relation))
return result
def parse_relation_attribute_list(attributelist):
ret = []
for attribute in attributelist:
ret.append(parse_relation_attribute_element(attribute))
return (True, {"attributes": ret})
def parse_relation_attribute_element(element):
# Parses an attribute into a dictionary containing an element
# {"attribute": } and also an additional element
# containing any xml attributes.
# e.g number
# -> {"attribute": "number", "value": "BuxWV 1"}
result = {}
for attr in element.attrib:
if "{" in attr:
a = fixtag(attr, NS_MAP)[0]
else:
a = attr
result[a] = element.attrib[attr]
result["attribute"] = element.text
return result
def parse_release(release):
result = {}
attribs = ["id", "ext:score"]
elements = ["title", "status", "disambiguation", "quality", "country",
"barcode", "date", "packaging", "asin"]
inner_els = {"text-representation": parse_text_representation,
"artist-credit": parse_artist_credit,
"label-info-list": parse_label_info_list,
"medium-list": parse_medium_list,
"release-group": parse_release_group,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation,
"cover-art-archive": parse_caa,
"release-event-list": parse_release_event_list}
result.update(parse_attributes(attribs, release))
result.update(parse_elements(elements, inner_els, release))
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(
result["artist-credit"])
return result
def parse_medium_list(ml):
"""medium-list results from search have an additional
element containing the number of tracks
over all mediums. Optionally add this"""
medium_list = []
track_count = None
for m in ml:
tag = fixtag(m.tag, NS_MAP)[0]
if tag == "ws2:medium":
medium_list.append(parse_medium(m))
elif tag == "ws2:track-count":
track_count = int(m.text)
ret = {"medium-list": medium_list}
if track_count is not None:
ret["medium-track-count"] = track_count
return (True, ret)
def parse_release_event_list(rel):
return [parse_release_event(re) for re in rel]
def parse_release_event(event):
result = {}
elements = ["date"]
inner_els = {"area": parse_area}
result.update(parse_elements(elements, inner_els, event))
return result
def parse_medium(medium):
result = {}
elements = ["position", "format", "title"]
inner_els = {"disc-list": parse_disc_list,
"pregap": parse_track,
"track-list": parse_track_list,
"data-track-list": parse_track_list}
result.update(parse_elements(elements, inner_els, medium))
return result
def parse_disc_list(dl):
return [parse_disc(d) for d in dl]
def parse_text_representation(textr):
return parse_elements(["language", "script"], {}, textr)
def parse_release_group(rg):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["title", "user-rating", "first-release-date", "primary-type",
"disambiguation"]
inner_els = {"artist-credit": parse_artist_credit,
"release-list": parse_release_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"secondary-type-list": parse_element_list,
"relation-list": parse_relation_list,
"rating": parse_rating,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, rg))
result.update(parse_elements(elements, inner_els, rg))
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(result["artist-credit"])
return result
def parse_recording(recording):
result = {}
attribs = ["id", "ext:score"]
elements = ["title", "length", "user-rating", "disambiguation", "video"]
inner_els = {"artist-credit": parse_artist_credit,
"release-list": parse_release_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"isrc-list": parse_external_id_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, recording))
result.update(parse_elements(elements, inner_els, recording))
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(result["artist-credit"])
return result
def parse_series_list(sl):
return [parse_series(s) for s in sl]
def parse_series(series):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "disambiguation"]
inner_els = {"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, series))
result.update(parse_elements(elements, inner_els, series))
return result
def parse_external_id_list(pl):
return [parse_attributes(["id"], p)["id"] for p in pl]
def parse_element_list(el):
return [e.text for e in el]
def parse_work_list(wl):
return [parse_work(w) for w in wl]
def parse_work(work):
result = {}
attribs = ["id", "ext:score", "type"]
elements = ["title", "user-rating", "language", "iswc", "disambiguation"]
inner_els = {"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"alias-list": parse_alias_list,
"iswc-list": parse_element_list,
"relation-list": parse_relation_list,
"annotation": parse_response_message,
"attribute-list": parse_work_attribute_list
}
result.update(parse_attributes(attribs, work))
result.update(parse_elements(elements, inner_els, work))
return result
def parse_work_attribute_list(wal):
return [parse_work_attribute(wa) for wa in wal]
def parse_work_attribute(wa):
attribs = ["type"]
typeinfo = parse_attributes(attribs, wa)
result = {}
if typeinfo:
result = {"attribute": typeinfo["type"],
"value": wa.text}
return result
def parse_url_list(ul):
return [parse_url(u) for u in ul]
def parse_url(url):
result = {}
attribs = ["id"]
elements = ["resource"]
inner_els = {"relation-list": parse_relation_list}
result.update(parse_attributes(attribs, url))
result.update(parse_elements(elements, inner_els, url))
return result
def parse_disc(disc):
result = {}
attribs = ["id"]
elements = ["sectors"]
inner_els = {"release-list": parse_release_list,
"offset-list": parse_offset_list
}
result.update(parse_attributes(attribs, disc))
result.update(parse_elements(elements, inner_els, disc))
return result
def parse_cdstub(cdstub):
result = {}
attribs = ["id"]
elements = ["title", "artist", "barcode"]
inner_els = {"track-list": parse_track_list}
result.update(parse_attributes(attribs, cdstub))
result.update(parse_elements(elements, inner_els, cdstub))
return result
def parse_offset_list(ol):
return [int(o.text) for o in ol]
def parse_instrument_list(rl):
result = []
for r in rl:
result.append(parse_instrument(r))
return result
def parse_release_list(rl):
result = []
for r in rl:
result.append(parse_release(r))
return result
def parse_release_group_list(rgl):
result = []
for rg in rgl:
result.append(parse_release_group(rg))
return result
def parse_isrc(isrc):
result = {}
attribs = ["id"]
inner_els = {"recording-list": parse_recording_list}
result.update(parse_attributes(attribs, isrc))
result.update(parse_elements([], inner_els, isrc))
return result
def parse_recording_list(recs):
result = []
for r in recs:
result.append(parse_recording(r))
return result
def parse_artist_credit(ac):
result = []
for namecredit in ac:
result.append(parse_name_credit(namecredit))
join = parse_attributes(["joinphrase"], namecredit)
if "joinphrase" in join:
result.append(join["joinphrase"])
return result
def parse_name_credit(nc):
result = {}
elements = ["name"]
inner_els = {"artist": parse_artist}
result.update(parse_elements(elements, inner_els, nc))
return result
def parse_label_info_list(lil):
result = []
for li in lil:
result.append(parse_label_info(li))
return result
def parse_label_info(li):
result = {}
elements = ["catalog-number"]
inner_els = {"label": parse_label}
result.update(parse_elements(elements, inner_els, li))
return result
def parse_track_list(tl):
result = []
for t in tl:
result.append(parse_track(t))
return result
def parse_track(track):
result = {}
attribs = ["id"]
elements = ["number", "position", "title", "length"]
inner_els = {"recording": parse_recording,
"artist-credit": parse_artist_credit}
result.update(parse_attributes(attribs, track))
result.update(parse_elements(elements, inner_els, track))
if "artist-credit" in result.get("recording", {}) and "artist-credit" not in result:
result["artist-credit"] = result["recording"]["artist-credit"]
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(result["artist-credit"])
# Make a length field that contains track length or recording length
track_or_recording = None
if "length" in result:
track_or_recording = result["length"]
elif result.get("recording", {}).get("length"):
track_or_recording = result.get("recording", {}).get("length")
if track_or_recording:
result["track_or_recording_length"] = track_or_recording
return result
def parse_tag_list(tl):
return [parse_tag(t) for t in tl]
def parse_tag(tag):
result = {}
attribs = ["count"]
elements = ["name"]
result.update(parse_attributes(attribs, tag))
result.update(parse_elements(elements, {}, tag))
return result
def parse_rating(rating):
result = {}
attribs = ["votes-count"]
result.update(parse_attributes(attribs, rating))
result["rating"] = rating.text
return result
def parse_alias_list(al):
return [parse_alias(a) for a in al]
def parse_alias(alias):
result = {}
attribs = ["locale", "sort-name", "type", "primary",
"begin-date", "end-date"]
result.update(parse_attributes(attribs, alias))
result["alias"] = alias.text
return result
def parse_caa(caa_element):
result = {}
elements = ["artwork", "count", "front", "back", "darkened"]
result.update(parse_elements(elements, {}, caa_element))
return result
###
def make_barcode_request(release2barcode):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
rel_list = ET.SubElement(root, "{%s}release-list" % NS)
for release, barcode in release2barcode.items():
rel_xml = ET.SubElement(rel_list, "{%s}release" % NS)
bar_xml = ET.SubElement(rel_xml, "{%s}barcode" % NS)
rel_xml.set("{%s}id" % NS, release)
bar_xml.text = barcode
return ET.tostring(root, "utf-8")
def make_tag_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
for entity_type in ['artist', 'label', 'place', 'recording', 'release', 'release_group', 'work']:
entity_tags = kwargs.pop(entity_type + '_tags', None)
if entity_tags is not None:
e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
for e, tags in entity_tags.items():
e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
e_xml.set("{%s}id" % NS, e)
taglist = ET.SubElement(e_xml, "{%s}user-tag-list" % NS)
for tag in tags:
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
name_xml.text = tag
if kwargs.keys():
raise TypeError("make_tag_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8")
def make_rating_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
for entity_type in ['artist', 'label', 'recording', 'release_group', 'work']:
entity_ratings = kwargs.pop(entity_type + '_ratings', None)
if entity_ratings is not None:
e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
for e, rating in entity_ratings.items():
e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
e_xml.set("{%s}id" % NS, e)
rating_xml = ET.SubElement(e_xml, "{%s}user-rating" % NS)
rating_xml.text = str(rating)
if kwargs.keys():
raise TypeError("make_rating_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8")
def make_isrc_request(recording2isrcs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
rec_list = ET.SubElement(root, "{%s}recording-list" % NS)
for rec, isrcs in recording2isrcs.items():
if len(isrcs) > 0:
rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS)
rec_xml.set("{%s}id" % NS, rec)
isrc_list_xml = ET.SubElement(rec_xml, "{%s}isrc-list" % NS)
isrc_list_xml.set("{%s}count" % NS, str(len(isrcs)))
for isrc in isrcs:
isrc_xml = ET.SubElement(isrc_list_xml, "{%s}isrc" % NS)
isrc_xml.set("{%s}id" % NS, isrc)
return ET.tostring(root, "utf-8")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578763676.0
musicbrainzngs-0.7.1/musicbrainzngs/musicbrainz.py 0000644 0000765 0000024 00000135544 00000000000 023453 0 ustar 00alastair staff 0000000 0000000 # This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Adrian Sampson, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
import re
import threading
import time
import logging
import socket
import hashlib
import locale
import sys
import json
import xml.etree.ElementTree as etree
from xml.parsers import expat
from warnings import warn
from musicbrainzngs import mbxml
from musicbrainzngs import util
from musicbrainzngs import compat
_version = "0.7.1"
_log = logging.getLogger("musicbrainzngs")
LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
# Constants for validation.
RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'event', 'recording', 'release', 'release-group', 'series', 'url', 'work', 'instrument']
RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES]
TAG_INCLUDES = ["tags", "user-tags"]
RATING_INCLUDES = ["ratings", "user-ratings"]
VALID_INCLUDES = {
'area' : ["aliases", "annotation"] + RELATION_INCLUDES,
'artist': [
"recordings", "releases", "release-groups", "works", # Subqueries
"various-artists", "discids", "media", "isrcs",
"aliases", "annotation"
] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
'annotation': [
],
'instrument': ["aliases", "annotation"
] + RELATION_INCLUDES + TAG_INCLUDES,
'label': [
"releases", # Subqueries
"discids", "media",
"aliases", "annotation"
] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
'place' : ["aliases", "annotation"] + RELATION_INCLUDES + TAG_INCLUDES,
'event' : ["aliases"] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
'recording': [
"artists", "releases", # Subqueries
"discids", "media", "artist-credits", "isrcs",
"work-level-rels", "annotation", "aliases"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'release': [
"artists", "labels", "recordings", "release-groups", "media",
"artist-credits", "discids", "isrcs",
"recording-level-rels", "work-level-rels", "annotation", "aliases"
] + TAG_INCLUDES + RELATION_INCLUDES,
'release-group': [
"artists", "releases", "discids", "media",
"artist-credits", "annotation", "aliases"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'series': [
"annotation", "aliases"
] + RELATION_INCLUDES,
'work': [
"aliases", "annotation"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'url': RELATION_INCLUDES,
'discid': [ # Discid should be the same as release
"artists", "labels", "recordings", "release-groups", "media",
"artist-credits", "discids", "isrcs",
"recording-level-rels", "work-level-rels", "annotation", "aliases"
] + RELATION_INCLUDES,
'isrc': ["artists", "releases", "isrcs"],
'iswc': ["artists"],
'collection': ['releases'],
}
VALID_BROWSE_INCLUDES = {
'artist': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'event': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'label': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'recording': ["artist-credits", "isrcs"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'release': ["artist-credits", "labels", "recordings", "isrcs",
"release-groups", "media", "discids"] + RELATION_INCLUDES,
'place': ["aliases"] + TAG_INCLUDES + RELATION_INCLUDES,
'release-group': ["artist-credits"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'url': RELATION_INCLUDES,
'work': ["aliases", "annotation"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
}
#: These can be used to filter whenever releases are includes or browsed
VALID_RELEASE_TYPES = [
"nat",
"album", "single", "ep", "broadcast", "other", # primary types
"compilation", "soundtrack", "spokenword", "interview", "audiobook",
"live", "remix", "dj-mix", "mixtape/street", # secondary types
]
#: These can be used to filter whenever releases or release-groups are involved
VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"]
VALID_SEARCH_FIELDS = {
'annotation': [
'entity', 'name', 'text', 'type'
],
'area': [
'aid', 'alias', 'area', 'areaaccent', 'begin', 'comment', 'end',
'ended', 'iso', 'iso1', 'iso2', 'iso3', 'sortname', 'tag', 'type'
],
'artist': [
'alias', 'area', 'arid', 'artist', 'artistaccent', 'begin', 'beginarea',
'comment', 'country', 'end', 'endarea', 'ended', 'gender',
'ipi', 'isni', 'primary_alias', 'sortname', 'tag', 'type'
],
'event': [
'aid', 'alias', 'area', 'arid', 'artist', 'begin', 'comment', 'eid',
'end', 'ended', 'event', 'eventaccent', 'pid', 'place', 'tag', 'type'
],
'instrument': [
'alias', 'comment', 'description', 'iid', 'instrument',
'instrumentaccent', 'tag', 'type'
],
'label': [
'alias', 'area', 'begin', 'code', 'comment', 'country', 'end', 'ended',
'ipi', 'label', 'labelaccent', 'laid', 'release_count', 'sortname',
'tag', 'type'
],
'place': [
'address', 'alias', 'area', 'begin', 'comment', 'end', 'ended', 'lat', 'long',
'pid', 'place', 'placeaccent', 'type'
],
'recording': [
'alias', 'arid', 'artist', 'artistname', 'comment', 'country',
'creditname', 'date', 'dur', 'format', 'isrc', 'number', 'position',
'primarytype', 'qdur', 'recording', 'recordingaccent', 'reid',
'release', 'rgid', 'rid', 'secondarytype', 'status', 'tag', 'tid',
'tnum', 'tracks', 'tracksrelease', 'type', 'video'],
'release-group': [
'alias', 'arid', 'artist', 'artistname', 'comment', 'creditname',
'primarytype', 'reid', 'release', 'releasegroup', 'releasegroupaccent',
'releases', 'rgid', 'secondarytype', 'status', 'tag', 'type'
],
'release': [
'alias', 'arid', 'artist', 'artistname', 'asin', 'barcode', 'catno',
'comment', 'country', 'creditname', 'date', 'discids', 'discidsmedium',
'format', 'label', 'laid', 'lang', 'mediums', 'primarytype', 'quality',
'reid', 'release', 'releaseaccent', 'rgid', 'script', 'secondarytype',
'status', 'tag', 'tracks', 'tracksmedium', 'type'
],
'series': [
'alias', 'comment', 'orderingattribute', 'series', 'seriesaccent',
'sid', 'tag', 'type'
],
'work': [
'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'recording',
'recording_count', 'rid', 'tag', 'type', 'wid', 'work', 'workaccent'
]
}
# Constants
class AUTH_YES: pass
class AUTH_NO: pass
class AUTH_IFSET: pass
# Exceptions.
class MusicBrainzError(Exception):
"""Base class for all exceptions related to MusicBrainz."""
pass
class UsageError(MusicBrainzError):
"""Error related to misuse of the module API."""
pass
class InvalidSearchFieldError(UsageError):
pass
class InvalidIncludeError(UsageError):
def __init__(self, msg='Invalid Includes', reason=None):
super(InvalidIncludeError, self).__init__(self)
self.msg = msg
self.reason = reason
def __str__(self):
return self.msg
class InvalidFilterError(UsageError):
def __init__(self, msg='Invalid Includes', reason=None):
super(InvalidFilterError, self).__init__(self)
self.msg = msg
self.reason = reason
def __str__(self):
return self.msg
class WebServiceError(MusicBrainzError):
"""Error related to MusicBrainz API requests."""
def __init__(self, message=None, cause=None):
"""Pass ``cause`` if this exception was caused by another
exception.
"""
self.message = message
self.cause = cause
def __str__(self):
if self.message:
msg = "%s, " % self.message
else:
msg = ""
msg += "caused by: %s" % str(self.cause)
return msg
class NetworkError(WebServiceError):
"""Problem communicating with the MB server."""
pass
class ResponseError(WebServiceError):
"""Bad response sent by the MB server."""
pass
class AuthenticationError(WebServiceError):
"""Received a HTTP 401 response while accessing a protected resource."""
pass
# Helpers for validating and formatting allowed sets.
def _check_includes_impl(includes, valid_includes):
for i in includes:
if i not in valid_includes:
raise InvalidIncludeError("Bad includes: "
"%s is not a valid include" % i)
def _check_includes(entity, inc):
_check_includes_impl(inc, VALID_INCLUDES[entity])
def _check_filter(values, valid):
for v in values:
if v not in valid:
raise InvalidFilterError(v)
def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[]):
"""Check that the status or type values are valid. Then, check that
the filters can be used with the given includes. Return a params
dict that can be passed to _do_mb_query.
"""
if isinstance(release_status, compat.basestring):
release_status = [release_status]
if isinstance(release_type, compat.basestring):
release_type = [release_type]
_check_filter(release_status, VALID_RELEASE_STATUSES)
_check_filter(release_type, VALID_RELEASE_TYPES)
if (release_status
and "releases" not in includes and entity != "release"):
raise InvalidFilterError("Can't have a status with no release include")
if (release_type
and "release-groups" not in includes and "releases" not in includes
and entity not in ["release-group", "release"]):
raise InvalidFilterError("Can't have a release type "
"with no releases or release-groups involved")
# Build parameters.
params = {}
if len(release_status):
params["status"] = "|".join(release_status)
if len(release_type):
params["type"] = "|".join(release_type)
return params
def _docstring_get(entity):
includes = list(VALID_INCLUDES.get(entity, []))
return _docstring_impl("includes", includes)
def _docstring_browse(entity):
includes = list(VALID_BROWSE_INCLUDES.get(entity, []))
return _docstring_impl("includes", includes)
def _docstring_search(entity):
search_fields = list(VALID_SEARCH_FIELDS.get(entity, []))
return _docstring_impl("fields", search_fields)
def _docstring_impl(name, values):
def _decorator(func):
vstr = ", ".join(values)
args = {name: vstr}
if func.__doc__:
func.__doc__ = func.__doc__.format(**args)
return func
return _decorator
# Global authentication and endpoint details.
user = password = ""
hostname = "musicbrainz.org"
https = True
_client = ""
_useragent = ""
def auth(u, p):
"""Set the username and password to be used in subsequent queries to
the MusicBrainz XML API that require authentication.
"""
global user, password
user = u
password = p
def set_useragent(app, version, contact=None):
"""Set the User-Agent to be used for requests to the MusicBrainz webservice.
This must be set before requests are made."""
global _useragent, _client
if not app or not version:
raise ValueError("App and version can not be empty")
if contact is not None:
_useragent = "%s/%s python-musicbrainzngs/%s ( %s )" % (app, version, _version, contact)
else:
_useragent = "%s/%s python-musicbrainzngs/%s" % (app, version, _version)
_client = "%s-%s" % (app, version)
_log.debug("set user-agent to %s" % _useragent)
def set_hostname(new_hostname, use_https=False):
"""Set the hostname for MusicBrainz webservice requests.
Defaults to 'musicbrainz.org', accessing over https.
For backwards compatibility, `use_https` is False by default.
:param str new_hostname: The hostname (and port) of the MusicBrainz server to connect to
:param bool use_https: `True` if the host should be accessed using https. Default is `False`
Specify a non-standard port by adding it to the hostname,
for example 'localhost:8000'."""
global hostname
global https
hostname = new_hostname
https = use_https
# Rate limiting.
limit_interval = 1.0
limit_requests = 1
do_rate_limit = True
def set_rate_limit(limit_or_interval=1.0, new_requests=1):
"""Sets the rate limiting behavior of the module. Must be invoked
before the first Web service call.
If the `limit_or_interval` parameter is set to False then
rate limiting will be disabled. If it is a number then only
a set number of requests (`new_requests`) will be made per
given interval (`limit_or_interval`).
"""
global limit_interval
global limit_requests
global do_rate_limit
if isinstance(limit_or_interval, bool):
do_rate_limit = limit_or_interval
else:
if limit_or_interval <= 0.0:
raise ValueError("limit_or_interval can't be less than 0")
if new_requests <= 0:
raise ValueError("new_requests can't be less than 0")
do_rate_limit = True
limit_interval = limit_or_interval
limit_requests = new_requests
class _rate_limit(object):
"""A decorator that limits the rate at which the function may be
called. The rate is controlled by the `limit_interval` and
`limit_requests` global variables. The limiting is thread-safe;
only one thread may be in the function at a time (acts like a
monitor in this sense). The globals must be set before the first
call to the limited function.
"""
def __init__(self, fun):
self.fun = fun
self.last_call = 0.0
self.lock = threading.Lock()
self.remaining_requests = None # Set on first invocation.
def _update_remaining(self):
"""Update remaining requests based on the elapsed time since
they were last calculated.
"""
# On first invocation, we have the maximum number of requests
# available.
if self.remaining_requests is None:
self.remaining_requests = float(limit_requests)
else:
since_last_call = time.time() - self.last_call
self.remaining_requests += since_last_call * \
(limit_requests / limit_interval)
self.remaining_requests = min(self.remaining_requests,
float(limit_requests))
self.last_call = time.time()
def __call__(self, *args, **kwargs):
with self.lock:
if do_rate_limit:
self._update_remaining()
# Delay if necessary.
while self.remaining_requests < 0.999:
time.sleep((1.0 - self.remaining_requests) *
(limit_requests / limit_interval))
self._update_remaining()
# Call the original function, "paying" for this call.
self.remaining_requests -= 1.0
return self.fun(*args, **kwargs)
# From pymb2
class _RedirectPasswordMgr(compat.HTTPPasswordMgr):
def __init__(self):
self._realms = { }
def find_user_password(self, realm, uri):
# ignoring the uri parameter intentionally
try:
return self._realms[realm]
except KeyError:
return (None, None)
def add_password(self, realm, uri, username, password):
# ignoring the uri parameter intentionally
self._realms[realm] = (username, password)
class _DigestAuthHandler(compat.HTTPDigestAuthHandler):
def get_authorization (self, req, chal):
qop = chal.get ('qop', None)
if qop and ',' in qop and 'auth' in qop.split (','):
chal['qop'] = 'auth'
return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal)
def _encode_utf8(self, msg):
"""The MusicBrainz server also accepts UTF-8 encoded passwords."""
encoding = sys.stdin.encoding or locale.getpreferredencoding()
try:
# This works on Python 2 (msg in bytes)
msg = msg.decode(encoding)
except AttributeError:
# on Python 3 (msg is already in unicode)
pass
return msg.encode("utf-8")
def get_algorithm_impls(self, algorithm):
# algorithm should be case-insensitive according to RFC2617
algorithm = algorithm.upper()
# lambdas assume digest modules are imported at the top level
if algorithm == 'MD5':
H = lambda x: hashlib.md5(self._encode_utf8(x)).hexdigest()
elif algorithm == 'SHA':
H = lambda x: hashlib.sha1(self._encode_utf8(x)).hexdigest()
# XXX MD5-sess
KD = lambda s, d: H("%s:%s" % (s, d))
return H, KD
class _MusicbrainzHttpRequest(compat.Request):
""" A custom request handler that allows DELETE and PUT"""
def __init__(self, method, url, data=None):
compat.Request.__init__(self, url, data)
allowed_m = ["GET", "POST", "DELETE", "PUT"]
if method not in allowed_m:
raise ValueError("invalid method: %s" % method)
self.method = method
def get_method(self):
return self.method
# Core (internal) functions for calling the MB API.
def _safe_read(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
"""Open an HTTP request with a given URL opener and (optionally) a
request body. Transient errors lead to retries. Permanent errors
and repeated errors are translated into a small set of handleable
exceptions. Return a bytestring.
"""
last_exc = None
for retry_num in range(max_retries):
if retry_num: # Not the first try: delay an increasing amount.
_log.info("retrying after delay (#%i)" % retry_num)
time.sleep(retry_num * retry_delay_delta)
try:
if body:
f = opener.open(req, body)
else:
f = opener.open(req)
return f.read()
except compat.HTTPError as exc:
if exc.code in (400, 404, 411):
# Bad request, not found, etc.
raise ResponseError(cause=exc)
elif exc.code in (503, 502, 500):
# Rate limiting, internal overloading...
_log.info("HTTP error %i" % exc.code)
elif exc.code in (401, ):
raise AuthenticationError(cause=exc)
else:
# Other, unknown error. Should handle more cases, but
# retrying for now.
_log.info("unknown HTTP error %i" % exc.code)
last_exc = exc
except compat.BadStatusLine as exc:
_log.info("bad status line")
last_exc = exc
except compat.HTTPException as exc:
_log.info("miscellaneous HTTP exception: %s" % str(exc))
last_exc = exc
except compat.URLError as exc:
if isinstance(exc.reason, socket.error):
code = exc.reason.errno
if code == 104: # "Connection reset by peer."
continue
raise NetworkError(cause=exc)
except socket.timeout as exc:
_log.info("socket timeout")
last_exc = exc
except socket.error as exc:
if exc.errno == 104:
continue
raise NetworkError(cause=exc)
except IOError as exc:
raise NetworkError(cause=exc)
# Out of retries!
raise NetworkError("retried %i times" % max_retries, last_exc)
# Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
# and ElementTree 1.3.
if hasattr(etree, 'ParseError'):
ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
else:
ETREE_EXCEPTIONS = (expat.ExpatError)
# Parsing setup
def mb_parser_null(resp):
"""Return the raw response (XML)"""
return resp
def mb_parser_xml(resp):
"""Return a Python dict representing the XML response"""
# Parse the response.
try:
return mbxml.parse_message(resp)
except UnicodeError as exc:
raise ResponseError(cause=exc)
except Exception as exc:
if isinstance(exc, ETREE_EXCEPTIONS):
raise ResponseError(cause=exc)
else:
raise
# Defaults
parser_fun = mb_parser_xml
ws_format = "xml"
def set_parser(new_parser_fun=None):
"""Sets the function used to parse the response from the
MusicBrainz web service.
If no parser is given, the parser is reset to the default parser
:func:`mb_parser_xml`.
"""
global parser_fun
if new_parser_fun is None:
new_parser_fun = mb_parser_xml
if not callable(new_parser_fun):
raise ValueError("new_parser_fun must be callable")
parser_fun = new_parser_fun
def set_format(fmt="xml"):
"""Sets the format that should be returned by the Web Service.
The server currently supports `xml` and `json`.
This method will set a default parser for the specified format,
but you can modify it with :func:`set_parser`.
.. warning:: The json format used by the server is different from
the json format returned by the `musicbrainzngs` internal parser
when using the `xml` format! This format may change at any time.
"""
global ws_format
if fmt == "xml":
ws_format = fmt
set_parser() # set to default
elif fmt == "json":
ws_format = fmt
warn("The json format is non-official and may change at any time")
set_parser(json.loads)
else:
raise ValueError("invalid format: %s" % fmt)
@_rate_limit
def _mb_request(path, method='GET', auth_required=AUTH_NO,
client_required=False, args=None, data=None, body=None):
"""Makes a request for the specified `path` (endpoint) on /ws/2 on
the globally-specified hostname. Parses the responses and returns
the resulting object. `auth_required` and `client_required` control
whether exceptions should be raised if the username/password and
client are left unspecified, respectively.
"""
global parser_fun
if args is None:
args = {}
else:
args = dict(args) or {}
if _useragent == "":
raise UsageError("set a proper user-agent with "
"set_useragent(\"application name\", \"application version\", \"contact info (preferably URL or email for your application)\")")
if client_required:
args["client"] = _client
if ws_format != "xml":
args["fmt"] = ws_format
# Convert args from a dictionary to a list of tuples
# so that the ordering of elements is stable for easy
# testing (in this case we order alphabetically)
# Encode Unicode arguments using UTF-8.
newargs = []
for key, value in sorted(args.items()):
if isinstance(value, compat.unicode):
value = value.encode('utf8')
newargs.append((key, value))
# Construct the full URL for the request, including hostname and
# query string.
url = compat.urlunparse((
'https' if https else 'http',
hostname,
'/ws/2/%s' % path,
'',
compat.urlencode(newargs),
''
))
_log.debug("%s request for %s" % (method, url))
# Set up HTTP request handler and URL opener.
httpHandler = compat.HTTPHandler(debuglevel=0)
handlers = [httpHandler]
# Add credentials if required.
add_auth = False
if auth_required == AUTH_YES:
_log.debug("Auth required for %s" % url)
if not user:
raise UsageError("authorization required; "
"use auth(user, pass) first")
add_auth = True
if auth_required == AUTH_IFSET and user:
_log.debug("Using auth for %s because user and pass is set" % url)
add_auth = True
if add_auth:
passwordMgr = _RedirectPasswordMgr()
authHandler = _DigestAuthHandler(passwordMgr)
authHandler.add_password("musicbrainz.org", (), user, password)
handlers.append(authHandler)
opener = compat.build_opener(*handlers)
# Make request.
req = _MusicbrainzHttpRequest(method, url, data)
req.add_header('User-Agent', _useragent)
_log.debug("requesting with UA %s" % _useragent)
if body:
req.add_header('Content-Type', 'application/xml; charset=UTF-8')
elif not data and not req.has_header('Content-Length'):
# Explicitly indicate zero content length if no request data
# will be sent (avoids HTTP 411 error).
req.add_header('Content-Length', '0')
resp = _safe_read(opener, req, body)
return parser_fun(resp)
def _get_auth_type(entity, id, includes):
""" Some calls require authentication. This returns
True if a call does, False otherwise
"""
if "user-tags" in includes or "user-ratings" in includes:
return AUTH_YES
elif entity.startswith("collection"):
if not id:
return AUTH_YES
else:
return AUTH_IFSET
else:
return AUTH_NO
def _do_mb_query(entity, id, includes=[], params={}):
"""Make a single GET call to the MusicBrainz XML API. `entity` is a
string indicated the type of object to be retrieved. The id may be
empty, in which case the query is a search. `includes` is a list
of strings that must be valid includes for the entity type. `params`
is a dictionary of additional parameters for the API call. The
response is parsed and returned.
"""
# Build arguments.
if not isinstance(includes, list):
includes = [includes]
_check_includes(entity, includes)
auth_required = _get_auth_type(entity, id, includes)
args = dict(params)
if len(includes) > 0:
inc = " ".join(includes)
args["inc"] = inc
# Build the endpoint components.
path = '%s/%s' % (entity, id)
return _mb_request(path, 'GET', auth_required, args=args)
def _do_mb_search(entity, query='', fields={},
limit=None, offset=None, strict=False):
"""Perform a full-text search on the MusicBrainz search server.
`query` is a lucene query string when no fields are set,
but is escaped when any fields are given. `fields` is a dictionary
of key/value query parameters. They keys in `fields` must be valid
for the given entity type.
"""
# Encode the query terms as a Lucene query string.
query_parts = []
if query:
clean_query = util._unicode(query)
if fields:
clean_query = re.sub(LUCENE_SPECIAL, r'\\\1',
clean_query)
if strict:
query_parts.append('"%s"' % clean_query)
else:
query_parts.append(clean_query.lower())
else:
query_parts.append(clean_query)
for key, value in fields.items():
# Ensure this is a valid search field.
if key not in VALID_SEARCH_FIELDS[entity]:
raise InvalidSearchFieldError(
'%s is not a valid search field for %s' % (key, entity)
)
# Escape Lucene's special characters.
value = util._unicode(value)
value = re.sub(LUCENE_SPECIAL, r'\\\1', value)
if value:
if strict:
query_parts.append('%s:"%s"' % (key, value))
else:
value = value.lower() # avoid AND / OR
query_parts.append('%s:(%s)' % (key, value))
if strict:
full_query = ' AND '.join(query_parts).strip()
else:
full_query = ' '.join(query_parts).strip()
if not full_query:
raise ValueError('at least one query term is required')
# Additional parameters to the search.
params = {'query': full_query}
if limit:
params['limit'] = str(limit)
if offset:
params['offset'] = str(offset)
return _do_mb_query(entity, '', [], params)
def _do_mb_delete(path):
"""Send a DELETE request for the specified object.
"""
return _mb_request(path, 'DELETE', AUTH_YES, True)
def _do_mb_put(path):
"""Send a PUT request for the specified object.
"""
return _mb_request(path, 'PUT', AUTH_YES, True)
def _do_mb_post(path, body):
"""Perform a single POST call for an endpoint with a specified
request body.
"""
return _mb_request(path, 'POST', AUTH_YES, True, body=body)
# The main interface!
# Single entity by ID
@_docstring_get("area")
def get_area_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the area with the MusicBrainz `id` as a dict with an 'area' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("area", includes,
release_status, release_type)
return _do_mb_query("area", id, includes, params)
@_docstring_get("artist")
def get_artist_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the artist with the MusicBrainz `id` as a dict with an 'artist' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("artist", includes,
release_status, release_type)
return _do_mb_query("artist", id, includes, params)
@_docstring_get("instrument")
def get_instrument_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the instrument with the MusicBrainz `id` as a dict with an 'artist' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("instrument", includes,
release_status, release_type)
return _do_mb_query("instrument", id, includes, params)
@_docstring_get("label")
def get_label_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the label with the MusicBrainz `id` as a dict with a 'label' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("label", includes,
release_status, release_type)
return _do_mb_query("label", id, includes, params)
@_docstring_get("place")
def get_place_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the place with the MusicBrainz `id` as a dict with an 'place' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("place", includes,
release_status, release_type)
return _do_mb_query("place", id, includes, params)
@_docstring_get("event")
def get_event_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the event with the MusicBrainz `id` as a dict with an 'event' key.
The event dict has the following keys:
`id`, `type`, `name`, `time`, `disambiguation` and `life-span`.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("event", includes,
release_status, release_type)
return _do_mb_query("event", id, includes, params)
@_docstring_get("recording")
def get_recording_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the recording with the MusicBrainz `id` as a dict
with a 'recording' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("recording", includes,
release_status, release_type)
return _do_mb_query("recording", id, includes, params)
@_docstring_get("release")
def get_release_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the release with the MusicBrainz `id` as a dict with a 'release' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("release", includes,
release_status, release_type)
return _do_mb_query("release", id, includes, params)
@_docstring_get("release-group")
def get_release_group_by_id(id, includes=[],
release_status=[], release_type=[]):
"""Get the release group with the MusicBrainz `id` as a dict
with a 'release-group' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("release-group", includes,
release_status, release_type)
return _do_mb_query("release-group", id, includes, params)
@_docstring_get("series")
def get_series_by_id(id, includes=[]):
"""Get the series with the MusicBrainz `id` as a dict with a 'series' key.
*Available includes*: {includes}"""
return _do_mb_query("series", id, includes)
@_docstring_get("work")
def get_work_by_id(id, includes=[]):
"""Get the work with the MusicBrainz `id` as a dict with a 'work' key.
*Available includes*: {includes}"""
return _do_mb_query("work", id, includes)
@_docstring_get("url")
def get_url_by_id(id, includes=[]):
"""Get the url with the MusicBrainz `id` as a dict with a 'url' key.
*Available includes*: {includes}"""
return _do_mb_query("url", id, includes)
# Searching
@_docstring_search("annotation")
def search_annotations(query='', limit=None, offset=None, strict=False, **fields):
"""Search for annotations and return a dict with an 'annotation-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('annotation', query, fields, limit, offset, strict)
@_docstring_search("area")
def search_areas(query='', limit=None, offset=None, strict=False, **fields):
"""Search for areas and return a dict with an 'area-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('area', query, fields, limit, offset, strict)
@_docstring_search("artist")
def search_artists(query='', limit=None, offset=None, strict=False, **fields):
"""Search for artists and return a dict with an 'artist-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('artist', query, fields, limit, offset, strict)
@_docstring_search("event")
def search_events(query='', limit=None, offset=None, strict=False, **fields):
"""Search for events and return a dict with an 'event-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('event', query, fields, limit, offset, strict)
@_docstring_search("instrument")
def search_instruments(query='', limit=None, offset=None, strict=False, **fields):
"""Search for instruments and return a dict with a 'instrument-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('instrument', query, fields, limit, offset, strict)
@_docstring_search("label")
def search_labels(query='', limit=None, offset=None, strict=False, **fields):
"""Search for labels and return a dict with a 'label-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('label', query, fields, limit, offset, strict)
@_docstring_search("place")
def search_places(query='', limit=None, offset=None, strict=False, **fields):
"""Search for places and return a dict with a 'place-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('place', query, fields, limit, offset, strict)
@_docstring_search("recording")
def search_recordings(query='', limit=None, offset=None,
strict=False, **fields):
"""Search for recordings and return a dict with a 'recording-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('recording', query, fields, limit, offset, strict)
@_docstring_search("release")
def search_releases(query='', limit=None, offset=None, strict=False, **fields):
"""Search for recordings and return a dict with a 'recording-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('release', query, fields, limit, offset, strict)
@_docstring_search("release-group")
def search_release_groups(query='', limit=None, offset=None,
strict=False, **fields):
"""Search for release groups and return a dict
with a 'release-group-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('release-group', query, fields, limit, offset, strict)
@_docstring_search("series")
def search_series(query='', limit=None, offset=None, strict=False, **fields):
"""Search for series and return a dict with a 'series-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('series', query, fields, limit, offset, strict)
@_docstring_search("work")
def search_works(query='', limit=None, offset=None, strict=False, **fields):
"""Search for works and return a dict with a 'work-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('work', query, fields, limit, offset, strict)
# Lists of entities
@_docstring_get("discid")
def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None):
"""Search for releases with a :musicbrainz:`Disc ID` or table of contents.
When a `toc` is provided and no release with the disc ID is found,
a fuzzy search by the toc is done.
The `toc` should have to same format as :attr:`discid.Disc.toc_string`.
When a `toc` is provided, the format of the discid itself is not
checked server-side, so any value may be passed if searching by only
`toc` is desired.
If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does,
the CD Stub will be returned. Prevent this from happening by
passing `cdstubs=False`.
By default only results that match a format that allows discids
(e.g. CD) are included. To include all media formats, pass
`media_format='all'`.
The result is a dict with either a 'disc' , a 'cdstub' key
or a 'release-list' (fuzzy match with TOC).
A 'disc' has an 'offset-count', an 'offset-list' and a 'release-list'.
A 'cdstub' key has direct 'artist' and 'title' keys.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("discid", includes, release_status=[],
release_type=[])
if toc:
params["toc"] = toc
if not cdstubs:
params["cdstubs"] = "no"
if media_format:
params["media-format"] = media_format
return _do_mb_query("discid", id, includes, params)
@_docstring_get("recording")
def get_recordings_by_isrc(isrc, includes=[], release_status=[],
release_type=[]):
"""Search for recordings with an :musicbrainz:`ISRC`.
The result is a dict with an 'isrc' key,
which again includes a 'recording-list'.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("isrc", includes,
release_status, release_type)
return _do_mb_query("isrc", isrc, includes, params)
@_docstring_get("work")
def get_works_by_iswc(iswc, includes=[]):
"""Search for works with an :musicbrainz:`ISWC`.
The result is a dict with a`work-list`.
*Available includes*: {includes}"""
return _do_mb_query("iswc", iswc, includes)
def _browse_impl(entity, includes, limit, offset, params, release_status=[], release_type=[]):
includes = includes if isinstance(includes, list) else [includes]
valid_includes = VALID_BROWSE_INCLUDES[entity]
_check_includes_impl(includes, valid_includes)
p = {}
for k,v in params.items():
if v:
p[k] = v
if len(p) > 1:
raise Exception("Can't have more than one of " + ", ".join(params.keys()))
if limit: p["limit"] = limit
if offset: p["offset"] = offset
filterp = _check_filter_and_make_params(entity, includes, release_status, release_type)
p.update(filterp)
return _do_mb_query(entity, "", includes, p)
# Browse methods
# Browse include are a subset of regular get includes, so we check them here
# and the test in _do_mb_query will pass anyway.
@_docstring_browse("artist")
def browse_artists(recording=None, release=None, release_group=None,
work=None, includes=[], limit=None, offset=None):
"""Get all artists linked to a recording, a release or a release group.
You need to give one MusicBrainz ID.
*Available includes*: {includes}"""
params = {"recording": recording,
"release": release,
"release-group": release_group,
"work": work}
return _browse_impl("artist", includes, limit, offset, params)
@_docstring_browse("event")
def browse_events(area=None, artist=None, place=None,
includes=[], limit=None, offset=None):
"""Get all events linked to a area, a artist or a place.
You need to give one MusicBrainz ID.
*Available includes*: {includes}"""
params = {"area": area,
"artist": artist,
"place": place}
return _browse_impl("event", includes, limit, offset, params)
@_docstring_browse("label")
def browse_labels(release=None, includes=[], limit=None, offset=None):
"""Get all labels linked to a relase. You need to give a MusicBrainz ID.
*Available includes*: {includes}"""
params = {"release": release}
return _browse_impl("label", includes, limit, offset, params)
@_docstring_browse("place")
def browse_places(area=None, includes=[], limit=None, offset=None):
"""Get all places linked to an area. You need to give a MusicBrainz ID.
*Available includes*: {includes}"""
params = {"area": area}
return _browse_impl("place", includes, limit, offset, params)
@_docstring_browse("recording")
def browse_recordings(artist=None, release=None, includes=[],
limit=None, offset=None):
"""Get all recordings linked to an artist or a release.
You need to give one MusicBrainz ID.
*Available includes*: {includes}"""
params = {"artist": artist,
"release": release}
return _browse_impl("recording", includes, limit, offset, params)
@_docstring_browse("release")
def browse_releases(artist=None, track_artist=None, label=None, recording=None,
release_group=None, release_status=[], release_type=[],
includes=[], limit=None, offset=None):
"""Get all releases linked to an artist, a label, a recording
or a release group. You need to give one MusicBrainz ID.
You can also browse by `track_artist`, which gives all releases where some
tracks are attributed to that artist, but not the whole release.
You can filter by :data:`musicbrainz.VALID_RELEASE_TYPES` or
:data:`musicbrainz.VALID_RELEASE_STATUSES`.
*Available includes*: {includes}"""
# track_artist param doesn't work yet
params = {"artist": artist,
"track_artist": track_artist,
"label": label,
"recording": recording,
"release-group": release_group}
return _browse_impl("release", includes, limit, offset,
params, release_status, release_type)
@_docstring_browse("release-group")
def browse_release_groups(artist=None, release=None, release_type=[],
includes=[], limit=None, offset=None):
"""Get all release groups linked to an artist or a release.
You need to give one MusicBrainz ID.
You can filter by :data:`musicbrainz.VALID_RELEASE_TYPES`.
*Available includes*: {includes}"""
params = {"artist": artist,
"release": release}
return _browse_impl("release-group", includes, limit,
offset, params, [], release_type)
@_docstring_browse("url")
def browse_urls(resource=None, includes=[], limit=None, offset=None):
"""Get urls by actual URL string.
You need to give a URL string as 'resource'
*Available includes*: {includes}"""
params = {"resource": resource}
return _browse_impl("url", includes, limit, offset, params)
@_docstring_browse("work")
def browse_works(artist=None, includes=[], limit=None, offset=None):
"""Get all works linked to an artist
*Available includes*: {includes}"""
params = {"artist": artist}
return _browse_impl("work", includes, limit, offset, params)
# Collections
def get_collections():
"""List the collections for the currently :func:`authenticated ` user
as a dict with a 'collection-list' key."""
# Missing the count in the reply
return _do_mb_query("collection", '')
def _do_collection_query(collection, collection_type, limit, offset):
params = {}
if limit: params["limit"] = limit
if offset: params["offset"] = offset
return _do_mb_query("collection", "%s/%s" % (collection, collection_type), [], params)
def get_artists_in_collection(collection, limit=None, offset=None):
"""List the artists in a collection.
Returns a dict with a 'collection' key, which again has a 'artist-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "artists", limit, offset)
def get_releases_in_collection(collection, limit=None, offset=None):
"""List the releases in a collection.
Returns a dict with a 'collection' key, which again has a 'release-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "releases", limit, offset)
def get_events_in_collection(collection, limit=None, offset=None):
"""List the events in a collection.
Returns a dict with a 'collection' key, which again has a 'event-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "events", limit, offset)
def get_places_in_collection(collection, limit=None, offset=None):
"""List the places in a collection.
Returns a dict with a 'collection' key, which again has a 'place-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "places", limit, offset)
def get_recordings_in_collection(collection, limit=None, offset=None):
"""List the recordings in a collection.
Returns a dict with a 'collection' key, which again has a 'recording-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "recordings", limit, offset)
def get_works_in_collection(collection, limit=None, offset=None):
"""List the works in a collection.
Returns a dict with a 'collection' key, which again has a 'work-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "works", limit, offset)
# Submission methods
def submit_barcodes(release_barcode):
"""Submits a set of {release_id1: barcode, ...}"""
query = mbxml.make_barcode_request(release_barcode)
return _do_mb_post("release", query)
def submit_isrcs(recording_isrcs):
"""Submit ISRCs.
Submits a set of {recording-id1: [isrc1, ...], ...}
or {recording_id1: isrc, ...}.
"""
rec2isrcs = dict()
for (rec, isrcs) in recording_isrcs.items():
rec2isrcs[rec] = isrcs if isinstance(isrcs, list) else [isrcs]
query = mbxml.make_isrc_request(rec2isrcs)
return _do_mb_post("recording", query)
def submit_tags(**kwargs):
"""Submit user tags.
Takes parameters named e.g. 'artist_tags', 'recording_tags', etc.,
and of the form:
{entity_id1: [tag1, ...], ...}
If you only have one tag for an entity you can use a string instead
of a list.
The user's tags for each entity will be set to that list, adding or
removing tags as necessary. Submitting an empty list for an entity
will remove all tags for that entity by the user.
"""
for k, v in kwargs.items():
for id, tags in v.items():
kwargs[k][id] = tags if isinstance(tags, list) else [tags]
query = mbxml.make_tag_request(**kwargs)
return _do_mb_post("tag", query)
def submit_ratings(**kwargs):
"""Submit user ratings.
Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc.,
and of the form:
{entity_id1: rating, ...}
Ratings are numbers from 0-100, at intervals of 20 (20 per 'star').
Submitting a rating of 0 will remove the user's rating.
"""
query = mbxml.make_rating_request(**kwargs)
return _do_mb_post("rating", query)
def add_releases_to_collection(collection, releases=[]):
"""Add releases to a collection.
Collection and releases should be identified by their MBIDs
"""
# XXX: Maximum URI length of 16kb means we should only allow ~400 releases
releaselist = ";".join(releases)
return _do_mb_put("collection/%s/releases/%s" % (collection, releaselist))
def remove_releases_from_collection(collection, releases=[]):
"""Remove releases from a collection.
Collection and releases should be identified by their MBIDs
"""
releaselist = ";".join(releases)
return _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1377116296.0
musicbrainzngs-0.7.1/musicbrainzngs/util.py 0000644 0000765 0000024 00000002624 00000000000 022072 0 ustar 00alastair staff 0000000 0000000 # This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Adrian Sampson, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
import sys
import locale
import xml.etree.ElementTree as ET
from . import compat
def _unicode(string, encoding=None):
"""Try to decode byte strings to unicode.
This can only be a guess, but this might be better than failing.
It is safe to use this on numbers or strings that are already unicode.
"""
if isinstance(string, compat.unicode):
unicode_string = string
elif isinstance(string, compat.bytes):
# use given encoding, stdin, preferred until something != None is found
if encoding is None:
encoding = sys.stdin.encoding
if encoding is None:
encoding = locale.getpreferredencoding()
unicode_string = string.decode(encoding, "ignore")
else:
unicode_string = compat.unicode(string)
return unicode_string.replace('\x00', '').strip()
def bytes_to_elementtree(bytes_or_file):
"""Given a bytestring or a file-like object that will produce them,
parse and return an ElementTree.
"""
if isinstance(bytes_or_file, compat.basestring):
s = bytes_or_file
else:
s = bytes_or_file.read()
if compat.is_py3:
s = _unicode(s, "utf-8")
f = compat.StringIO(s)
tree = ET.ElementTree(file=f)
return tree
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 011451 x ustar 00 0000000 0000000 27 mtime=1578764319.847188
musicbrainzngs-0.7.1/musicbrainzngs.egg-info/ 0000755 0000765 0000024 00000000000 00000000000 022231 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578764319.0
musicbrainzngs-0.7.1/musicbrainzngs.egg-info/PKG-INFO 0000644 0000765 0000024 00000007404 00000000000 023333 0 ustar 00alastair staff 0000000 0000000 Metadata-Version: 2.1
Name: musicbrainzngs
Version: 0.7.1
Summary: Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices
Home-page: https://python-musicbrainzngs.readthedocs.io/
Author: Alastair Porter
Author-email: alastair@porter.net.nz
License: BSD 2-clause
Description: Musicbrainz NGS bindings
########################
This library implements webservice bindings for the Musicbrainz NGS site, also known as /ws/2
and the `Cover Art Archive `_.
For more information on the musicbrainz webservice see ``_.
Usage
*****
.. code:: python
# Import the module
import musicbrainzngs
# If you plan to submit data, authenticate
musicbrainzngs.auth("user", "password")
# Tell musicbrainz what your app is, and how to contact you
# (this step is required, as per the webservice access rules
# at http://wiki.musicbrainz.org/XML_Web_Service/Rate_Limiting )
musicbrainzngs.set_useragent("Example music app", "0.1", "http://example.com/music")
# If you are connecting to a different server
musicbrainzngs.set_hostname("beta.musicbrainz.org")
See the ``query.py`` file for more examples.
More documentation is available at
`Read the Docs `_.
Contribute
**********
If you want to contribute to this repository, please read `the
contribution guidelines
`_ first.
Authors
*******
These bindings were written by `Alastair Porter `_.
Contributions have been made by:
* `Adrian Sampson `_
* `Corey Farwell `_
* `Galen Hazelwood `_
* `Greg Ward `_
* `Ian McEwen `_
* `Jérémie Detrey `_
* `Johannes Dewender `_
* `Michael Marineau `_
* `Patrick Speiser `_
* `Pavan Chander `_
* `Paul Bailey `_
* `Rui Gonçalves `_
* `Ryan Helinski `_
* `Sam Doshi `_
* `Shadab Zafar `_
* `Simon Chopin `_
* `Thomas Vander Stichele `_
* `Wieland Hoffmann `_
License
*******
This library is released under the simplified BSD license except for the file
``musicbrainzngs/compat.py`` which is licensed under the ISC license.
See COPYING for details.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: BSD License
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Description-Content-Type: text/x-rst
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578764319.0
musicbrainzngs-0.7.1/musicbrainzngs.egg-info/SOURCES.txt 0000644 0000765 0000024 00000013217 00000000000 024121 0 ustar 00alastair staff 0000000 0000000 CHANGES
COPYING
MANIFEST.in
README.rst
query.py
setup.py
docs/Makefile
docs/api.rst
docs/conf.py
docs/index.rst
docs/installation.rst
docs/make.bat
docs/usage.rst
examples/browse.py
examples/collection.py
examples/find_disc.py
examples/isrcsubmit.py
examples/releasesearch.py
musicbrainzngs/__init__.py
musicbrainzngs/caa.py
musicbrainzngs/compat.py
musicbrainzngs/mbxml.py
musicbrainzngs/musicbrainz.py
musicbrainzngs/util.py
musicbrainzngs.egg-info/PKG-INFO
musicbrainzngs.egg-info/SOURCES.txt
musicbrainzngs.egg-info/dependency_links.txt
musicbrainzngs.egg-info/top_level.txt
musicbrainzngs/../docs/Makefile
musicbrainzngs/../docs/api.rst
musicbrainzngs/../docs/conf.py
musicbrainzngs/../docs/index.rst
musicbrainzngs/../docs/installation.rst
musicbrainzngs/../docs/make.bat
musicbrainzngs/../docs/usage.rst
musicbrainzngs/../examples/browse.py
musicbrainzngs/../examples/collection.py
musicbrainzngs/../examples/find_disc.py
musicbrainzngs/../examples/isrcsubmit.py
musicbrainzngs/../examples/releasesearch.py
musicbrainzngs/../test/data/search-artist.xml
musicbrainzngs/../test/data/search-label.xml
musicbrainzngs/../test/data/search-recording.xml
musicbrainzngs/../test/data/search-release-group.xml
musicbrainzngs/../test/data/search-release.xml
musicbrainzngs/../test/data/search-work.xml
musicbrainzngs/../test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml
musicbrainzngs/../test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml
musicbrainzngs/../test/data/label/022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml
musicbrainzngs/../test/data/label/e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml
musicbrainzngs/../test/data/release/212895ca-ee36-439a-a824-d2620cd10461-recordings.xml
musicbrainzngs/../test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml
musicbrainzngs/../test/data/release/a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml
musicbrainzngs/../test/data/release/b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml
musicbrainzngs/../test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml
musicbrainzngs/../test/data/release-group/f52bc6a1-c848-49e6-85de-f8f53459a624.xml
musicbrainzngs/../test/data/work/3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml
musicbrainzngs/../test/data/work/80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml
test/__init__.py
test/_common.py
test/test_browse.py
test/test_caa.py
test/test_collection.py
test/test_getentity.py
test/test_mbxml.py
test/test_mbxml_artist.py
test/test_mbxml_collection.py
test/test_mbxml_discid.py
test/test_mbxml_event.py
test/test_mbxml_instrument.py
test/test_mbxml_label.py
test/test_mbxml_place.py
test/test_mbxml_recording.py
test/test_mbxml_release.py
test/test_mbxml_release_group.py
test/test_mbxml_search.py
test/test_mbxml_work.py
test/test_ratelimit.py
test/test_requests.py
test/test_search.py
test/test_submit.py
test/data/search-artist.xml
test/data/search-event.xml
test/data/search-instrument.xml
test/data/search-label.xml
test/data/search-place.xml
test/data/search-recording.xml
test/data/search-release-group.xml
test/data/search-release.xml
test/data/search-work.xml
test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml
test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml
test/data/artist/b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml
test/data/collection/0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml
test/data/collection/20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml
test/data/collection/2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml
test/data/collection/29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml
test/data/collection/855b134e-9a3b-4717-8df8-8c4838d89924-places.xml
test/data/collection/a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml
test/data/discid/f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml
test/data/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-.xml
test/data/event/770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml
test/data/event/e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml
test/data/instrument/01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml
test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml
test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml
test/data/instrument/9447c0af-5569-48f2-b4c5-241105d58c91.xml
test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml
test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml
test/data/instrument/dabdeb41-560f-4d84-aa6a-cf22349326fe.xml
test/data/label/022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml
test/data/label/e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml
test/data/place/0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml
test/data/place/browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml
test/data/recording/f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml
test/data/release/212895ca-ee36-439a-a824-d2620cd10461-recordings.xml
test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml
test/data/release/8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml
test/data/release/9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml
test/data/release/a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml
test/data/release/b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml
test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml
test/data/release/fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml
test/data/release-group/f52bc6a1-c848-49e6-85de-f8f53459a624.xml
test/data/work/3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml
test/data/work/72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml
test/data/work/80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml
test/data/work/8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578764319.0
musicbrainzngs-0.7.1/musicbrainzngs.egg-info/dependency_links.txt 0000644 0000765 0000024 00000000001 00000000000 026277 0 ustar 00alastair staff 0000000 0000000
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578764319.0
musicbrainzngs-0.7.1/musicbrainzngs.egg-info/top_level.txt 0000644 0000765 0000024 00000000017 00000000000 024761 0 ustar 00alastair staff 0000000 0000000 musicbrainzngs
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1536178072.0
musicbrainzngs-0.7.1/query.py 0000644 0000765 0000024 00000002026 00000000000 017220 0 ustar 00alastair staff 0000000 0000000 import sys
import musicbrainzngs as m
def main():
m.set_useragent("application", "0.01", "http://example.com")
print m.get_artist_by_id("952a4205-023d-4235-897c-6fdb6f58dfaa", [])
#print m.get_label_by_id("aab2e720-bdd2-4565-afc2-460743585f16")
#print m.get_release_by_id("e94757ff-2655-4690-b369-4012beba6114")
#print m.get_release_group_by_id("9377d65d-ffd5-35d6-b64d-43f86ef9188d")
#print m.get_recording_by_id("cb4d4d70-930c-4d1a-a157-776de18be66a")
#print m.get_work_by_id("7e48685c-72dd-3a8b-9274-4777efb2aa75")
#print m.get_releases_by_discid("BG.iuI50.qn1DOBAWIk8fUYoeHM-")
#print m.get_recordings_by_isrc("GBAYE9300106")
m.auth("", "")
#m.submit_barcodes({"e94757ff-2655-4690-b369-4012beba6114": "9421021463277"})
#m.submit_tags(recording_tags={"cb4d4d70-930c-4d1a-a157-776de18be66a":["these", "are", "my", "tags"]})
#m.submit_tags(artist_tags={"952a4205-023d-4235-897c-6fdb6f58dfaa":["NZ", "twee"]})
#m.submit_ratings(recording_ratings={"cb4d4d70-930c-4d1a-a157-776de18be66a":20})
if __name__ == "__main__":
main()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8826716
musicbrainzngs-0.7.1/setup.cfg 0000644 0000765 0000024 00000000046 00000000000 017322 0 ustar 00alastair staff 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1578763725.0
musicbrainzngs-0.7.1/setup.py 0000644 0000765 0000024 00000002130 00000000000 017207 0 ustar 00alastair staff 0000000 0000000 #!/usr/bin/env python
from setuptools import setup
from musicbrainzngs import musicbrainz
with open("README.rst", "r") as fh:
long_description = fh.read()
setup(
name="musicbrainzngs",
version=musicbrainz._version,
description="Python bindings for the MusicBrainz NGS and"
" the Cover Art Archive webservices",
long_description=long_description,
long_description_content_type="text/x-rst",
author="Alastair Porter",
author_email="alastair@porter.net.nz",
url="https://python-musicbrainzngs.readthedocs.io/",
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
packages=['musicbrainzngs'],
license='BSD 2-clause',
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: BSD License",
"License :: OSI Approved :: ISC License (ISCL)",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Database :: Front-Ends",
"Topic :: Software Development :: Libraries :: Python Modules"
]
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8676014
musicbrainzngs-0.7.1/test/ 0000755 0000765 0000024 00000000000 00000000000 016460 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1480710684.0
musicbrainzngs-0.7.1/test/__init__.py 0000644 0000765 0000024 00000000000 00000000000 020557 0 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1494614522.0
musicbrainzngs-0.7.1/test/_common.py 0000644 0000765 0000024 00000004144 00000000000 020464 0 ustar 00alastair staff 0000000 0000000 """Common support for the test cases."""
import time
import musicbrainzngs
from musicbrainzngs import compat
from os.path import join
try:
from urllib2 import OpenerDirector
except ImportError:
from urllib.request import OpenerDirector
from io import BytesIO
try:
import StringIO
except ImportError:
import io as StringIO
class FakeOpener(OpenerDirector):
""" A URL Opener that saves the URL requested and
returns a dummy response or raises an exception """
def __init__(self, response="", exception=None):
self.myurl = None
self.headers = None
self.response = response
self.exception = exception
def open(self, request, body=None):
self.myurl = request.get_full_url()
self.headers = request.header_items()
self.request = request
if self.exception:
raise self.exception
if isinstance(self.response, compat.unicode):
return StringIO.StringIO(self.response)
else:
return BytesIO(self.response)
def get_url(self):
return self.myurl
# Mock timing.
class Timecop(object):
"""Mocks the timing system (namely time() and sleep()) for testing.
Inspired by the Ruby timecop library.
"""
def __init__(self):
self.now = time.time()
def time(self):
return self.now
def sleep(self, amount):
self.now += amount
def install(self):
self.orig = {
'time': time.time,
'sleep': time.sleep,
}
time.time = self.time
time.sleep = self.sleep
def restore(self):
time.time = self.orig['time']
time.sleep = self.orig['sleep']
def open_and_parse_test_data(datadir, filename):
""" Opens an XML file dumped from the MusicBrainz web service and returns
the parses it.
:datadir: The directory containing the file
:filename: The filename of the XML file
:returns: The parsed representation of the XML files content
"""
with open(join(datadir, filename), 'rb') as msg:
res = musicbrainzngs.mbxml.parse_message(msg)
return res
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8695302
musicbrainzngs-0.7.1/test/data/ 0000755 0000765 0000024 00000000000 00000000000 017371 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1578764319.8703475
musicbrainzngs-0.7.1/test/data/artist/ 0000755 0000765 0000024 00000000000 00000000000 020677 5 ustar 00alastair staff 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1360103477.0
musicbrainzngs-0.7.1/test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml 0000644 0000765 0000024 00000004426 00000000000 027715 0 ustar 00alastair staff 0000000 0000000 Сергей Сергеевич ПрокофьевProkofiev, Sergei SergeyevichRussian composerMaleRU1891-04-271953-03-05trueProkofiefProkofieffProkofievProkofiev, SergeiProkofiev, SergejProkovieffS. ProkofievSerge ProkofieffSerge ProkofievSerge ProkofjevSerge ProkofjewSergei ProkofiefSergei ProkofieffSergei ProkofievSergei ProkofjefSergei ProkofjevSergei ProkovievSergei Sergeyevich ProkofievSergej ProkofjevSergej ProkofjewSergej Sergeevič Prokof'evSergey ProkofievSergey Sergeyevich ProkofievSerghei ProkofievSergi ProkofievSergueï ProkofievПрокофьев|Prokofievプロコフィエフ