Showing preview only (540K chars total). Download the full file or copy to clipboard to get everything.
Repository: alastair/python-musicbrainzngs
Branch: master
Commit: 1638c6271e0b
Files: 96
Total size: 506.2 KB
Directory structure:
gitextract_w1w5cf42/
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── CHANGES
├── CONTRIBUTING.md
├── COPYING
├── MANIFEST.in
├── README.rst
├── docs/
│ ├── Makefile
│ ├── api.rst
│ ├── conf.py
│ ├── index.rst
│ ├── installation.rst
│ ├── make.bat
│ └── usage.rst
├── examples/
│ ├── collection.py
│ ├── find_disc.py
│ └── releasesearch.py
├── musicbrainzngs/
│ ├── __init__.py
│ ├── caa.py
│ ├── compat.py
│ ├── mbxml.py
│ ├── musicbrainz.py
│ └── util.py
├── query.py
├── setup.py
├── test/
│ ├── __init__.py
│ ├── _common.py
│ ├── data/
│ │ ├── artist/
│ │ │ ├── 0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml
│ │ │ ├── 2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml
│ │ │ └── b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml
│ │ ├── collection/
│ │ │ ├── 0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml
│ │ │ ├── 20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml
│ │ │ ├── 2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml
│ │ │ ├── 29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml
│ │ │ ├── 855b134e-9a3b-4717-8df8-8c4838d89924-places.xml
│ │ │ └── a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml
│ │ ├── discid/
│ │ │ ├── f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml
│ │ │ └── xp5tz6rE4OHrBafj0bLfDRMGK48-.xml
│ │ ├── event/
│ │ │ ├── 770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml
│ │ │ └── e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml
│ │ ├── instrument/
│ │ │ ├── 01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml
│ │ │ ├── 6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml
│ │ │ ├── 6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml
│ │ │ ├── 9447c0af-5569-48f2-b4c5-241105d58c91.xml
│ │ │ ├── d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml
│ │ │ ├── d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml
│ │ │ └── dabdeb41-560f-4d84-aa6a-cf22349326fe.xml
│ │ ├── label/
│ │ │ ├── 022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml
│ │ │ └── e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml
│ │ ├── place/
│ │ │ ├── 0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml
│ │ │ └── browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml
│ │ ├── recording/
│ │ │ └── f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml
│ │ ├── release/
│ │ │ ├── 212895ca-ee36-439a-a824-d2620cd10461-recordings.xml
│ │ │ ├── 833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml
│ │ │ ├── 8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml
│ │ │ ├── 9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml
│ │ │ ├── a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml
│ │ │ ├── b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml
│ │ │ ├── fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml
│ │ │ └── fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml
│ │ ├── release-group/
│ │ │ └── f52bc6a1-c848-49e6-85de-f8f53459a624.xml
│ │ ├── search-artist.xml
│ │ ├── search-event.xml
│ │ ├── search-instrument.xml
│ │ ├── search-label.xml
│ │ ├── search-place.xml
│ │ ├── search-recording.xml
│ │ ├── search-release-group.xml
│ │ ├── search-release.xml
│ │ ├── search-work.xml
│ │ └── work/
│ │ ├── 3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml
│ │ ├── 72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml
│ │ ├── 80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml
│ │ └── 8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml
│ ├── test_browse.py
│ ├── test_caa.py
│ ├── test_collection.py
│ ├── test_getentity.py
│ ├── test_mbxml.py
│ ├── test_mbxml_artist.py
│ ├── test_mbxml_collection.py
│ ├── test_mbxml_discid.py
│ ├── test_mbxml_event.py
│ ├── test_mbxml_instrument.py
│ ├── test_mbxml_label.py
│ ├── test_mbxml_place.py
│ ├── test_mbxml_recording.py
│ ├── test_mbxml_release.py
│ ├── test_mbxml_release_group.py
│ ├── test_mbxml_search.py
│ ├── test_mbxml_work.py
│ ├── test_ratelimit.py
│ ├── test_requests.py
│ ├── test_search.py
│ └── test_submit.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/tests.yml
================================================
name: Unit tests
on:
- push
- pull_request
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['2.7', '3.7', '3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox tox-gh-actions
- name: Test with tox
run: tox
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
env/
build/
dist/
# Unit test / coverage reports
.tox/
# Sphinx documentation
docs/_build/
================================================
FILE: CHANGES
================================================
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
================================================
FILE: CONTRIBUTING.md
================================================
# Contribute
1. Fork the [repository](https://github.com/alastair/python-musicbrainzngs>)
on Github.
2. Make and test whatever changes you desire.
3. Signoff and commit your changes using ``git commit -s``.
4. Send a pull request.
================================================
FILE: COPYING
================================================
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.
================================================
FILE: MANIFEST.in
================================================
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
================================================
FILE: README.rst
================================================
Musicbrainz NGS bindings
########################
This library implements webservice bindings for the Musicbrainz NGS site, also known as /ws/2
and the `Cover Art Archive <https://coverartarchive.org/>`_.
For more information on the musicbrainz webservice see `<http://wiki.musicbrainz.org/XML_Web_Service>`_.
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 <https://python-musicbrainzngs.readthedocs.org>`_.
Contribute
**********
If you want to contribute to this repository, please read `the
contribution guidelines
<https://github.com/alastair/python-musicbrainzngs/blob/master/CONTRIBUTING.md>`_ first.
Authors
*******
These bindings were written by `Alastair Porter <http://github.com/alastair>`_.
Contributions have been made by:
* `Adrian Sampson <https://github.com/sampsyo>`_
* `Corey Farwell <https://github.com/frewsxcv>`_
* `Galen Hazelwood <https://github.com/galenhz>`_
* `Greg Ward <https://github.com/gward>`_
* `Ian McEwen <https://github.com/ianmcorvidae>`_
* `Jérémie Detrey <https://github.com/jdetrey>`_
* `Johannes Dewender <https://github.com/JonnyJD>`_
* `Michael Marineau <https://github.com/marineam>`_
* `Patrick Speiser <https://github.com/doskir>`_
* `Pavan Chander <https://github.com/navap>`_
* `Paul Bailey <https://github.com/paulbailey>`_
* `Rui Gonçalves <https://github.com/ruippeixotog>`_
* `Ryan Helinski <https://github.com/rlhelinski>`_
* `Sam Doshi <https://github.com/samdoshi>`_
* `Shadab Zafar <https://github.com/dufferzafar>`_
* `Simon Chopin <https://github.com/laarmen>`_
* `Thomas Vander Stichele <https://github.com/thomasvs>`_
* `Wieland Hoffmann <https://github.com/mineo>`_
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.
================================================
FILE: docs/Makefile
================================================
# 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 <target>' where <target> 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."
================================================
FILE: docs/api.rst
================================================
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 entities 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)
================================================
FILE: docs/conf.py
================================================
# -*- 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
# "<project> v<release> 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 <link> 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'
================================================
FILE: docs/index.rst
================================================
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`
================================================
FILE: docs/installation.rst
================================================
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 <http://www.pip-installer.org>`_ as easy as::
pip install musicbrainzngs
Git
---
If you want the latest code or even feel like contributing, the code is
available on `GitHub <https://github.com/alastair/python-musicbrainzngs>`_.
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
================================================
FILE: docs/make.bat
================================================
@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 ^<target^>` where ^<target^> 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
================================================
FILE: docs/usage.rst
================================================
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
<http://musicbrainz.org/doc/XML_Web_Service/Version_2#Identifying_your_application_to_the_MusicBrainz_Web_Service>`_
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
<http://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#Provide_meaningful_User-Agent_strings>`_.
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
automatically 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 <browsing>`_.
Cover Art Data
^^^^^^^^^^^^^^
This library includes a few methods to access data from the `Cover Art Archive
<https://coverartarchive.org/>`_ which has a `documented API
<https://musicbrainz.org/doc/Cover_Art_Archive/API>`_.
Both :func:`musicbrainzngs.get_image_list` and
:func:`musicbrainzngs.get_release_group_image_list` return the deserialized
cover art listing for a `release
<https://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease.2F.7Bmbid.7D.2F>`_
or `release group
<https://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease-group.2F.7Bmbid.7D.2F>`_.
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 possibilities.
More Examples
-------------
You can find some examples for using `musicbrainzngs` in the
`examples directory <https://github.com/alastair/python-musicbrainzngs/tree/master/examples>`_.
================================================
FILE: examples/collection.py
================================================
#!/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 collection.
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()
================================================
FILE: examples/find_disc.py
================================================
#!/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")
================================================
FILE: examples/releasesearch.py
================================================
#!/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()
================================================
FILE: musicbrainzngs/__init__.py
================================================
from musicbrainzngs.musicbrainz import *
from musicbrainzngs.caa import *
================================================
FILE: musicbrainzngs/caa.py
================================================
# 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
<http://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease.2F.7Bmbid.7D.2F>`_
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: The release with an MBID of `releaseid` does not exist or
there is no cover art available for it.
* 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
<http://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease-group.2F.7Bmbid.7D.2F>`_
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: `releasegroupid` is not a valid UUID
* 404: The release group with an MBID of `releasegroupid` does not exist or
there is no cover art available for it.
* 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: The release with an MBID of `releaseid` does not exist or no cover
art with an id of `coverid` exists.
* 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)
================================================
FILE: musicbrainzngs/compat.py
================================================
# -*- 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)
================================================
FILE: musicbrainzngs/mbxml.py
================================================
# 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
<error><text>x</text><text>y</text></error>, 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:
<element>
<subelement>Text</subelement>
</element>
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:
<element>
<subelement>
<a>Foo</a><b>Bar</b>
</subelement>
</element>
and a dictionary {'subelement': parse_subelement},
call parse_subelement(<subelement>) and
return a dict {'subelement': <result>}
if parse_subelement returns a tuple of the form
(True, {'subelement-key': <result>})
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:
<element type="Group" />
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": <text value>} and also an additional element
# containing any xml attributes.
# e.g <attribute value="BuxWV 1">number</attribute>
# -> {"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
<track-count> 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")
================================================
FILE: musicbrainzngs/musicbrainz.py
================================================
# 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")
_max_retries = 8
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", "genres", "user-genres"]
RATING_INCLUDES = ["ratings", "user-ratings"]
VALID_INCLUDES = {
'area' : ["aliases", "annotation"] + RELATION_INCLUDES + TAG_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 + TAG_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", "work-level-rels"] + 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", "audio drama" # 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
AUTH_REQUIRED_INCLUDES = ["user-tags", "user-ratings", "user-genres"]
# 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=_max_retries, 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
a constant (Yes, No, IfSet) for the auth status of the call.
"""
if "user-tags" in includes or "user-ratings" in includes or "user-genres" 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 <auth>` user
as a dict with a 'collection-list' key."""
# Missing <release-list count="n"> 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))
================================================
FILE: musicbrainzngs/util.py
================================================
# 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
================================================
FILE: query.py
================================================
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()
================================================
FILE: setup.py
================================================
#!/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"
]
)
================================================
FILE: test/__init__.py
================================================
================================================
FILE: test/_common.py
================================================
"""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="<response/>", exception=None):
self.myurl = None
self.headers = None
self.response = response
self.exception = exception
self.handlers = []
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
def add_handlers_and_return(self, handlers=[]):
self.handlers.extend(handlers)
return self
# 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
================================================
FILE: test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><artist type="Person" id="0e43fe9d-c472-4b62-be9e-55f971a023e1"><name>Сергей Сергеевич Прокофьев</name><sort-name>Prokofiev, Sergei Sergeyevich</sort-name><disambiguation>Russian composer</disambiguation><gender>Male</gender><country>RU</country><life-span><begin>1891-04-27</begin><end>1953-03-05</end><ended>true</ended></life-span><alias-list count="28"><alias sort-name="Prokofief">Prokofief</alias><alias sort-name="Prokofieff">Prokofieff</alias><alias sort-name="Prokofiev">Prokofiev</alias><alias sort-name="Prokofiev, Sergei">Prokofiev, Sergei</alias><alias sort-name="Prokofiev, Sergej">Prokofiev, Sergej</alias><alias sort-name="Prokovieff">Prokovieff</alias><alias sort-name="S. Prokofiev">S. Prokofiev</alias><alias sort-name="Serge Prokofieff">Serge Prokofieff</alias><alias sort-name="Serge Prokofiev">Serge Prokofiev</alias><alias sort-name="Serge Prokofjev">Serge Prokofjev</alias><alias sort-name="Serge Prokofjew">Serge Prokofjew</alias><alias sort-name="Sergei Prokofief">Sergei Prokofief</alias><alias sort-name="Sergei Prokofieff">Sergei Prokofieff</alias><alias sort-name="Sergei Prokofiev">Sergei Prokofiev</alias><alias sort-name="Sergei Prokofjef">Sergei Prokofjef</alias><alias primary="primary" sort-name="Prokofjev, Sergei" locale="et">Sergei Prokofjev</alias><alias sort-name="Sergei Prokoviev">Sergei Prokoviev</alias><alias primary="primary" sort-name="Prokofiev, Sergei Sergeyevich" locale="en">Sergei Sergeyevich Prokofiev</alias><alias sort-name="Sergej Prokofjev">Sergej Prokofjev</alias><alias sort-name="Sergej Prokofjew">Sergej Prokofjew</alias><alias sort-name="Sergej Sergeevič Prokof'ev">Sergej Sergeevič Prokof'ev</alias><alias sort-name="Sergey Prokofiev">Sergey Prokofiev</alias><alias sort-name="Sergey Sergeyevich Prokofiev">Sergey Sergeyevich Prokofiev</alias><alias sort-name="Serghei Prokofiev">Serghei Prokofiev</alias><alias sort-name="Sergi Prokofiev">Sergi Prokofiev</alias><alias primary="primary" sort-name="Prokofiev, Sergueï" locale="fr">Sergueï Prokofiev</alias><alias sort-name="Прокофьев|Prokofiev">Прокофьев|Prokofiev</alias><alias sort-name="プロコフィエフ">プロコフィエフ</alias></alias-list></artist></metadata>
================================================
FILE: test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><artist type="Group" id="2736bad5-6280-4c8f-92c8-27a5e63bbab2"><name>Errors</name><sort-name>Errors</sort-name><country>GB</country><life-span><begin>2004</begin></life-span></artist></metadata>
================================================
FILE: test/data/artist/b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><artist type="Group" type-id="e431f5f6-b5d2-343d-8b36-72607fffb74b" id="b3785a55-2cf6-497d-b8e3-cfa21a36f997"><name>EXO</name><sort-name>EXO</sort-name><disambiguation>South Korean-Chinese boy group</disambiguation><country>KR</country><area id="b9f7d640-46e8-313e-b158-ded6d18593b3"><name>South Korea</name><sort-name>South Korea</sort-name><iso-3166-1-code-list><iso-3166-1-code>KR</iso-3166-1-code></iso-3166-1-code-list></area><begin-area id="aa03e165-4c73-4959-91c0-a99f9fa8ecab"><name>Seoul</name><sort-name>Seoul</sort-name><iso-3166-2-code-list><iso-3166-2-code>KR-11</iso-3166-2-code></iso-3166-2-code-list></begin-area><life-span><begin>2011-12-23</begin></life-span><relation-list target-type="artist"><relation type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821" type="member of band"><target>143472cb-de19-4da3-b8ac-8a7d01b6638d</target><direction>backward</direction><begin>2011</begin><end>2015</end><ended>true</ended><attribute-list><attribute>original</attribute></attribute-list><artist id="143472cb-de19-4da3-b8ac-8a7d01b6638d"><name>黄子韬</name><sort-name>Huang Zitao</sort-name></artist><target-credit>TAO</target-credit></relation><relation type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821" type="member of band"><target>1fed07c6-adf1-4668-b34b-434ae9741763</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>lead vocals</attribute><attribute>original</attribute></attribute-list><artist id="1fed07c6-adf1-4668-b34b-434ae9741763"><name>D.O.</name><sort-name>D.O.</sort-name><disambiguation>South Korean singer, member of EXO</disambiguation></artist><target-credit>D.O.</target-credit></relation><relation type-id="7802f96b-d995-4ce9-8f70-6366faad758e" type="subgroup"><target>254658f7-f4eb-4c62-bafb-28f57707517b</target><direction>backward</direction><begin>2012</begin><artist id="254658f7-f4eb-4c62-bafb-28f57707517b"><name>EXO-K</name><sort-name>EXO-K</sort-name></artist></relation><relation type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821" type="member of band"><target>2e675c4a-396c-49e8-96b7-a8a72361df84</target><direction>backward</direction><begin>2011</begin><end>2014</end><ended>true</ended><attribute-list><attribute>original</attribute></attribute-list><artist id="2e675c4a-396c-49e8-96b7-a8a72361df84"><name>吴亦凡</name><sort-name>Wu, Yi Fan</sort-name></artist><target-credit>KRIS</target-credit></relation><relation type-id="7802f96b-d995-4ce9-8f70-6366faad758e" type="subgroup"><target>31e909fa-cdf6-4b7d-a7d3-8b928de4e0ba</target><direction>backward</direction><begin>2012</begin><artist id="31e909fa-cdf6-4b7d-a7d3-8b928de4e0ba"><name>EXO-M</name><sort-name>EXO-M</sort-name></artist></relation><relation type="member of band" type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821"><target>36af49c3-7edf-44bf-b040-cf5d9b21ebe7</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="36af49c3-7edf-44bf-b040-cf5d9b21ebe7"><name>시우민</name><sort-name>Xiumin</sort-name></artist><target-credit>XIUMIN</target-credit></relation><relation type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821" type="member of band"><target>439c9247-9291-47a8-8282-7f80bc3f369d</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>lead vocals</attribute><attribute>original</attribute></attribute-list><artist id="439c9247-9291-47a8-8282-7f80bc3f369d"><name>첸</name><sort-name>Chen</sort-name></artist><target-credit>CHEN</target-credit></relation><relation type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821" type="member of band"><target>53be8ba6-8be8-4e3b-8b20-f83c08ecf124</target><direction>backward</direction><begin>2011-12</begin><end>2014-10-10</end><ended>true</ended><attribute-list><attribute>original</attribute></attribute-list><artist id="53be8ba6-8be8-4e3b-8b20-f83c08ecf124"><name>鹿晗</name><sort-name>Lu Han</sort-name></artist><target-credit>LUHAN</target-credit></relation><relation type="member of band" type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821"><target>5d7686b2-90d5-44c6-ab70-693e98506fb6</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="5d7686b2-90d5-44c6-ab70-693e98506fb6"><name>LAY</name><sort-name>LAY</sort-name><disambiguation>EXO</disambiguation></artist></relation><relation type="member of band" type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821"><target>6afff86d-fc4a-4446-a41e-f88e1322a5be</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="6afff86d-fc4a-4446-a41e-f88e1322a5be"><name>카이</name><sort-name>Kai</sort-name><disambiguation>EXO</disambiguation></artist><target-credit>KAI</target-credit></relation><relation type="member of band" type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821"><target>7100af1a-1224-4636-83ad-7f7fcf0973d7</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="7100af1a-1224-4636-83ad-7f7fcf0973d7"><name>찬열</name><sort-name>Chanyeol</sort-name></artist><target-credit>CHANYEOL</target-credit></relation><relation type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821" type="member of band"><target>7593e0e2-fc1c-4855-a645-731c7504e16b</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="7593e0e2-fc1c-4855-a645-731c7504e16b"><name>백현</name><sort-name>Baekhyun</sort-name></artist><target-credit>BAEKHYUN</target-credit></relation><relation type="member of band" type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821"><target>9892db11-4c1b-4029-bed5-6aae508e7fce</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="9892db11-4c1b-4029-bed5-6aae508e7fce"><name>세훈</name><sort-name>Sehun</sort-name></artist><target-credit>SEHUN</target-credit></relation><relation type="member of band" type-id="5be4c609-9afa-4ea0-910b-12ffb71e3821"><target>9aba1d1e-c460-400d-88a7-35f08721d311</target><direction>backward</direction><begin>2011</begin><attribute-list><attribute>original</attribute></attribute-list><artist id="9aba1d1e-c460-400d-88a7-35f08721d311"><name>수호</name><sort-name>Suho</sort-name></artist><target-credit>SUHO</target-credit></relation><relation type-id="7802f96b-d995-4ce9-8f70-6366faad758e" type="subgroup"><target>db095c11-8b25-41b5-adda-d850d9001dcd</target><direction>backward</direction><begin>2016-10</begin><artist id="db095c11-8b25-41b5-adda-d850d9001dcd"><name>EXO-CBX</name><sort-name>EXO-CBX</sort-name></artist></relation></relation-list></artist></metadata>
================================================
FILE: test/data/collection/0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><collection entity-type="release" type="Release" id="0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac"><name>My Collection</name><editor>JonnyJD</editor><release-list count="400"><release id="256b1535-83d8-4245-933b-7d1ed683cbc3"><title>Entities</title><status>Official</status><quality>normal</quality><packaging>Cardboard/Paper Sleeve</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992-10</date><country>DE</country><release-event-list count="1"><release-event><date>1992-10</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>4013859210366</barcode></release><release id="2df7deb7-604e-4861-8e42-79f570d4a22d"><title>German Mystic Sound Sampler, Volume III: Indie-Classics, Volume IV</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992-11-06</date><country>DE</country><release-event-list count="1"><release-event><date>1992-11-06</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>0718759100529</barcode></release><release id="3f831ec6-e8bb-35fa-b6a0-134dabf30b3f"><title>Boys Don’t Cry</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1986-04-29</date><country>DE</country><release-event-list count="1"><release-event><date>1986-04-29</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>042281501128</barcode></release><release id="5aa039be-d3b7-42e6-b805-d066867cb9dd"><title>Zillo Romantic Sound Sampler: Indie Classics, Volume III</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1991</date><country>DE</country><release-event-list count="1"><release-event><date>1991</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>718759100222</barcode></release><release id="63e0d752-5d05-4b16-9b11-02252ce7c599"><title>Bram Stoker's Dracula</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992-11-04</date><country>US</country><release-event-list count="1"><release-event><date>1992-11-04</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>074645316529</barcode></release><release id="7bf735e8-d0aa-35b7-8e05-e1487ed17976"><title>Black Sabbath</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1970</date><country>DE</country><release-event-list count="1"><release-event><date>1970</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode /></release><release id="91481687-d5f4-4d93-ab7a-f25204dc91be"><title>Follow the Blind</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1989</date><country>DE</country><release-event-list count="1"><release-event><date>1989</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list></release><release id="9c3da2f7-f11b-4d9e-a333-ce53038c267c"><title>Live! Exile on Valletta Street</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1991-09-13</date><country>DE</country><release-event-list count="1"><release-event><date>1991-09-13</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>731451117527</barcode></release><release id="a467cfea-68fb-4003-85a3-07ba5d19e0c7"><title>German Mystic Sound Sampler, Volume I: Indie-Classics, Volume I</title><status>Promotion</status><quality>normal</quality><text-representation><language>mul</language><script>Latn</script></text-representation><date>1991-05-17</date><country>DE</country><release-event-list count="1"><release-event><date>1991-05-17</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>4012170902028</barcode></release><release id="a4e06d7f-d0d9-429b-bde0-6ff63ddaa245"><title>Fixed</title><status>Official</status><quality>high</quality><packaging>Digipak</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992-12-07</date><country>US</country><release-event-list count="1"><release-event><date>1992-12-07</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>606949609320</barcode></release><release id="b57c17cd-ba06-4331-ba2e-6b7360925208"><title>Gothic Rock</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992</date><country>GB</country><release-event-list count="1"><release-event><date>1992</date><area id="8a754a16-0027-3a29-b6d7-2b40ea0481ed"><name>United Kingdom</name><sort-name>United Kingdom</sort-name><iso-3166-1-code-list><iso-3166-1-code>GB</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>5013145203828</barcode></release><release id="b8337232-1233-4666-b048-c81aa853d4ce"><title>Burning From the Inside</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1988</date><country>GB</country><release-event-list count="1"><release-event><date>1988</date><area id="8a754a16-0027-3a29-b6d7-2b40ea0481ed"><name>United Kingdom</name><sort-name>United Kingdom</sort-name><iso-3166-1-code-list><iso-3166-1-code>GB</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>5012093004525</barcode></release><release id="bce8246c-5ad0-4578-8a33-f15e49574760"><title>High</title><status>Official</status><quality>normal</quality><packaging>Digipak</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992-02-27</date><country>US</country><release-event-list count="1"><release-event><date>1992-02-27</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>075596643726</barcode></release><release id="c619795b-7426-4bb9-9957-d53c5ab1814b"><title>Methods of Silence</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1989-09-12</date><country>US</country><release-event-list count="1"><release-event><date>1989-09-12</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>075678200229</barcode></release><release id="cc3763b2-55e0-4718-a245-bf410020ef12"><title>Bouquet of Dreams</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1991-08-19</date><country>DE</country><release-event-list count="1"><release-event><date>1991-08-19</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>0718751108523</barcode></release><release id="cd59608e-d847-4391-a813-969ae10101e7"><title>German Mystic Sound Sampler, Volume II: Indie-Classics, Volume II</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1991</date><country>DE</country><release-event-list count="1"><release-event><date>1991</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>0718759100123</barcode></release><release id="d497b726-c646-4302-93e7-22ce52c3d49b"><title>Haus der Lüge</title><status>Official</status><quality>normal</quality><text-representation><language>deu</language><script>Latn</script></text-representation><date>1989</date><country>US</country><release-event-list count="1"><release-event><date>1989</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>023138007123</barcode></release><release id="dc4d63d7-af36-44e6-bab1-49362d389700"><title>Front by Front</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1992</date><country>US</country><release-event-list count="1"><release-event><date>1992</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>074645240626</barcode></release><release id="e0cac4b8-ea17-4c0e-83c0-5934b42e9b32"><title>I: Lieder der Arbeiterklasse & Lieder aus dem spanischen Bürgerkrieg</title><status>Official</status><quality>normal</quality><text-representation><language>deu</language><script>Latn</script></text-representation><date>1989-05-02</date><country>DE</country><release-event-list count="1"><release-event><date>1989-05-02</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>4007198839876</barcode></release><release id="e4f8ec20-6cd3-4f46-a773-b6272b6051fe"><title>Monarchie und Alltag</title><status>Official</status><quality>normal</quality><text-representation><language>deu</language><script>Latn</script></text-representation><date>1980</date><country>DE</country><release-event-list count="1"><release-event><date>1980</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list></release><release id="e8086bec-de5d-3ca3-bfae-c48a1dc672f2"><title>Methods of Silence</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1989-06-05</date><country>DE</country><release-event-list count="1"><release-event><date>1989-06-05</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>042283961326</barcode></release><release id="ed2a7e38-1125-4b19-be16-2ddccff0cdb0"><title>Basically Sad</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1986</date><country>DE</country><release-event-list count="1"><release-event><date>1986</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>042283508224</barcode></release><release id="eed4a1ee-7043-4013-916c-81821a48dd4f"><title>Electro Revenge</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1991</date><country>SE</country><release-event-list count="1"><release-event><date>1991</date><area id="23d10872-f5ae-3f0c-bf55-332788a16ecb"><name>Sweden</name><sort-name>Sweden</sort-name><iso-3166-1-code-list><iso-3166-1-code>SE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>7391946035014</barcode></release><release id="f49e85de-5cda-4981-ba59-49b27e628e6f"><title>Gold und Liebe</title><status>Official</status><quality>normal</quality><text-representation><language>deu</language><script>Latn</script></text-representation><date>1981-11</date><country>DE</country><release-event-list count="1"><release-event><date>1981-11</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list></release><release id="fec34d45-457a-4d21-81c9-21e48bbe3511"><title>Flags of Revolution</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1990</date><country>DE</country><release-event-list count="1"><release-event><date>1990</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list></release></release-list></collection></metadata>
================================================
FILE: test/data/collection/20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><collection entity-type="event" id="20562e36-c7cc-44fb-96b4-486d51a1174b" type="Event"><name>event collection</name><editor>alastairp</editor><event-list count="1"><event id="fe4bce99-6e9b-4050-9681-eac48cdf3199"><name>T on the Fringe 2006</name><life-span><begin>2006-08-04</begin><end>2006-08-30</end></life-span></event></event-list></collection></metadata>
================================================
FILE: test/data/collection/2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><collection id="2326c2e8-be4b-4300-acc6-dbd0adf5645b" type="Work" entity-type="work"><name>work collection</name><editor>alastairp</editor><work-list count="1"><work id="541f07e2-389d-4a0c-8a97-896b6752dd35"><title>Maggot Brain</title></work></work-list></collection></metadata>
================================================
FILE: test/data/collection/29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><collection entity-type="artist" type="Artist" id="29611d8b-b3ad-4ffb-acb5-27f77342a5b0"><name>artist collection</name><editor>alastairp</editor><artist-list count="1"><artist id="0383dadf-2a4e-4d10-a46a-e9e041da8eb3"><name>Queen</name><sort-name>Queen</sort-name><disambiguation>UK rock group</disambiguation><life-span><begin>1970-06-27</begin></life-span></artist></artist-list></collection></metadata>
================================================
FILE: test/data/collection/855b134e-9a3b-4717-8df8-8c4838d89924-places.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><collection entity-type="place" type="Place" id="855b134e-9a3b-4717-8df8-8c4838d89924"><name>place collection</name><editor>alastairp</editor><place-list count="1"><place id="f75dd832-8cac-4068-8432-de0472866d33"><name>San Francisco Bath House</name><disambiguation>aka 'San Fran'</disambiguation><address>171 Cuba Street, Wellington, New Zealand</address></place></place-list></collection></metadata>
================================================
FILE: test/data/collection/a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><collection type="Recording" id="a91320b2-fd2f-4a93-9e4e-603d16d514b6" entity-type="recording"><name>recording collection</name><editor>alastairp</editor><recording-list count="1"><recording id="be462d72-b71b-4cb0-8f65-3434f99822a7"><title>Maggot Brain</title><length>1201000</length></recording></recording-list></collection></metadata>
================================================
FILE: test/data/discid/f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><disc id="f7agNZK1HMQ2WUWq9bwDymw9aHA-"><sectors>217245</sectors><offset-list count="13"><offset position="1">17990</offset><offset position="2">26452</offset><offset position="3">38762</offset><offset position="4">55052</offset><offset position="5">78990</offset><offset position="6">96705</offset><offset position="7">109755</offset><offset position="8">126972</offset><offset position="9">137342</offset><offset position="10">156600</offset><offset position="11">171900</offset><offset position="12">188400</offset><offset position="13">203475</offset></offset-list><release-list count="2"><release id="55a5a355-042f-39d0-9ba0-0de8090c84b9"><title>Geräusch</title><status>Official</status><quality>normal</quality><text-representation><language>deu</language><script>Latn</script></text-representation><date>2003</date><country>AT</country><release-event-list count="1"><release-event><date>2003</date><area id="caac77d1-a5c8-3e6e-8e27-90b44dcc1446"><name>Austria</name><sort-name>Austria</sort-name><iso-3166-1-code-list><iso-3166-1-code>AT</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>0602498655023</barcode><asin>B000UH8BIG</asin><cover-art-archive><artwork>false</artwork><count>0</count><front>false</front><back>false</back></cover-art-archive><medium-list count="2"><medium><title>Schwarzes Geräusch</title><position>1</position><format>CD</format><disc-list count="1"><disc id="f7agNZK1HMQ2WUWq9bwDymw9aHA-"><sectors>217245</sectors><offset-list count="13"><offset position="1">17990</offset><offset position="2">26452</offset><offset position="3">38762</offset><offset position="4">55052</offset><offset position="5">78990</offset><offset position="6">96705</offset><offset position="7">109755</offset><offset position="8">126972</offset><offset position="9">137342</offset><offset position="10">156600</offset><offset position="11">171900</offset><offset position="12">188400</offset><offset position="13">203475</offset></offset-list></disc></disc-list><track-list count="13" /></medium><medium><title>Rotes Geräusch</title><position>2</position><format>CD</format><disc-list count="1"><disc id="EMM4xgOXn40XXFgxCd2hf84lc1Q-"><sectors>215300</sectors><offset-list count="13"><offset position="1">150</offset><offset position="2">18245</offset><offset position="3">34376</offset><offset position="4">45773</offset><offset position="5">62903</offset><offset position="6">85481</offset><offset position="7">102576</offset><offset position="8">120412</offset><offset position="9">139696</offset><offset position="10">156229</offset><offset position="11">174924</offset><offset position="12">184535</offset><offset position="13">192889</offset></offset-list></disc></disc-list><track-list count="13" /></medium></medium-list></release><release id="e6f8f4d8-851c-4f6f-b611-71fa97f9dd5c"><title>Geräusch</title><status>Official</status><quality>normal</quality><packaging>Other</packaging><text-representation><language>deu</language><script>Latn</script></text-representation><date>2003-09-29</date><country>DE</country><release-event-list count="1"><release-event><date>2003-09-29</date><area id="85752fda-13c4-31a3-bee5-0e5cb1f51dad"><name>Germany</name><sort-name>Germany</sort-name><iso-3166-1-code-list><iso-3166-1-code>DE</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>4019593899829</barcode><asin>B0000AN32D</asin><cover-art-archive><artwork>true</artwork><count>3</count><front>true</front><back>true</back></cover-art-archive><medium-list count="2"><medium><title>Schwarzes Geräusch</title><position>1</position><format>CD</format><disc-list count="1"><disc id="f7agNZK1HMQ2WUWq9bwDymw9aHA-"><sectors>217245</sectors><offset-list count="13"><offset position="1">17990</offset><offset position="2">26452</offset><offset position="3">38762</offset><offset position="4">55052</offset><offset position="5">78990</offset><offset position="6">96705</offset><offset position="7">109755</offset><offset position="8">126972</offset><offset position="9">137342</offset><offset position="10">156600</offset><offset position="11">171900</offset><offset position="12">188400</offset><offset position="13">203475</offset></offset-list></disc></disc-list><track-list count="13" /></medium><medium><title>Rotes Geräusch</title><position>2</position><format>CD</format><disc-list count="2"><disc id="EMM4xgOXn40XXFgxCd2hf84lc1Q-"><sectors>215300</sectors><offset-list count="13"><offset position="1">150</offset><offset position="2">18245</offset><offset position="3">34376</offset><offset position="4">45773</offset><offset position="5">62903</offset><offset position="6">85481</offset><offset position="7">102576</offset><offset position="8">120412</offset><offset position="9">139696</offset><offset position="10">156229</offset><offset position="11">174924</offset><offset position="12">184535</offset><offset position="13">192889</offset></offset-list></disc><disc id="j_3_T0_IpgzY05fJpD2cQkg2gaQ-"><sectors>217140</sectors><offset-list count="13"><offset position="1">150</offset><offset position="2">18398</offset><offset position="3">34682</offset><offset position="4">46232</offset><offset position="5">63515</offset><offset position="6">86246</offset><offset position="7">103494</offset><offset position="8">121483</offset><offset position="9">140920</offset><offset position="10">157606</offset><offset position="11">176455</offset><offset position="12">186219</offset><offset position="13">194727</offset></offset-list></disc></disc-list><track-list count="13" /></medium></medium-list></release></release-list></disc></metadata>
================================================
FILE: test/data/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><disc id="xp5tz6rE4OHrBafj0bLfDRMGK48-"><sectors>212075</sectors><offset-list count="8"><offset position="1">182</offset><offset position="2">33322</offset><offset position="3">52597</offset><offset position="4">73510</offset><offset position="5">98882</offset><offset position="6">136180</offset><offset position="7">169185</offset><offset position="8">187490</offset></offset-list><release-list count="3"><release id="68c27a13-97a9-3614-b482-5e6e780bd230"><title>Tales of Ephidrina</title><status>Official</status><quality>normal</quality><packaging>Jewel Case</packaging><text-representation><language>eng</language><script>Latn</script></text-representation><date>1993-07-05</date><country>GB</country><release-event-list count="1"><release-event><date>1993-07-05</date><area id="8a754a16-0027-3a29-b6d7-2b40ea0481ed"><name>United Kingdom</name><sort-name>United Kingdom</sort-name><iso-3166-1-code-list><iso-3166-1-code>GB</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>077778823827</barcode><asin>B000026GLQ</asin><cover-art-archive><artwork>false</artwork><count>0</count><front>false</front><back>false</back></cover-art-archive><medium-list count="1"><medium><position>1</position><format>CD</format><disc-list count="4"><disc id="5sQ7UZcKjJaCH43UKtt_61W7avw-"><sectors>212115</sectors><offset-list count="8"><offset position="1">222</offset><offset position="2">33362</offset><offset position="3">52637</offset><offset position="4">73550</offset><offset position="5">98922</offset><offset position="6">136220</offset><offset position="7">169225</offset><offset position="8">187530</offset></offset-list></disc><disc id="DNlrvGROpc28aJtprTzehV.XE7o-"><sectors>212043</sectors><offset-list count="8"><offset position="1">150</offset><offset position="2">33290</offset><offset position="3">52565</offset><offset position="4">73478</offset><offset position="5">98850</offset><offset position="6">136148</offset><offset position="7">169153</offset><offset position="8">187458</offset></offset-list></disc><disc id="eXuIBrzsHYjtlF_OQgrFxUCg0NA-"><sectors>211912</sectors><offset-list count="8"><offset position="1">150</offset><offset position="2">33150</offset><offset position="3">52428</offset><offset position="4">73340</offset><offset position="5">98715</offset><offset position="6">136015</offset><offset position="7">169015</offset><offset position="8">187323</offset></offset-list></disc><disc id="xp5tz6rE4OHrBafj0bLfDRMGK48-"><sectors>212075</sectors><offset-list count="8"><offset position="1">182</offset><offset position="2">33322</offset><offset position="3">52597</offset><offset position="4">73510</offset><offset position="5">98882</offset><offset position="6">136180</offset><offset position="7">169185</offset><offset position="8">187490</offset></offset-list></disc></disc-list><track-list count="8" /></medium></medium-list></release><release id="6bed9eb1-c7ff-4ddb-ac5d-171e6b335263"><title>Tales of Ephidrina</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1993</date><country>CA</country><release-event-list count="1"><release-event><date>1993</date><area id="71bbafaa-e825-3e15-8ca9-017dcad1748b"><name>Canada</name><sort-name>Canada</sort-name><iso-3166-1-code-list><iso-3166-1-code>CA</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><cover-art-archive><artwork>false</artwork><count>0</count><front>false</front><back>false</back></cover-art-archive><medium-list count="1"><medium><position>1</position><format>CD</format><disc-list count="4"><disc id="5sQ7UZcKjJaCH43UKtt_61W7avw-"><sectors>212115</sectors><offset-list count="8"><offset position="1">222</offset><offset position="2">33362</offset><offset position="3">52637</offset><offset position="4">73550</offset><offset position="5">98922</offset><offset position="6">136220</offset><offset position="7">169225</offset><offset position="8">187530</offset></offset-list></disc><disc id="DNlrvGROpc28aJtprTzehV.XE7o-"><sectors>212043</sectors><offset-list count="8"><offset position="1">150</offset><offset position="2">33290</offset><offset position="3">52565</offset><offset position="4">73478</offset><offset position="5">98850</offset><offset position="6">136148</offset><offset position="7">169153</offset><offset position="8">187458</offset></offset-list></disc><disc id="eXuIBrzsHYjtlF_OQgrFxUCg0NA-"><sectors>211912</sectors><offset-list count="8"><offset position="1">150</offset><offset position="2">33150</offset><offset position="3">52428</offset><offset position="4">73340</offset><offset position="5">98715</offset><offset position="6">136015</offset><offset position="7">169015</offset><offset position="8">187323</offset></offset-list></disc><disc id="xp5tz6rE4OHrBafj0bLfDRMGK48-"><sectors>212075</sectors><offset-list count="8"><offset position="1">182</offset><offset position="2">33322</offset><offset position="3">52597</offset><offset position="4">73510</offset><offset position="5">98882</offset><offset position="6">136180</offset><offset position="7">169185</offset><offset position="8">187490</offset></offset-list></disc></disc-list><track-list count="8" /></medium></medium-list></release><release id="aad0161e-83f7-3468-9816-26528ca3898d"><title>Tales of Ephidrina</title><status>Official</status><quality>normal</quality><text-representation><language>eng</language><script>Latn</script></text-representation><date>1993-07-30</date><country>US</country><release-event-list count="1"><release-event><date>1993-07-30</date><area id="489ce91b-6658-3307-9877-795b68554c98"><name>United States</name><sort-name>United States</sort-name><iso-3166-1-code-list><iso-3166-1-code>US</iso-3166-1-code></iso-3166-1-code-list></area></release-event></release-event-list><barcode>017046610124</barcode><asin>B000003RVA</asin><cover-art-archive><artwork>false</artwork><count>0</count><front>false</front><back>false</back></cover-art-archive><medium-list count="1"><medium><position>1</position><format>CD</format><disc-list count="4"><disc id="5sQ7UZcKjJaCH43UKtt_61W7avw-"><sectors>212115</sectors><offset-list count="8"><offset position="1">222</offset><offset position="2">33362</offset><offset position="3">52637</offset><offset position="4">73550</offset><offset position="5">98922</offset><offset position="6">136220</offset><offset position="7">169225</offset><offset position="8">187530</offset></offset-list></disc><disc id="DNlrvGROpc28aJtprTzehV.XE7o-"><sectors>212043</sectors><offset-list count="8"><offset position="1">150</offset><offset position="2">33290</offset><offset position="3">52565</offset><offset position="4">73478</offset><offset position="5">98850</offset><offset position="6">136148</offset><offset position="7">169153</offset><offset position="8">187458</offset></offset-list></disc><disc id="eXuIBrzsHYjtlF_OQgrFxUCg0NA-"><sectors>211912</sectors><offset-list count="8"><offset position="1">150</offset><offset position="2">33150</offset><offset position="3">52428</offset><offset position="4">73340</offset><offset position="5">98715</offset><offset position="6">136015</offset><offset position="7">169015</offset><offset position="8">187323</offset></offset-list></disc><disc id="xp5tz6rE4OHrBafj0bLfDRMGK48-"><sectors>212075</sectors><offset-list count="8"><offset position="1">182</offset><offset position="2">33322</offset><offset position="3">52597</offset><offset position="4">73510</offset><offset position="5">98882</offset><offset position="6">136180</offset><offset position="7">169185</offset><offset position="8">187490</offset></offset-list></disc></disc-list><track-list count="8" /></medium></medium-list></release></release-list></disc></metadata>
================================================
FILE: test/data/event/770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><event type="Concert" id="770fb0b4-0ad8-4774-9275-099b66627355"><name>1987-06-07: Rock am Ring, Nürburgring, Nürburg, Germany</name><life-span><begin>1987-06-07</begin><end>1987-06-07</end></life-span><relation-list target-type="place"><relation type-id="e2c6f697-07dc-38b1-be0b-83d740165532" type="held at"><target>7643f13a-dcda-4db4-8196-3ffcc1b99ab7</target><place id="7643f13a-dcda-4db4-8196-3ffcc1b99ab7"><name>Nürburgring</name><address>Nürburgring Boulevard 1, 53520 Nürburg</address><coordinates><latitude>50.33556</latitude><longitude>6.9475</longitude></coordinates></place></relation></relation-list></event></metadata>
================================================
FILE: test/data/event/e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><event id="e921686d-ba86-4122-bc3b-777aec90d231" type="Concert"><name>Skunk D.F. @ Sala Arena, Madrid (Gira 20 aniversario)</name><life-span><begin>2014-12-14</begin><end>2014-12-14</end></life-span><time>20:00</time><setlist>* [64de7281-02d7-42a4-a099-9e2927ffca8b|En Noches como Esta]
* [ddcbce3d-721a-4485-8320-e1da5a73cbf3|Cirkus]
* [24ebb1eb-a865-4c5c-b790-17bdf17893c7|Decreto Ley]
* [025608b0-5013-4746-ab77-2919c4aa8285|Estrella de la Muerte]
* [475cbb7f-5a0d-4220-8579-476a03119989|Musa]
* [ffbf45b0-04a1-4a2c-94b5-020e3db1109c|Supernova]
* [38ce45f2-baa3-3a83-8bbe-6691f92120b6|En 5 minutos]
* [3241a14b-6ece-4261-b309-a77695cca246|Muerte y destrucción]
* [1f5fd9f1-76d1-438c-a482-83b62ac0c544|El año del Dragón]
* [8e5c5177-a43c-4562-afe5-e8a40be4856b|Lucha Interior]
* [fb1809bb-0a70-4e39-9276-672deb6a5a31|Mantis]
* [13f6b3ef-77b2-453b-85f7-4bdb13a64fe7|Loto]
* [e40c39cb-ef2e-468d-9e49-7c380029f30a|El Encanto de la Imperfección]
* [2728c7ae-0922-36ed-9b92-707d7aa31f0f|El Cuarto Oscuro]
* [f5c8b0fe-9262-4181-b208-2060a000b840|Última Oportunidad]
* [c44747cc-eedc-42b0-ae0b-d588965e900f|Himen]
* [b104089e-b299-4aa6-a5bc-5d5d0f8e85de|La vida es ahora (acústico)]
* [9c347f02-0494-4f74-a339-078310ce70b5|Crisol]
* [798b281a-687b-478b-a093-2e0406e90551|Anestesia]
* [a941424e-728a-4229-ab0c-4e36ceb3b04c|Alicia]
* [7e901163-6f72-463a-b6f6-215508f880be|Algo Grande]
* [9d9c9ad6-47b2-4478-beca-a9af9ff80de2|Carpe Diem]</setlist><relation-list target-type="artist"><relation type-id="936c7c95-3156-3889-a062-8a0cd57f8946" type="main performer"><target>f9113809-1403-4575-8c20-61bfa96b48db</target><direction>backward</direction><artist id="f9113809-1403-4575-8c20-61bfa96b48db"><name>Skunk D.F.</name><sort-name>Skunk D.F.</sort-name></artist></relation></relation-list><tag-list><tag count="1"><name>redundant-title</name></tag></tag-list></event></metadata>
================================================
FILE: test/data/instrument/01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml
================================================
<?xml version="1.0" encoding="UTF-8"?><metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#"><instrument id="01ba56a2-4306-493d-8088-c7e9b671c74e" type="String instrument"><name>kemenche</name><description>Various types of stringed bowed musical instruments having their origin in the Eastern Mediterranean</description><relation-list target-type="instrument"><relation type-id="12678b88-1adb-3536-890e-9b39b9a14b2d" type="children"><target>04a21d03-535a-4ace-9098-12013867b8e5</target><direction>backward</direction><instrument id="04a21d03-535a-4ace-9098-12013867b8e5"><name>fiddle</name></instrument></relation><relation type="children" type-id="12678b88-1adb-3536-890e-9b39b9a14b2d"><target>ad09a4ed-d1b6-47c3-ac85-acb531244a4d</target><instrument id="ad09a4ed-d1b6-47c3-ac85-acb531244a4d"><name>kemençe of the Black Sea</name><description>Turkish box-shaped kemenche, mainly used for folk music.</description></instrument></relation><relation type="children" type-id="12678b88-1adb-3536-890e-9b39b9a14b2d"><target>b9692581-c117-47f3-9524-3deeb69c6d3f</target><instrument id="b9692581-c117-47f3-9524-3deeb69c6d3f"><name>classical kemençe</name><description>Turkish bowl-shaped kemenche, mainly used in classical Ottoman music</description></instrument></relation></relation-list></instrument></metadata>
====================================
gitextract_w1w5cf42/ ├── .github/ │ └── workflows/ │ └── tests.yml ├── .gitignore ├── CHANGES ├── CONTRIBUTING.md ├── COPYING ├── MANIFEST.in ├── README.rst ├── docs/ │ ├── Makefile │ ├── api.rst │ ├── conf.py │ ├── index.rst │ ├── installation.rst │ ├── make.bat │ └── usage.rst ├── examples/ │ ├── collection.py │ ├── find_disc.py │ └── releasesearch.py ├── musicbrainzngs/ │ ├── __init__.py │ ├── caa.py │ ├── compat.py │ ├── mbxml.py │ ├── musicbrainz.py │ └── util.py ├── query.py ├── setup.py ├── test/ │ ├── __init__.py │ ├── _common.py │ ├── data/ │ │ ├── artist/ │ │ │ ├── 0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml │ │ │ ├── 2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml │ │ │ └── b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml │ │ ├── collection/ │ │ │ ├── 0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml │ │ │ ├── 20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml │ │ │ ├── 2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml │ │ │ ├── 29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml │ │ │ ├── 855b134e-9a3b-4717-8df8-8c4838d89924-places.xml │ │ │ └── a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml │ │ ├── discid/ │ │ │ ├── f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml │ │ │ └── xp5tz6rE4OHrBafj0bLfDRMGK48-.xml │ │ ├── event/ │ │ │ ├── 770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml │ │ │ └── e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml │ │ ├── instrument/ │ │ │ ├── 01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml │ │ │ ├── 6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml │ │ │ ├── 6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml │ │ │ ├── 9447c0af-5569-48f2-b4c5-241105d58c91.xml │ │ │ ├── d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml │ │ │ ├── d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml │ │ │ └── dabdeb41-560f-4d84-aa6a-cf22349326fe.xml │ │ ├── label/ │ │ │ ├── 022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml │ │ │ └── e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml │ │ ├── place/ │ │ │ ├── 0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml │ │ │ └── browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml │ │ ├── recording/ │ │ │ └── f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml │ │ ├── release/ │ │ │ ├── 212895ca-ee36-439a-a824-d2620cd10461-recordings.xml │ │ │ ├── 833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml │ │ │ ├── 8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml │ │ │ ├── 9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml │ │ │ ├── a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml │ │ │ ├── b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml │ │ │ ├── fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml │ │ │ └── fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml │ │ ├── release-group/ │ │ │ └── f52bc6a1-c848-49e6-85de-f8f53459a624.xml │ │ ├── search-artist.xml │ │ ├── search-event.xml │ │ ├── search-instrument.xml │ │ ├── search-label.xml │ │ ├── search-place.xml │ │ ├── search-recording.xml │ │ ├── search-release-group.xml │ │ ├── search-release.xml │ │ ├── search-work.xml │ │ └── work/ │ │ ├── 3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml │ │ ├── 72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml │ │ ├── 80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml │ │ └── 8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml │ ├── test_browse.py │ ├── test_caa.py │ ├── test_collection.py │ ├── test_getentity.py │ ├── test_mbxml.py │ ├── test_mbxml_artist.py │ ├── test_mbxml_collection.py │ ├── test_mbxml_discid.py │ ├── test_mbxml_event.py │ ├── test_mbxml_instrument.py │ ├── test_mbxml_label.py │ ├── test_mbxml_place.py │ ├── test_mbxml_recording.py │ ├── test_mbxml_release.py │ ├── test_mbxml_release_group.py │ ├── test_mbxml_search.py │ ├── test_mbxml_work.py │ ├── test_ratelimit.py │ ├── test_requests.py │ ├── test_search.py │ └── test_submit.py └── tox.ini
SYMBOL INDEX (415 symbols across 30 files)
FILE: examples/collection.py
function show_collections (line 44) | def show_collections():
function show_collection (line 65) | def show_collection(collection_id, ctype):
function show_releases (line 120) | def show_releases(collection):
FILE: examples/find_disc.py
function show_release_details (line 25) | def show_release_details(rel):
function show_offsets (line 36) | def show_offsets(offset_list):
FILE: examples/releasesearch.py
function show_release_details (line 21) | def show_release_details(rel):
FILE: musicbrainzngs/caa.py
function set_caa_hostname (line 22) | def set_caa_hostname(new_hostname, use_https=False):
function _caa_request (line 36) | def _caa_request(mbid, imageid=None, size=None, entitytype="release"):
function get_image_list (line 94) | def get_image_list(releaseid):
function get_release_group_image_list (line 112) | def get_release_group_image_list(releasegroupid):
function get_release_group_image_front (line 130) | def get_release_group_image_front(releasegroupid, size=None):
function get_image_front (line 139) | def get_image_front(releaseid, size=None):
function get_image_back (line 147) | def get_image_back(releaseid, size=None):
function get_image (line 155) | def get_image(mbid, coverid, size=None, entitytype="release"):
FILE: musicbrainzngs/mbxml.py
function fixtag (line 13) | def fixtag(tag, namespaces):
function get_error_message (line 36) | def get_error_message(error):
function make_artist_credit (line 52) | def make_artist_credit(artists):
function parse_elements (line 64) | def parse_elements(valid_els, inner_els, element):
function parse_attributes (line 111) | def parse_attributes(attributes, element):
function parse_message (line 131) | def parse_message(message):
function parse_response_message (line 174) | def parse_response_message(message):
function parse_collection_list (line 177) | def parse_collection_list(cl):
function parse_collection (line 180) | def parse_collection(collection):
function parse_annotation_list (line 195) | def parse_annotation_list(al):
function parse_annotation (line 198) | def parse_annotation(annotation):
function parse_lifespan (line 206) | def parse_lifespan(lifespan):
function parse_area_list (line 211) | def parse_area_list(al):
function parse_area (line 214) | def parse_area(area):
function parse_artist_list (line 231) | def parse_artist_list(al):
function parse_artist (line 234) | def parse_artist(artist):
function parse_coordinates (line 261) | def parse_coordinates(c):
function parse_place_list (line 264) | def parse_place_list(pl):
function parse_place (line 267) | def parse_place(place):
function parse_event_list (line 286) | def parse_event_list(el):
function parse_event (line 289) | def parse_event(event):
function parse_instrument (line 305) | def parse_instrument(instrument):
function parse_label_list (line 318) | def parse_label_list(ll):
function parse_label (line 321) | def parse_label(label):
function parse_relation_target (line 342) | def parse_relation_target(tgt):
function parse_relation_list (line 349) | def parse_relation_list(rl):
function parse_relation (line 355) | def parse_relation(relation):
function parse_relation_attribute_list (line 381) | def parse_relation_attribute_list(attributelist):
function parse_relation_attribute_element (line 387) | def parse_relation_attribute_element(element):
function parse_release (line 403) | def parse_release(release):
function parse_medium_list (line 428) | def parse_medium_list(ml):
function parse_release_event_list (line 446) | def parse_release_event_list(rel):
function parse_release_event (line 449) | def parse_release_event(event):
function parse_medium (line 457) | def parse_medium(medium):
function parse_disc_list (line 468) | def parse_disc_list(dl):
function parse_text_representation (line 471) | def parse_text_representation(textr):
function parse_release_group (line 474) | def parse_release_group(rg):
function parse_recording (line 495) | def parse_recording(recording):
function parse_series_list (line 515) | def parse_series_list(sl):
function parse_series (line 518) | def parse_series(series):
function parse_external_id_list (line 531) | def parse_external_id_list(pl):
function parse_element_list (line 534) | def parse_element_list(el):
function parse_work_list (line 537) | def parse_work_list(wl):
function parse_work (line 540) | def parse_work(work):
function parse_work_attribute_list (line 559) | def parse_work_attribute_list(wal):
function parse_work_attribute (line 562) | def parse_work_attribute(wa):
function parse_url_list (line 573) | def parse_url_list(ul):
function parse_url (line 576) | def parse_url(url):
function parse_disc (line 587) | def parse_disc(disc):
function parse_cdstub (line 600) | def parse_cdstub(cdstub):
function parse_offset_list (line 611) | def parse_offset_list(ol):
function parse_instrument_list (line 614) | def parse_instrument_list(rl):
function parse_release_list (line 620) | def parse_release_list(rl):
function parse_release_group_list (line 626) | def parse_release_group_list(rgl):
function parse_isrc (line 632) | def parse_isrc(isrc):
function parse_recording_list (line 642) | def parse_recording_list(recs):
function parse_artist_credit (line 648) | def parse_artist_credit(ac):
function parse_name_credit (line 657) | def parse_name_credit(nc):
function parse_label_info_list (line 666) | def parse_label_info_list(lil):
function parse_label_info (line 673) | def parse_label_info(li):
function parse_track_list (line 681) | def parse_track_list(tl):
function parse_track (line 687) | def parse_track(track):
function parse_tag_list (line 710) | def parse_tag_list(tl):
function parse_tag (line 713) | def parse_tag(tag):
function parse_rating (line 723) | def parse_rating(rating):
function parse_alias_list (line 732) | def parse_alias_list(al):
function parse_alias (line 735) | def parse_alias(alias):
function parse_caa (line 745) | def parse_caa(caa_element):
function make_barcode_request (line 755) | def make_barcode_request(release2barcode):
function make_tag_request (line 767) | def make_tag_request(**kwargs):
function make_rating_request (line 787) | def make_rating_request(**kwargs):
function make_isrc_request (line 804) | def make_isrc_request(recording2isrcs):
FILE: musicbrainzngs/musicbrainz.py
class AUTH_YES (line 167) | class AUTH_YES: pass
class AUTH_NO (line 168) | class AUTH_NO: pass
class AUTH_IFSET (line 169) | class AUTH_IFSET: pass
class MusicBrainzError (line 177) | class MusicBrainzError(Exception):
class UsageError (line 181) | class UsageError(MusicBrainzError):
class InvalidSearchFieldError (line 185) | class InvalidSearchFieldError(UsageError):
class InvalidIncludeError (line 188) | class InvalidIncludeError(UsageError):
method __init__ (line 189) | def __init__(self, msg='Invalid Includes', reason=None):
method __str__ (line 194) | def __str__(self):
class InvalidFilterError (line 197) | class InvalidFilterError(UsageError):
method __init__ (line 198) | def __init__(self, msg='Invalid Includes', reason=None):
method __str__ (line 203) | def __str__(self):
class WebServiceError (line 206) | class WebServiceError(MusicBrainzError):
method __init__ (line 208) | def __init__(self, message=None, cause=None):
method __str__ (line 215) | def __str__(self):
class NetworkError (line 223) | class NetworkError(WebServiceError):
class ResponseError (line 227) | class ResponseError(WebServiceError):
class AuthenticationError (line 231) | class AuthenticationError(WebServiceError):
function _check_includes_impl (line 238) | def _check_includes_impl(includes, valid_includes):
function _check_includes (line 243) | def _check_includes(entity, inc):
function _check_filter (line 246) | def _check_filter(values, valid):
function _check_filter_and_make_params (line 251) | def _check_filter_and_make_params(entity, includes, release_status=[], r...
function _docstring_get (line 280) | def _docstring_get(entity):
function _docstring_browse (line 284) | def _docstring_browse(entity):
function _docstring_search (line 288) | def _docstring_search(entity):
function _docstring_impl (line 292) | def _docstring_impl(name, values):
function auth (line 311) | def auth(u, p):
function set_useragent (line 319) | def set_useragent(app, version, contact=None):
function set_hostname (line 333) | def set_hostname(new_hostname, use_https=False):
function set_rate_limit (line 354) | def set_rate_limit(limit_or_interval=1.0, new_requests=1):
class _rate_limit (line 376) | class _rate_limit(object):
method __init__ (line 384) | def __init__(self, fun):
method _update_remaining (line 390) | def _update_remaining(self):
method __call__ (line 408) | def __call__(self, *args, **kwargs):
class _RedirectPasswordMgr (line 424) | class _RedirectPasswordMgr(compat.HTTPPasswordMgr):
method __init__ (line 425) | def __init__(self):
method find_user_password (line 428) | def find_user_password(self, realm, uri):
method add_password (line 435) | def add_password(self, realm, uri, username, password):
class _DigestAuthHandler (line 439) | class _DigestAuthHandler(compat.HTTPDigestAuthHandler):
method get_authorization (line 440) | def get_authorization (self, req, chal):
method _encode_utf8 (line 447) | def _encode_utf8(self, msg):
method get_algorithm_impls (line 458) | def get_algorithm_impls(self, algorithm):
class _MusicbrainzHttpRequest (line 470) | class _MusicbrainzHttpRequest(compat.Request):
method __init__ (line 472) | def __init__(self, method, url, data=None):
method get_method (line 479) | def get_method(self):
function _safe_read (line 485) | def _safe_read(opener, req, body=None, max_retries=_max_retries, retry_d...
function mb_parser_null (line 553) | def mb_parser_null(resp):
function mb_parser_xml (line 557) | def mb_parser_xml(resp):
function set_parser (line 574) | def set_parser(new_parser_fun=None):
function set_format (line 588) | def set_format(fmt="xml"):
function _mb_request (line 612) | def _mb_request(path, method='GET', auth_required=AUTH_NO,
function _get_auth_type (line 699) | def _get_auth_type(entity, id, includes):
function _do_mb_query (line 714) | def _do_mb_query(entity, id, includes=[], params={}):
function _do_mb_search (line 736) | def _do_mb_search(entity, query='', fields={},
function _do_mb_delete (line 790) | def _do_mb_delete(path):
function _do_mb_put (line 795) | def _do_mb_put(path):
function _do_mb_post (line 800) | def _do_mb_post(path, body):
function get_area_by_id (line 812) | def get_area_by_id(id, includes=[], release_status=[], release_type=[]):
function get_artist_by_id (line 821) | def get_artist_by_id(id, includes=[], release_status=[], release_type=[]):
function get_instrument_by_id (line 830) | def get_instrument_by_id(id, includes=[], release_status=[], release_typ...
function get_label_by_id (line 839) | def get_label_by_id(id, includes=[], release_status=[], release_type=[]):
function get_place_by_id (line 848) | def get_place_by_id(id, includes=[], release_status=[], release_type=[]):
function get_event_by_id (line 857) | def get_event_by_id(id, includes=[], release_status=[], release_type=[]):
function get_recording_by_id (line 869) | def get_recording_by_id(id, includes=[], release_status=[], release_type...
function get_release_by_id (line 879) | def get_release_by_id(id, includes=[], release_status=[], release_type=[]):
function get_release_group_by_id (line 888) | def get_release_group_by_id(id, includes=[],
function get_series_by_id (line 899) | def get_series_by_id(id, includes=[]):
function get_work_by_id (line 906) | def get_work_by_id(id, includes=[]):
function get_url_by_id (line 913) | def get_url_by_id(id, includes=[]):
function search_annotations (line 923) | def search_annotations(query='', limit=None, offset=None, strict=False, ...
function search_areas (line 930) | def search_areas(query='', limit=None, offset=None, strict=False, **fiel...
function search_artists (line 937) | def search_artists(query='', limit=None, offset=None, strict=False, **fi...
function search_events (line 944) | def search_events(query='', limit=None, offset=None, strict=False, **fie...
function search_instruments (line 951) | def search_instruments(query='', limit=None, offset=None, strict=False, ...
function search_labels (line 958) | def search_labels(query='', limit=None, offset=None, strict=False, **fie...
function search_places (line 965) | def search_places(query='', limit=None, offset=None, strict=False, **fie...
function search_recordings (line 972) | def search_recordings(query='', limit=None, offset=None,
function search_releases (line 980) | def search_releases(query='', limit=None, offset=None, strict=False, **f...
function search_release_groups (line 987) | def search_release_groups(query='', limit=None, offset=None,
function search_series (line 996) | def search_series(query='', limit=None, offset=None, strict=False, **fie...
function search_works (line 1003) | def search_works(query='', limit=None, offset=None, strict=False, **fiel...
function get_releases_by_discid (line 1012) | def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, medi...
function get_recordings_by_isrc (line 1048) | def get_recordings_by_isrc(isrc, includes=[], release_status=[],
function get_works_by_iswc (line 1060) | def get_works_by_iswc(iswc, includes=[]):
function _browse_impl (line 1068) | def _browse_impl(entity, includes, limit, offset, params, release_status...
function browse_artists (line 1088) | def browse_artists(recording=None, release=None, release_group=None,
function browse_events (line 1101) | def browse_events(area=None, artist=None, place=None,
function browse_labels (line 1113) | def browse_labels(release=None, includes=[], limit=None, offset=None):
function browse_places (line 1121) | def browse_places(area=None, includes=[], limit=None, offset=None):
function browse_recordings (line 1129) | def browse_recordings(artist=None, release=None, includes=[],
function browse_releases (line 1140) | def browse_releases(artist=None, track_artist=None, label=None, recordin...
function browse_release_groups (line 1163) | def browse_release_groups(artist=None, release=None, release_type=[],
function browse_urls (line 1177) | def browse_urls(resource=None, includes=[], limit=None, offset=None):
function browse_works (line 1186) | def browse_works(artist=None, includes=[], limit=None, offset=None):
function get_collections (line 1194) | def get_collections():
function _do_collection_query (line 1200) | def _do_collection_query(collection, collection_type, limit, offset):
function get_artists_in_collection (line 1206) | def get_artists_in_collection(collection, limit=None, offset=None):
function get_releases_in_collection (line 1214) | def get_releases_in_collection(collection, limit=None, offset=None):
function get_events_in_collection (line 1222) | def get_events_in_collection(collection, limit=None, offset=None):
function get_places_in_collection (line 1230) | def get_places_in_collection(collection, limit=None, offset=None):
function get_recordings_in_collection (line 1238) | def get_recordings_in_collection(collection, limit=None, offset=None):
function get_works_in_collection (line 1246) | def get_works_in_collection(collection, limit=None, offset=None):
function submit_barcodes (line 1257) | def submit_barcodes(release_barcode):
function submit_isrcs (line 1263) | def submit_isrcs(recording_isrcs):
function submit_tags (line 1274) | def submit_tags(**kwargs):
function submit_ratings (line 1293) | def submit_ratings(**kwargs):
function add_releases_to_collection (line 1305) | def add_releases_to_collection(collection, releases=[]):
function remove_releases_from_collection (line 1313) | def remove_releases_from_collection(collection, releases=[]):
FILE: musicbrainzngs/util.py
function _unicode (line 12) | def _unicode(string, encoding=None):
function bytes_to_elementtree (line 30) | def bytes_to_elementtree(bytes_or_file):
FILE: query.py
function main (line 5) | def main():
FILE: test/_common.py
class FakeOpener (line 20) | class FakeOpener(OpenerDirector):
method __init__ (line 23) | def __init__(self, response="<response/>", exception=None):
method open (line 30) | def open(self, request, body=None):
method get_url (line 43) | def get_url(self):
method add_handlers_and_return (line 46) | def add_handlers_and_return(self, handlers=[]):
class Timecop (line 52) | class Timecop(object):
method __init__ (line 56) | def __init__(self):
method time (line 59) | def time(self):
method sleep (line 62) | def sleep(self, amount):
method install (line 65) | def install(self):
method restore (line 73) | def restore(self):
function open_and_parse_test_data (line 77) | def open_and_parse_test_data(datadir, filename):
FILE: test/test_browse.py
class BrowseTest (line 7) | class BrowseTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method tearDown (line 16) | def tearDown(self):
method test_browse (line 19) | def test_browse(self):
method test_browse_includes (line 24) | def test_browse_includes(self):
method test_browse_single_include (line 29) | def test_browse_single_include(self):
method test_browse_multiple_by (line 34) | def test_browse_multiple_by(self):
method test_browse_limit_offset (line 39) | def test_browse_limit_offset(self):
method test_browse_artist (line 45) | def test_browse_artist(self):
method test_browse_event (line 62) | def test_browse_event(self):
method test_browse_label (line 75) | def test_browse_label(self):
method test_browse_recording (line 80) | def test_browse_recording(self):
method test_browse_place (line 89) | def test_browse_place(self):
method test_browse_release (line 94) | def test_browse_release(self):
method test_browse_release_group (line 113) | def test_browse_release_group(self):
method test_browse_url (line 127) | def test_browse_url(self):
method test_browse_work (line 137) | def test_browse_work(self):
method test_browse_includes_is_subset_of_includes (line 142) | def test_browse_includes_is_subset_of_includes(self):
FILE: test/test_caa.py
class CaaTest (line 10) | class CaaTest(unittest.TestCase):
method test_get_list (line 12) | def test_get_list(self):
method test_get_release_group_list (line 22) | def test_get_release_group_list(self):
method test_list_none (line 33) | def test_list_none(self):
method test_list_baduuid (line 45) | def test_list_baduuid(self):
method test_set_useragent (line 55) | def test_set_useragent(self):
method test_coverid (line 68) | def test_coverid(self):
method test_get_size (line 77) | def test_get_size(self):
method test_front (line 86) | def test_front(self):
method test_release_group_front (line 95) | def test_release_group_front(self):
method test_back (line 104) | def test_back(self):
FILE: test/test_collection.py
class CollectionTest (line 7) | class CollectionTest(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method tearDown (line 14) | def tearDown(self):
method test_auth_required (line 17) | def test_auth_required(self):
method test_my_collections (line 35) | def test_my_collections(self):
method test_other_collection (line 54) | def test_other_collection(self):
method test_no_collection (line 78) | def test_no_collection(self):
method test_private_collection (line 90) | def test_private_collection(self):
FILE: test/test_getentity.py
class UrlTest (line 6) | class UrlTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method tearDown (line 16) | def tearDown(self):
method testGetArtist (line 19) | def testGetArtist(self):
method testGetEvent (line 43) | def testGetEvent(self):
method testGetPlace (line 55) | def testGetPlace(self):
method testGetLabel (line 63) | def testGetLabel(self):
method testGetRecording (line 78) | def testGetRecording(self):
method testGetReleasegroup (line 87) | def testGetReleasegroup(self):
method testGetWork (line 108) | def testGetWork(self):
method testGetByDiscid (line 112) | def testGetByDiscid(self):
method testGetInstrument (line 127) | def testGetInstrument(self):
FILE: test/test_mbxml.py
class MbXML (line 5) | class MbXML(unittest.TestCase):
method testMakeBarcode (line 7) | def testMakeBarcode(self):
method test_make_tag_request (line 14) | def test_make_tag_request(self):
method test_read_error (line 25) | def test_read_error(self):
FILE: test/test_mbxml_artist.py
class GetArtistTest (line 8) | class GetArtistTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method testArtistAliases (line 12) | def testArtistAliases(self):
method testArtistTargets (line 30) | def testArtistTargets(self):
FILE: test/test_mbxml_collection.py
class UrlTest (line 9) | class UrlTest(unittest.TestCase):
method setUp (line 12) | def setUp(self):
method tearDown (line 19) | def tearDown(self):
method testGetCollection (line 22) | def testGetCollection(self):
class GetCollectionTest (line 42) | class GetCollectionTest(unittest.TestCase):
method setUp (line 43) | def setUp(self):
method testCollectionInfo (line 46) | def testCollectionInfo(self):
method testCollectionReleases (line 52) | def testCollectionReleases(self):
method testCollectionWorks (line 66) | def testCollectionWorks(self):
method testCollectionArtists (line 77) | def testCollectionArtists(self):
method testCollectionEvents (line 88) | def testCollectionEvents(self):
method testCollectionPlaces (line 99) | def testCollectionPlaces(self):
method testCollectionRecordings (line 110) | def testCollectionRecordings(self):
FILE: test/test_mbxml_discid.py
class UrlTest (line 9) | class UrlTest(unittest.TestCase):
method setUp (line 12) | def setUp(self):
method tearDown (line 19) | def tearDown(self):
method testGetDiscId (line 22) | def testGetDiscId(self):
class GetDiscIdTest (line 37) | class GetDiscIdTest(unittest.TestCase):
method setUp (line 38) | def setUp(self):
method testDiscId (line 41) | def testDiscId(self):
method testTrackCount (line 48) | def testTrackCount(self):
method testOffsets (line 62) | def testOffsets(self):
method testReleaseList (line 73) | def testReleaseList(self):
FILE: test/test_mbxml_event.py
class EventTest (line 8) | class EventTest(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method testCorrectId (line 13) | def testCorrectId(self):
method testPlace (line 18) | def testPlace(self):
method testType (line 26) | def testType(self):
method testEventElements (line 31) | def testEventElements(self):
FILE: test/test_mbxml_instrument.py
class GetInstrumentTest (line 9) | class GetInstrumentTest(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method testData (line 13) | def testData(self):
method testAliases (line 22) | def testAliases(self):
method testTags (line 35) | def testTags(self):
method testUrlRels (line 44) | def testUrlRels(self):
method testAnnotations (line 54) | def testAnnotations(self):
method testInstrumentRels (line 59) | def testInstrumentRels(self):
method testDisambiguation (line 71) | def testDisambiguation(self):
FILE: test/test_mbxml_label.py
class GetLabelTest (line 8) | class GetLabelTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method testLabelAliases (line 12) | def testLabelAliases(self):
FILE: test/test_mbxml_place.py
class PlaceTest (line 8) | class PlaceTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method testPlace (line 12) | def testPlace(self):
method testListFromBrowse (line 27) | def testListFromBrowse(self):
FILE: test/test_mbxml_recording.py
class GetRecordingTest (line 9) | class GetRecordingTest(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method testRecordingRelationCreditedAs (line 13) | def testRecordingRelationCreditedAs(self):
FILE: test/test_mbxml_release.py
class UrlTest (line 9) | class UrlTest(unittest.TestCase):
method setUp (line 12) | def setUp(self):
method tearDown (line 19) | def tearDown(self):
method testGetRelease (line 22) | def testGetRelease(self):
class GetReleaseTest (line 36) | class GetReleaseTest(unittest.TestCase):
method setUp (line 37) | def setUp(self):
method testArtistCredit (line 40) | def testArtistCredit(self):
method testTrackId (line 62) | def testTrackId(self):
method testTrackLength (line 70) | def testTrackLength(self):
method testTrackTitle (line 102) | def testTrackTitle(self):
method testTrackNumber (line 105) | def testTrackNumber(self):
method testVideo (line 127) | def testVideo(self):
method testPregapTrack (line 137) | def testPregapTrack(self):
method testDataTracklist (line 149) | def testDataTracklist(self):
FILE: test/test_mbxml_release_group.py
class GetReleaseGroupTest (line 8) | class GetReleaseGroupTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method testTypesExist (line 13) | def testTypesExist(self):
method testTypesResult (line 21) | def testTypesResult(self):
FILE: test/test_mbxml_search.py
class SearchArtistTest (line 9) | class SearchArtistTest(unittest.TestCase):
method testFields (line 10) | def testFields(self):
class SearchReleaseTest (line 23) | class SearchReleaseTest(unittest.TestCase):
method testFields (line 24) | def testFields(self):
class SearchReleaseGroupTest (line 39) | class SearchReleaseGroupTest(unittest.TestCase):
method testFields (line 40) | def testFields(self):
class SearchWorkTest (line 50) | class SearchWorkTest(unittest.TestCase):
method testFields (line 51) | def testFields(self):
class SearchLabelTest (line 61) | class SearchLabelTest(unittest.TestCase):
method testFields (line 62) | def testFields(self):
class SearchRecordingTest (line 72) | class SearchRecordingTest(unittest.TestCase):
method testFields (line 73) | def testFields(self):
class SearchInstrumentTest (line 83) | class SearchInstrumentTest(unittest.TestCase):
method testFields (line 84) | def testFields(self):
class SearchPlaceTest (line 96) | class SearchPlaceTest(unittest.TestCase):
method testFields (line 97) | def testFields(self):
class SearchEventTest (line 110) | class SearchEventTest(unittest.TestCase):
method testFields (line 111) | def testFields(self):
FILE: test/test_mbxml_work.py
class GetWorkTest (line 9) | class GetWorkTest(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method testWorkAliases (line 13) | def testWorkAliases(self):
method testWorkAttributes (line 35) | def testWorkAttributes(self):
method testWorkRelationAttributes (line 54) | def testWorkRelationAttributes(self):
FILE: test/test_ratelimit.py
class RateLimitArgumentTest (line 8) | class RateLimitArgumentTest(unittest.TestCase):
method test_invalid_args (line 9) | def test_invalid_args(self):
class RateLimitingTest (line 36) | class RateLimitingTest(unittest.TestCase):
method setUp (line 37) | def setUp(self):
method tearDown (line 46) | def tearDown(self):
method test_do_not_wait_initially (line 49) | def test_do_not_wait_initially(self):
method test_second_rapid_query_waits (line 55) | def test_second_rapid_query_waits(self):
method test_second_distant_query_does_not_wait (line 63) | def test_second_distant_query_does_not_wait(self):
class BatchedRateLimitingTest (line 72) | class BatchedRateLimitingTest(unittest.TestCase):
method setUp (line 73) | def setUp(self):
method tearDown (line 84) | def tearDown(self):
method test_initial_rapid_queries_not_delayed (line 89) | def test_initial_rapid_queries_not_delayed(self):
method test_overage_query_delayed (line 97) | def test_overage_query_delayed(self):
class NoRateLimitingTest (line 106) | class NoRateLimitingTest(unittest.TestCase):
method setUp (line 108) | def setUp(self):
method tearDown (line 119) | def tearDown(self):
method test_initial_rapid_queries_not_delayed (line 124) | def test_initial_rapid_queries_not_delayed(self):
FILE: test/test_requests.py
class ArgumentTest (line 7) | class ArgumentTest(unittest.TestCase):
method setUp (line 11) | def setUp(self):
method tearDown (line 16) | def tearDown(self):
method test_no_client (line 19) | def test_no_client(self):
method test_client (line 24) | def test_client(self):
method test_false_useragent (line 29) | def test_false_useragent(self):
method test_missing_auth (line 35) | def test_missing_auth(self):
method test_missing_useragent (line 42) | def test_missing_useragent(self):
method test_auth_headers (line 47) | def test_auth_headers(self):
method test_auth_headers_ifset (line 53) | def test_auth_headers_ifset(self):
method test_auth_headers_ifset_no_user (line 59) | def test_auth_headers_ifset_no_user(self):
class MethodTest (line 67) | class MethodTest(unittest.TestCase):
method setUp (line 71) | def setUp(self):
method tearDown (line 78) | def tearDown(self):
method test_invalid_method (line 81) | def test_invalid_method(self):
method test_delete (line 85) | def test_delete(self):
method test_put (line 89) | def test_put(self):
method test_post (line 93) | def test_post(self):
method test_get (line 97) | def test_get(self):
class HostnameTest (line 102) | class HostnameTest(unittest.TestCase):
method setUp (line 105) | def setUp(self):
method tearDown (line 110) | def tearDown(self):
method test_default_musicbrainz_https (line 114) | def test_default_musicbrainz_https(self):
method test_set_http (line 118) | def test_set_http(self):
method test_set_https (line 124) | def test_set_https(self):
method test_set_port (line 130) | def test_set_port(self):
FILE: test/test_search.py
class SearchUrlTest (line 7) | class SearchUrlTest(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method tearDown (line 17) | def tearDown(self):
method test_search_annotations (line 20) | def test_search_annotations(self):
method test_search_artists (line 36) | def test_search_artists(self):
method test_search_events (line 49) | def test_search_events(self):
method test_search_labels (line 62) | def test_search_labels(self):
method test_search_places (line 75) | def test_search_places(self):
method test_search_releases (line 88) | def test_search_releases(self):
method test_search_release_groups (line 101) | def test_search_release_groups(self):
method test_search_recordings (line 114) | def test_search_recordings(self):
method test_search_works (line 127) | def test_search_works(self):
FILE: test/test_submit.py
class SubmitTest (line 7) | class SubmitTest(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method tearDown (line 15) | def tearDown(self):
method test_submit_tags (line 23) | def test_submit_tags(self):
method test_submit_single_tag (line 34) | def test_submit_single_tag(self):
Condensed preview — 96 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (548K chars).
[
{
"path": ".github/workflows/tests.yml",
"chars": 560,
"preview": "name: Unit tests\n\non:\n - push\n - pull_request\n\njobs:\n build:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n "
},
{
"path": ".gitignore",
"chars": 195,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# Distribution / packaging\nenv/\nbuild/\ndist/\n"
},
{
"path": "CHANGES",
"chars": 5614,
"preview": "0.7.1 (2020-01-11):\n * include README file in pypi\n\n0.7 (2020-01-09):\n * removed support for PUIDs and Echoprint ("
},
{
"path": "CONTRIBUTING.md",
"chars": 235,
"preview": "# Contribute\n\n1. Fork the [repository](https://github.com/alastair/python-musicbrainzngs>)\n on Github.\n2. Make and tes"
},
{
"path": "COPYING",
"chars": 2116,
"preview": "Copyright 2011 Alastair Porter, Adrian Sampson, and others.\nAll rights reserved.\n\nRedistribution and use in source and b"
},
{
"path": "MANIFEST.in",
"chars": 322,
"preview": "include COPYING README.rst CHANGES query.py\nrecursive-include test *.py\nrecursive-include test/data *.xml\ninclude test/d"
},
{
"path": "README.rst",
"chars": 2444,
"preview": "Musicbrainz NGS bindings\n########################\n\nThis library implements webservice bindings for the Musicbrainz NGS s"
},
{
"path": "docs/Makefile",
"chars": 5679,
"preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS =\nSPHINXBUILD "
},
{
"path": "docs/api.rst",
"chars": 5803,
"preview": "API\n~~~\n.. module:: musicbrainzngs\n\nThis is a shallow python binding of the MusicBrainz web service\nso you should read\n:"
},
{
"path": "docs/conf.py",
"chars": 8865,
"preview": "# -*- coding: utf-8 -*-\n#\n# musicbrainzngs documentation build configuration file, created by\n# sphinx-quickstart2 on Th"
},
{
"path": "docs/index.rst",
"chars": 641,
"preview": "musicbrainzngs |release|\n========================\n\n`musicbrainzngs` implements Python bindings of the `MusicBrainz Web S"
},
{
"path": "docs/installation.rst",
"chars": 874,
"preview": "Installation\n~~~~~~~~~~~~\n\nPackage manager\n---------------\n\nIf you want the latest stable version of musicbrainzngs, the"
},
{
"path": "docs/make.bat",
"chars": 5113,
"preview": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build2\n)\nset BU"
},
{
"path": "docs/usage.rst",
"chars": 8424,
"preview": "Usage\n~~~~~\n\nIn general you need to set a useragent for your application,\nstart searches to get to know corresponding Mu"
},
{
"path": "examples/collection.py",
"chars": 7168,
"preview": "#!/usr/bin/env python\n\"\"\"View and modify your MusicBrainz collections.\n\nTo show a list of your collections:\n\n $ ./col"
},
{
"path": "examples/find_disc.py",
"chars": 2631,
"preview": "#!/usr/bin/env python\n\"\"\"A script that looks for a release in the MusicBrainz database by disc ID\n\n $ ./find_disc.py "
},
{
"path": "examples/releasesearch.py",
"chars": 1960,
"preview": "#!/usr/bin/env python\n\"\"\"A simple script that searches for a release in the MusicBrainz\ndatabase and prints out a few de"
},
{
"path": "musicbrainzngs/__init__.py",
"chars": 74,
"preview": "from musicbrainzngs.musicbrainz import *\nfrom musicbrainzngs.caa import *\n"
},
{
"path": "musicbrainzngs/caa.py",
"chars": 6779,
"preview": "# This file is part of the musicbrainzngs library\n# Copyright (C) Alastair Porter, Wieland Hoffmann, and others\n# This f"
},
{
"path": "musicbrainzngs/compat.py",
"chars": 1719,
"preview": "# -*- coding: utf-8 -*-\n# Copyright (c) 2012 Kenneth Reitz.\n\n# Permission to use, copy, modify, and/or distribute this s"
},
{
"path": "musicbrainzngs/mbxml.py",
"chars": 28151,
"preview": "# This file is part of the musicbrainzngs library\n# Copyright (C) Alastair Porter, Adrian Sampson, and others\n# This fil"
},
{
"path": "musicbrainzngs/musicbrainz.py",
"chars": 48215,
"preview": "# This file is part of the musicbrainzngs library\n# Copyright (C) Alastair Porter, Adrian Sampson, and others\n# This fil"
},
{
"path": "musicbrainzngs/util.py",
"chars": 1428,
"preview": "# This file is part of the musicbrainzngs library\n# Copyright (C) Alastair Porter, Adrian Sampson, and others\n# This fil"
},
{
"path": "query.py",
"chars": 1046,
"preview": "import sys\n\nimport musicbrainzngs as m\n\ndef main():\n\tm.set_useragent(\"application\", \"0.01\", \"http://example.com\")\n\tprint"
},
{
"path": "setup.py",
"chars": 1112,
"preview": "#!/usr/bin/env python\n\nfrom setuptools import setup\n\nfrom musicbrainzngs import musicbrainz\n\nwith open(\"README.rst\", \"r\""
},
{
"path": "test/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "test/_common.py",
"chars": 2287,
"preview": "\"\"\"Common support for the test cases.\"\"\"\nimport time\n\nimport musicbrainzngs\nfrom musicbrainzngs import compat\nfrom os.pa"
},
{
"path": "test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml",
"chars": 2252,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><artist type=\"Person\" id=\"0e4"
},
{
"path": "test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml",
"chars": 285,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><artist type=\"Group\" id=\"2736"
},
{
"path": "test/data/artist/b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml",
"chars": 6762,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><artist type=\"Group\" type-id"
},
{
"path": "test/data/collection/0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml",
"chars": 15625,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><collection entity-type=\"rele"
},
{
"path": "test/data/collection/20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml",
"chars": 451,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><collection entity-type=\"even"
},
{
"path": "test/data/collection/2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml",
"chars": 369,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><collection id=\"2326c2e8-be4b"
},
{
"path": "test/data/collection/29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml",
"chars": 496,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><collection entity-type=\"arti"
},
{
"path": "test/data/collection/855b134e-9a3b-4717-8df8-8c4838d89924-places.xml",
"chars": 492,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><collection entity-type=\"plac"
},
{
"path": "test/data/collection/a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml",
"chars": 428,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><collection type=\"Recording\" "
},
{
"path": "test/data/discid/f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml",
"chars": 5726,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><disc id=\"f7agNZK1HMQ2WUWq9bw"
},
{
"path": "test/data/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-.xml",
"chars": 7875,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><disc id=\"xp5tz6rE4OHrBafj0bL"
},
{
"path": "test/data/event/770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml",
"chars": 722,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><event type=\"Concert\" id=\"770"
},
{
"path": "test/data/event/e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml",
"chars": 1960,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><event id=\"e921686d-ba86-4122"
},
{
"path": "test/data/instrument/01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml",
"chars": 1305,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument id=\"01ba56a2-4306"
},
{
"path": "test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml",
"chars": 1823,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument type=\"Other instr"
},
{
"path": "test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml",
"chars": 659,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument id=\"6505f98c-f698"
},
{
"path": "test/data/instrument/9447c0af-5569-48f2-b4c5-241105d58c91.xml",
"chars": 473,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument id=\"9447c0af-5569"
},
{
"path": "test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml",
"chars": 402,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument type=\"Other instr"
},
{
"path": "test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml",
"chars": 989,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument id=\"d00cec5f-f9bc"
},
{
"path": "test/data/instrument/dabdeb41-560f-4d84-aa6a-cf22349326fe.xml",
"chars": 450,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><instrument type=\"String inst"
},
{
"path": "test/data/label/022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml",
"chars": 562,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><label type=\"Original Product"
},
{
"path": "test/data/label/e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml",
"chars": 525,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><label type=\"Publisher\" id=\"e"
},
{
"path": "test/data/place/0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml",
"chars": 767,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><place type=\"Religious buildi"
},
{
"path": "test/data/place/browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml",
"chars": 4387,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><place-list count=\"395\"><plac"
},
{
"path": "test/data/recording/f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml",
"chars": 1512,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><recording id=\"f606f733-c1eb-"
},
{
"path": "test/data/release/212895ca-ee36-439a-a824-d2620cd10461-recordings.xml",
"chars": 5114,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"212895ca-ee36-4"
},
{
"path": "test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml",
"chars": 8328,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"833d4c3a-2635-4"
},
{
"path": "test/data/release/8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml",
"chars": 3904,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"8eb2b179-643d-35"
},
{
"path": "test/data/release/9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml",
"chars": 49054,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"9ce41d09-40e4-4"
},
{
"path": "test/data/release/a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml",
"chars": 778,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"a81f3c15-2f36-4"
},
{
"path": "test/data/release/b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml",
"chars": 1028,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"b66ebe6d-a577-4"
},
{
"path": "test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml",
"chars": 9576,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"fbe4490e-e366-4"
},
{
"path": "test/data/release/fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml",
"chars": 12881,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release id=\"fe29e7f0-eb46-4"
},
{
"path": "test/data/release-group/f52bc6a1-c848-49e6-85de-f8f53459a624.xml",
"chars": 395,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><release-group type=\"Soundtr"
},
{
"path": "test/data/search-artist.xml",
"chars": 7270,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\" xmlns:ext=\""
},
{
"path": "test/data/search-event.xml",
"chars": 1227,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><metadata created=\"2016-01-18T12:04:48.934Z\" xmlns=\"http://musicb"
},
{
"path": "test/data/search-instrument.xml",
"chars": 29075,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><metadata created=\"2015-11-18T20:10:35.821Z\" xmlns=\"http://musicb"
},
{
"path": "test/data/search-label.xml",
"chars": 488,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\" xmlns:ext=\""
},
{
"path": "test/data/search-place.xml",
"chars": 9111,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><metadata created=\"2016-01-18T11:57:20.771Z\" xmlns=\"http://musicb"
},
{
"path": "test/data/search-recording.xml",
"chars": 29657,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\" xmlns:ext=\""
},
{
"path": "test/data/search-release-group.xml",
"chars": 17597,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\" xmlns:ext=\""
},
{
"path": "test/data/search-release.xml",
"chars": 27189,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\" xmlns:ext=\""
},
{
"path": "test/data/search-work.xml",
"chars": 16604,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\" xmlns:ext=\""
},
{
"path": "test/data/work/3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml",
"chars": 1538,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><work id=\"3d7c7cd2-da79-37f4-"
},
{
"path": "test/data/work/72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml",
"chars": 715,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><work type=\"Cantata\" id=\"72c9"
},
{
"path": "test/data/work/80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml",
"chars": 590,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><work type=\"Symphony\" id=\"80"
},
{
"path": "test/data/work/8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml",
"chars": 422,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><metadata xmlns=\"http://musicbrainz.org/ns/mmd-2.0#\"><work id=\"8e134b32-99b8-4e96-"
},
{
"path": "test/test_browse.py",
"chars": 8152,
"preview": "import unittest\n\nimport musicbrainzngs\nfrom test import _common\n\n\nclass BrowseTest(unittest.TestCase):\n\n def setUp(se"
},
{
"path": "test/test_caa.py",
"chars": 5105,
"preview": "import unittest\n\nfrom musicbrainzngs import caa\nfrom musicbrainzngs import compat\nfrom musicbrainzngs.musicbrainz import"
},
{
"path": "test/test_collection.py",
"chars": 4447,
"preview": "import unittest\nimport musicbrainzngs\nfrom musicbrainzngs import compat\nfrom test import _common\n\n\nclass CollectionTest("
},
{
"path": "test/test_getentity.py",
"chars": 7648,
"preview": "import unittest\nimport musicbrainzngs\nfrom test import _common\n\n\nclass UrlTest(unittest.TestCase):\n \"\"\" Test that the"
},
{
"path": "test/test_mbxml.py",
"chars": 1498,
"preview": "import unittest\nfrom musicbrainzngs import mbxml\n\n\nclass MbXML(unittest.TestCase):\n\n def testMakeBarcode(self):\n "
},
{
"path": "test/test_mbxml_artist.py",
"chars": 1374,
"preview": "# Tests for parsing of artist queries\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass GetArtistTest(unittest"
},
{
"path": "test/test_mbxml_collection.py",
"chars": 5883,
"preview": "# Tests for parsing of collection queries\n\nimport unittest\nimport os\nimport musicbrainzngs\nfrom test import _common\n\n\ncl"
},
{
"path": "test/test_mbxml_discid.py",
"chars": 3254,
"preview": "# Tests for parsing of discid queries\n\nimport unittest\nimport os\nimport musicbrainzngs\nfrom test import _common\n\n\nclass "
},
{
"path": "test/test_mbxml_event.py",
"chars": 1545,
"preview": "# Tests for parsing of event results\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass EventTest(unittest.Test"
},
{
"path": "test/test_mbxml_instrument.py",
"chars": 3254,
"preview": "# -*- coding: UTF-8 -*-\n# Tests for parsing instrument queries\n\nimport unittest\nimport os\nfrom test import _common\n\n\ncla"
},
{
"path": "test/test_mbxml_label.py",
"chars": 1188,
"preview": "# Tests for parsing of label queries\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass GetLabelTest(unittest.T"
},
{
"path": "test/test_mbxml_place.py",
"chars": 1485,
"preview": "# Tests for parsing of place results\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass PlaceTest(unittest.Test"
},
{
"path": "test/test_mbxml_recording.py",
"chars": 961,
"preview": "# coding=utf-8\n# Tests for parsing of recording queries\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass GetR"
},
{
"path": "test/test_mbxml_release.py",
"chars": 7392,
"preview": "# Tests for parsing of release queries\n\nimport unittest\nimport os\nimport musicbrainzngs\nfrom test import _common\n\n\nclass"
},
{
"path": "test/test_mbxml_release_group.py",
"chars": 967,
"preview": "# Tests for parsing of release queries\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass GetReleaseGroupTest(u"
},
{
"path": "test/test_mbxml_search.py",
"chars": 4448,
"preview": "import unittest\nimport os\nfrom musicbrainzngs import mbxml\n\n\nDATA_DIR = os.path.join(os.path.dirname(__file__), \"data\")\n"
},
{
"path": "test/test_mbxml_work.py",
"chars": 2939,
"preview": "# coding=utf-8\n# Tests for parsing of work queries\n\nimport unittest\nimport os\nfrom test import _common\n\n\nclass GetWorkTe"
},
{
"path": "test/test_ratelimit.py",
"chars": 3544,
"preview": "import unittest\nimport time\nimport musicbrainzngs\nfrom musicbrainzngs import musicbrainz\nfrom test._common import Timeco"
},
{
"path": "test/test_requests.py",
"chars": 5517,
"preview": "import unittest\nimport musicbrainzngs\nfrom musicbrainzngs import musicbrainz\nfrom test import _common\n\n\nclass ArgumentTe"
},
{
"path": "test/test_search.py",
"chars": 6625,
"preview": "import unittest\n\nimport musicbrainzngs\nfrom test import _common\n\n\nclass SearchUrlTest(unittest.TestCase):\n \"\"\" Test t"
},
{
"path": "test/test_submit.py",
"chars": 1680,
"preview": "import unittest\nimport musicbrainzngs\nfrom musicbrainzngs import musicbrainz\nfrom test import _common\n\n\nclass SubmitTest"
},
{
"path": "tox.ini",
"chars": 175,
"preview": "[tox]\nenvlist=py27,py37,py38,py39,py310\n\n[testenv]\ncommands=pytest\ndeps=pytest\n\n[gh-actions]\npython =\n 2.7: py27\n "
}
]
About this extraction
This page contains the full source code of the alastair/python-musicbrainzngs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 96 files (506.2 KB), approximately 160.4k tokens, and a symbol index with 415 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.