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 `_.
For more information on the musicbrainz webservice see ``_.
Usage
*****
.. code:: python
# Import the module
import musicbrainzngs
# If you plan to submit data, authenticate
musicbrainzngs.auth("user", "password")
# Tell musicbrainz what your app is, and how to contact you
# (this step is required, as per the webservice access rules
# at http://wiki.musicbrainz.org/XML_Web_Service/Rate_Limiting )
musicbrainzngs.set_useragent("Example music app", "0.1", "http://example.com/music")
# If you are connecting to a different server
musicbrainzngs.set_hostname("beta.musicbrainz.org")
See the ``query.py`` file for more examples.
More documentation is available at
`Read the Docs `_.
Contribute
**********
If you want to contribute to this repository, please read `the
contribution guidelines
`_ first.
Authors
*******
These bindings were written by `Alastair Porter `_.
Contributions have been made by:
* `Adrian Sampson `_
* `Corey Farwell `_
* `Galen Hazelwood `_
* `Greg Ward `_
* `Ian McEwen `_
* `Jérémie Detrey `_
* `Johannes Dewender `_
* `Michael Marineau `_
* `Patrick Speiser `_
* `Pavan Chander `_
* `Paul Bailey `_
* `Rui Gonçalves `_
* `Ryan Helinski `_
* `Sam Doshi `_
* `Shadab Zafar `_
* `Simon Chopin `_
* `Thomas Vander Stichele `_
* `Wieland Hoffmann `_
License
*******
This library is released under the simplified BSD license except for the file
``musicbrainzngs/compat.py`` which is licensed under the ISC license.
See COPYING for details.
================================================
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 ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/doctrees
-rm -rf $(BUILDDIR)/html/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/musicbrainzngs.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/musicbrainzngs.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/musicbrainzngs"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/musicbrainzngs"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
================================================
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
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'musicbrainzngsdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'musicbrainzngs.tex', u'musicbrainzngs Documentation',
u'Alastair Porter et. al', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'musicbrainzngs', u'musicbrainzngs Documentation',
[u'Alastair Porter et. al'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'musicbrainzngs', u'musicbrainzngs Documentation',
u'Alastair Porter et. al', 'musicbrainzngs', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
================================================
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 `_ as easy as::
pip install musicbrainzngs
Git
---
If you want the latest code or even feel like contributing, the code is
available on `GitHub `_.
You can easily clone the code with git::
git clone git://github.com/alastair/python-musicbrainzngs.git
Now you can start hacking on the code or install it system-wide::
python setup.py install
================================================
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 ^` where ^ is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\musicbrainzngs.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\musicbrainzngs.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end
================================================
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
`_
by setting the useragent header made in HTTP requests to one that is unique to
your application.
To ease this, the convenience function :meth:`musicbrainzngs.set_useragent` is
provided which automatically sets the useragent based on information about the
application name, version and contact information to the format `recommended by
MusicBrainz
`_.
If a request is made without setting the useragent beforehand, a
:exc:`musicbrainzngs.UsageError` will be raised.
Authentication
--------------
Certain calls to the webservice require user authentication prior to the call
itself. The affected functions state this requirement in their documentation.
The user and password used for authentication are the same as for the
MusicBrainz website itself and can be set with the :meth:`musicbrainzngs.auth`
method. After calling this function, the credentials will be saved and
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 `_.
Cover Art Data
^^^^^^^^^^^^^^
This library includes a few methods to access data from the `Cover Art Archive
`_ which has a `documented API
`_.
Both :func:`musicbrainzngs.get_image_list` and
:func:`musicbrainzngs.get_release_group_image_list` return the deserialized
cover art listing for a `release
`_
or `release group
`_.
To find out whether a release
has an approved front image, you could use the following example code::
release_id = "46a48e90-819b-4bed-81fa-5ca8aa33fbf3"
data = musicbrainzngs.get_cover_art_list("46a48e90-819b-4bed-81fa-5ca8aa33fbf3")
for image in data["images"]:
if "Front" in image["types"] and image["approved"]:
print "%s is an approved front image!" % image["thumbnails"]["large"]
break
To retrieve an image itself, use :func:`musicbrainzngs.get_image`. A
few convenience functions like :func:`musicbrainzngs.get_image_front`
are provided to allow easy access to often requested images.
.. warning:: There is no upper bound for the size of images uploaded to the
Cover Art Archive and downloading an image will return the binary data in
memory. Consider using the :py:mod:`tempfile` module or similar
techniques to save images to disk as soon as possible.
Searching
---------
When you don't know the MusicBrainz IDs yet, you have to start a search.
Using :func:`musicbrainzngs.search_artists`::
result = musicbrainzngs.search_artists(artist="xx", type="group",
country="GB")
for artist in result['artist-list']:
print(u"{id}: {name}".format(id=artist['id'], name=artist["name"]))
.. tip:: Musicbrainzngs returns unicode strings.
It's up to you to make sure Python (2) doesn't try to convert these
to ascii again. In the example we force a unicode literal for print.
Python 3 works without fixes like these.
You can also use the query without specifying the search fields::
musicbrainzngs.search_release_groups("the clash london calling")
The query and the search fields can also be used at the same time.
Browsing
--------
When you want to fetch a list of entities greater than 25,
you have to use one of the browse functions.
Not only can you specify a `limit` as high as 100,
but you can also specify an `offset` to get the complete list
in multiple requests.
An example would be using :func:`musicbrainzngs.browse_release_groups`
to get all releases for a label::
label = "71247f6b-fd24-4a56-89a2-23512f006f0c"
limit = 100
offset = 0
releases = []
page = 1
print("fetching page number %d.." % page)
result = musicbrainzngs.browse_releases(label=label, includes=["labels"],
release_type=["album"], limit=limit)
page_releases = result['release-list']
releases += page_releases
# release-count is only available starting with musicbrainzngs 0.5
if "release-count" in result:
count = result['release-count']
print("")
while len(page_releases) >= limit:
offset += limit
page += 1
print("fetching page number %d.." % page)
result = musicbrainzngs.browse_releases(label=label, includes=["labels"],
release_type=["album"], limit=limit, offset=offset)
page_releases = result['release-list']
releases += page_releases
print("")
for release in releases:
for label_info in release['label-info-list']:
catnum = label_info.get('catalog-number')
if label_info['label']['id'] == label and catnum:
print("{catnum:>17}: {date:10} {title}".format(catnum=catnum,
date=release['date'], title=release['title']))
print("\n%d releases on %d pages" % (len(releases), page))
.. tip:: You should always try to filter in the query, when possible,
rather than fetching everything and filtering afterwards.
This will make your application faster
since web service requests are throttled.
In the example we filter by `release_type`.
Submitting
----------
You can also submit data using musicbrainzngs.
Please use :func:`musicbrainzngs.set_hostname` to set the host to
test.musicbrainz.org when testing the submission part of your application.
`Authentication`_ is necessary to submit any data to MusicBrainz.
An example using :func:`musicbrainzngs.submit_barcodes` looks like this::
musicbrainzngs.set_hostname("test.musicbrainz.org")
musicbrainzngs.auth("test", "mb")
barcodes = {
"174a5513-73d1-3c9d-a316-3c1c179e35f8": "5099749534728",
"838952af-600d-3f51-84d5-941d15880400": "602517737280"
}
musicbrainzngs.submit_barcodes(barcodes)
See :ref:`api_submitting` in the API for other possibilities.
More Examples
-------------
You can find some examples for using `musicbrainzngs` in the
`examples directory `_.
================================================
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
`_
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
`_
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
xy, return a list
of [x, y]"""
try:
tree = util.bytes_to_elementtree(error)
root = tree.getroot()
errors = []
if root.tag == "error":
for ch in root:
if ch.tag == "text":
errors.append(ch.text)
return errors
except ET.ParseError:
return None
def make_artist_credit(artists):
names = []
for artist in artists:
if isinstance(artist, dict):
if "name" in artist:
names.append(artist.get("name", ""))
else:
names.append(artist.get("artist", {}).get("name", ""))
else:
names.append(artist)
return "".join(names)
def parse_elements(valid_els, inner_els, element):
""" Extract single level subelements from an element.
For example, given the element:
Text
and a list valid_els that contains "subelement",
return a dict {'subelement': 'Text'}
Delegate the parsing of multi-level subelements to another function.
For example, given the element:
FooBar
and a dictionary {'subelement': parse_subelement},
call parse_subelement() and
return a dict {'subelement': }
if parse_subelement returns a tuple of the form
(True, {'subelement-key': })
then merge the second element of the tuple into the
result (which may have a key other than 'subelement' or
more than 1 key)
"""
result = {}
for sub in element:
t = fixtag(sub.tag, NS_MAP)[0]
if ":" in t:
t = t.split(":")[1]
if t in valid_els:
result[t] = sub.text or ""
elif t in inner_els.keys():
inner_result = inner_els[t](sub)
if isinstance(inner_result, tuple) and inner_result[0]:
result.update(inner_result[1])
else:
result[t] = inner_result
# add counts for lists when available
m = re.match(r'([a-z0-9-]+)-list', t)
if m and "count" in sub.attrib:
result["%s-count" % m.group(1)] = int(sub.attrib["count"])
else:
_log.info("in <%s>, uncaught <%s>",
fixtag(element.tag, NS_MAP)[0], t)
return result
def parse_attributes(attributes, element):
""" Extract attributes from an element.
For example, given the element:
and a list attributes that contains "type",
return a dict {'type': 'Group'}
"""
result = {}
for attr in element.attrib:
if "{" in attr:
a = fixtag(attr, NS_MAP)[0]
else:
a = attr
if a in attributes:
result[a] = element.attrib[attr]
else:
_log.info("in <%s>, uncaught attribute %s", fixtag(element.tag, NS_MAP)[0], attr)
return result
def parse_message(message):
tree = util.bytes_to_elementtree(message)
root = tree.getroot()
result = {}
valid_elements = {"area": parse_area,
"artist": parse_artist,
"instrument": parse_instrument,
"label": parse_label,
"place": parse_place,
"event": parse_event,
"release": parse_release,
"release-group": parse_release_group,
"series": parse_series,
"recording": parse_recording,
"work": parse_work,
"url": parse_url,
"disc": parse_disc,
"cdstub": parse_cdstub,
"isrc": parse_isrc,
"annotation-list": parse_annotation_list,
"area-list": parse_area_list,
"artist-list": parse_artist_list,
"label-list": parse_label_list,
"place-list": parse_place_list,
"event-list": parse_event_list,
"instrument-list": parse_instrument_list,
"release-list": parse_release_list,
"release-group-list": parse_release_group_list,
"series-list": parse_series_list,
"recording-list": parse_recording_list,
"work-list": parse_work_list,
"url-list": parse_url_list,
"collection-list": parse_collection_list,
"collection": parse_collection,
"message": parse_response_message
}
result.update(parse_elements([], valid_elements, root))
return result
def parse_response_message(message):
return parse_elements(["text"], {}, message)
def parse_collection_list(cl):
return [parse_collection(c) for c in cl]
def parse_collection(collection):
result = {}
attribs = ["id", "type", "entity-type"]
elements = ["name", "editor"]
inner_els = {"release-list": parse_release_list,
"artist-list": parse_artist_list,
"event-list": parse_event_list,
"place-list": parse_place_list,
"recording-list": parse_recording_list,
"work-list": parse_work_list}
result.update(parse_attributes(attribs, collection))
result.update(parse_elements(elements, inner_els, collection))
return result
def parse_annotation_list(al):
return [parse_annotation(a) for a in al]
def parse_annotation(annotation):
result = {}
attribs = ["type", "ext:score"]
elements = ["entity", "name", "text"]
result.update(parse_attributes(attribs, annotation))
result.update(parse_elements(elements, {}, annotation))
return result
def parse_lifespan(lifespan):
parts = parse_elements(["begin", "end", "ended"], {}, lifespan)
return parts
def parse_area_list(al):
return [parse_area(a) for a in al]
def parse_area(area):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "disambiguation"]
inner_els = {"life-span": parse_lifespan,
"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation,
"iso-3166-1-code-list": parse_element_list,
"iso-3166-2-code-list": parse_element_list,
"iso-3166-3-code-list": parse_element_list}
result.update(parse_attributes(attribs, area))
result.update(parse_elements(elements, inner_els, area))
return result
def parse_artist_list(al):
return [parse_artist(a) for a in al]
def parse_artist(artist):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "country", "user-rating",
"disambiguation", "gender", "ipi"]
inner_els = {"area": parse_area,
"begin-area": parse_area,
"end-area": parse_area,
"life-span": parse_lifespan,
"recording-list": parse_recording_list,
"relation-list": parse_relation_list,
"release-list": parse_release_list,
"release-group-list": parse_release_group_list,
"work-list": parse_work_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"ipi-list": parse_element_list,
"isni-list": parse_element_list,
"alias-list": parse_alias_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, artist))
result.update(parse_elements(elements, inner_els, artist))
return result
def parse_coordinates(c):
return parse_elements(['latitude', 'longitude'], {}, c)
def parse_place_list(pl):
return [parse_place(p) for p in pl]
def parse_place(place):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "address",
"ipi", "disambiguation"]
inner_els = {"area": parse_area,
"coordinates": parse_coordinates,
"life-span": parse_lifespan,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, place))
result.update(parse_elements(elements, inner_els, place))
return result
def parse_event_list(el):
return [parse_event(e) for e in el]
def parse_event(event):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "time", "setlist", "cancelled", "disambiguation", "user-rating"]
inner_els = {"life-span": parse_lifespan,
"relation-list": parse_relation_list,
"alias-list": parse_alias_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating}
result.update(parse_attributes(attribs, event))
result.update(parse_elements(elements, inner_els, event))
return result
def parse_instrument(instrument):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "description", "disambiguation"]
inner_els = {"relation-list": parse_relation_list,
"tag-list": parse_tag_list,
"alias-list": parse_alias_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, instrument))
result.update(parse_elements(elements, inner_els, instrument))
return result
def parse_label_list(ll):
return [parse_label(l) for l in ll]
def parse_label(label):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "country", "label-code", "user-rating",
"ipi", "disambiguation"]
inner_els = {"area": parse_area,
"life-span": parse_lifespan,
"release-list": parse_release_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"ipi-list": parse_element_list,
"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, label))
result.update(parse_elements(elements, inner_els, label))
return result
def parse_relation_target(tgt):
attributes = parse_attributes(['id'], tgt)
if 'id' in attributes:
return (True, {'target-id': attributes['id']})
else:
return (True, {'target-id': tgt.text})
def parse_relation_list(rl):
attribs = ["target-type"]
ttype = parse_attributes(attribs, rl)
key = "%s-relation-list" % ttype["target-type"]
return (True, {key: [parse_relation(r) for r in rl]})
def parse_relation(relation):
result = {}
attribs = ["type", "type-id"]
elements = ["target", "direction", "begin", "end", "ended", "ordering-key"]
inner_els = {"area": parse_area,
"artist": parse_artist,
"instrument": parse_instrument,
"label": parse_label,
"place": parse_place,
"event": parse_event,
"recording": parse_recording,
"release": parse_release,
"release-group": parse_release_group,
"series": parse_series,
"attribute-list": parse_element_list,
"work": parse_work,
"target": parse_relation_target
}
result.update(parse_attributes(attribs, relation))
result.update(parse_elements(elements, inner_els, relation))
# We parse attribute-list again to get attributes that have both
# text and attribute values
result.update(parse_elements(['target-credit'], {"attribute-list": parse_relation_attribute_list}, relation))
return result
def parse_relation_attribute_list(attributelist):
ret = []
for attribute in attributelist:
ret.append(parse_relation_attribute_element(attribute))
return (True, {"attributes": ret})
def parse_relation_attribute_element(element):
# Parses an attribute into a dictionary containing an element
# {"attribute": } and also an additional element
# containing any xml attributes.
# e.g number
# -> {"attribute": "number", "value": "BuxWV 1"}
result = {}
for attr in element.attrib:
if "{" in attr:
a = fixtag(attr, NS_MAP)[0]
else:
a = attr
result[a] = element.attrib[attr]
result["attribute"] = element.text
return result
def parse_release(release):
result = {}
attribs = ["id", "ext:score"]
elements = ["title", "status", "disambiguation", "quality", "country",
"barcode", "date", "packaging", "asin"]
inner_els = {"text-representation": parse_text_representation,
"artist-credit": parse_artist_credit,
"label-info-list": parse_label_info_list,
"medium-list": parse_medium_list,
"release-group": parse_release_group,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation,
"cover-art-archive": parse_caa,
"release-event-list": parse_release_event_list}
result.update(parse_attributes(attribs, release))
result.update(parse_elements(elements, inner_els, release))
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(
result["artist-credit"])
return result
def parse_medium_list(ml):
"""medium-list results from search have an additional
element containing the number of tracks
over all mediums. Optionally add this"""
medium_list = []
track_count = None
for m in ml:
tag = fixtag(m.tag, NS_MAP)[0]
if tag == "ws2:medium":
medium_list.append(parse_medium(m))
elif tag == "ws2:track-count":
track_count = int(m.text)
ret = {"medium-list": medium_list}
if track_count is not None:
ret["medium-track-count"] = track_count
return (True, ret)
def parse_release_event_list(rel):
return [parse_release_event(re) for re in rel]
def parse_release_event(event):
result = {}
elements = ["date"]
inner_els = {"area": parse_area}
result.update(parse_elements(elements, inner_els, event))
return result
def parse_medium(medium):
result = {}
elements = ["position", "format", "title"]
inner_els = {"disc-list": parse_disc_list,
"pregap": parse_track,
"track-list": parse_track_list,
"data-track-list": parse_track_list}
result.update(parse_elements(elements, inner_els, medium))
return result
def parse_disc_list(dl):
return [parse_disc(d) for d in dl]
def parse_text_representation(textr):
return parse_elements(["language", "script"], {}, textr)
def parse_release_group(rg):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["title", "user-rating", "first-release-date", "primary-type",
"disambiguation"]
inner_els = {"artist-credit": parse_artist_credit,
"release-list": parse_release_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"secondary-type-list": parse_element_list,
"relation-list": parse_relation_list,
"rating": parse_rating,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, rg))
result.update(parse_elements(elements, inner_els, rg))
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(result["artist-credit"])
return result
def parse_recording(recording):
result = {}
attribs = ["id", "ext:score"]
elements = ["title", "length", "user-rating", "disambiguation", "video"]
inner_els = {"artist-credit": parse_artist_credit,
"release-list": parse_release_list,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"isrc-list": parse_external_id_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, recording))
result.update(parse_elements(elements, inner_els, recording))
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(result["artist-credit"])
return result
def parse_series_list(sl):
return [parse_series(s) for s in sl]
def parse_series(series):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "disambiguation"]
inner_els = {"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, series))
result.update(parse_elements(elements, inner_els, series))
return result
def parse_external_id_list(pl):
return [parse_attributes(["id"], p)["id"] for p in pl]
def parse_element_list(el):
return [e.text for e in el]
def parse_work_list(wl):
return [parse_work(w) for w in wl]
def parse_work(work):
result = {}
attribs = ["id", "ext:score", "type"]
elements = ["title", "user-rating", "language", "iswc", "disambiguation"]
inner_els = {"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"rating": parse_rating,
"alias-list": parse_alias_list,
"iswc-list": parse_element_list,
"relation-list": parse_relation_list,
"annotation": parse_response_message,
"attribute-list": parse_work_attribute_list
}
result.update(parse_attributes(attribs, work))
result.update(parse_elements(elements, inner_els, work))
return result
def parse_work_attribute_list(wal):
return [parse_work_attribute(wa) for wa in wal]
def parse_work_attribute(wa):
attribs = ["type"]
typeinfo = parse_attributes(attribs, wa)
result = {}
if typeinfo:
result = {"attribute": typeinfo["type"],
"value": wa.text}
return result
def parse_url_list(ul):
return [parse_url(u) for u in ul]
def parse_url(url):
result = {}
attribs = ["id"]
elements = ["resource"]
inner_els = {"relation-list": parse_relation_list}
result.update(parse_attributes(attribs, url))
result.update(parse_elements(elements, inner_els, url))
return result
def parse_disc(disc):
result = {}
attribs = ["id"]
elements = ["sectors"]
inner_els = {"release-list": parse_release_list,
"offset-list": parse_offset_list
}
result.update(parse_attributes(attribs, disc))
result.update(parse_elements(elements, inner_els, disc))
return result
def parse_cdstub(cdstub):
result = {}
attribs = ["id"]
elements = ["title", "artist", "barcode"]
inner_els = {"track-list": parse_track_list}
result.update(parse_attributes(attribs, cdstub))
result.update(parse_elements(elements, inner_els, cdstub))
return result
def parse_offset_list(ol):
return [int(o.text) for o in ol]
def parse_instrument_list(rl):
result = []
for r in rl:
result.append(parse_instrument(r))
return result
def parse_release_list(rl):
result = []
for r in rl:
result.append(parse_release(r))
return result
def parse_release_group_list(rgl):
result = []
for rg in rgl:
result.append(parse_release_group(rg))
return result
def parse_isrc(isrc):
result = {}
attribs = ["id"]
inner_els = {"recording-list": parse_recording_list}
result.update(parse_attributes(attribs, isrc))
result.update(parse_elements([], inner_els, isrc))
return result
def parse_recording_list(recs):
result = []
for r in recs:
result.append(parse_recording(r))
return result
def parse_artist_credit(ac):
result = []
for namecredit in ac:
result.append(parse_name_credit(namecredit))
join = parse_attributes(["joinphrase"], namecredit)
if "joinphrase" in join:
result.append(join["joinphrase"])
return result
def parse_name_credit(nc):
result = {}
elements = ["name"]
inner_els = {"artist": parse_artist}
result.update(parse_elements(elements, inner_els, nc))
return result
def parse_label_info_list(lil):
result = []
for li in lil:
result.append(parse_label_info(li))
return result
def parse_label_info(li):
result = {}
elements = ["catalog-number"]
inner_els = {"label": parse_label}
result.update(parse_elements(elements, inner_els, li))
return result
def parse_track_list(tl):
result = []
for t in tl:
result.append(parse_track(t))
return result
def parse_track(track):
result = {}
attribs = ["id"]
elements = ["number", "position", "title", "length"]
inner_els = {"recording": parse_recording,
"artist-credit": parse_artist_credit}
result.update(parse_attributes(attribs, track))
result.update(parse_elements(elements, inner_els, track))
if "artist-credit" in result.get("recording", {}) and "artist-credit" not in result:
result["artist-credit"] = result["recording"]["artist-credit"]
if "artist-credit" in result:
result["artist-credit-phrase"] = make_artist_credit(result["artist-credit"])
# Make a length field that contains track length or recording length
track_or_recording = None
if "length" in result:
track_or_recording = result["length"]
elif result.get("recording", {}).get("length"):
track_or_recording = result.get("recording", {}).get("length")
if track_or_recording:
result["track_or_recording_length"] = track_or_recording
return result
def parse_tag_list(tl):
return [parse_tag(t) for t in tl]
def parse_tag(tag):
result = {}
attribs = ["count"]
elements = ["name"]
result.update(parse_attributes(attribs, tag))
result.update(parse_elements(elements, {}, tag))
return result
def parse_rating(rating):
result = {}
attribs = ["votes-count"]
result.update(parse_attributes(attribs, rating))
result["rating"] = rating.text
return result
def parse_alias_list(al):
return [parse_alias(a) for a in al]
def parse_alias(alias):
result = {}
attribs = ["locale", "sort-name", "type", "primary",
"begin-date", "end-date"]
result.update(parse_attributes(attribs, alias))
result["alias"] = alias.text
return result
def parse_caa(caa_element):
result = {}
elements = ["artwork", "count", "front", "back", "darkened"]
result.update(parse_elements(elements, {}, caa_element))
return result
###
def make_barcode_request(release2barcode):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
rel_list = ET.SubElement(root, "{%s}release-list" % NS)
for release, barcode in release2barcode.items():
rel_xml = ET.SubElement(rel_list, "{%s}release" % NS)
bar_xml = ET.SubElement(rel_xml, "{%s}barcode" % NS)
rel_xml.set("{%s}id" % NS, release)
bar_xml.text = barcode
return ET.tostring(root, "utf-8")
def make_tag_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
for entity_type in ['artist', 'label', 'place', 'recording', 'release', 'release_group', 'work']:
entity_tags = kwargs.pop(entity_type + '_tags', None)
if entity_tags is not None:
e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
for e, tags in entity_tags.items():
e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
e_xml.set("{%s}id" % NS, e)
taglist = ET.SubElement(e_xml, "{%s}user-tag-list" % NS)
for tag in tags:
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
name_xml.text = tag
if kwargs.keys():
raise TypeError("make_tag_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8")
def make_rating_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
for entity_type in ['artist', 'label', 'recording', 'release_group', 'work']:
entity_ratings = kwargs.pop(entity_type + '_ratings', None)
if entity_ratings is not None:
e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
for e, rating in entity_ratings.items():
e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
e_xml.set("{%s}id" % NS, e)
rating_xml = ET.SubElement(e_xml, "{%s}user-rating" % NS)
rating_xml.text = str(rating)
if kwargs.keys():
raise TypeError("make_rating_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8")
def make_isrc_request(recording2isrcs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
rec_list = ET.SubElement(root, "{%s}recording-list" % NS)
for rec, isrcs in recording2isrcs.items():
if len(isrcs) > 0:
rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS)
rec_xml.set("{%s}id" % NS, rec)
isrc_list_xml = ET.SubElement(rec_xml, "{%s}isrc-list" % NS)
isrc_list_xml.set("{%s}count" % NS, str(len(isrcs)))
for isrc in isrcs:
isrc_xml = ET.SubElement(isrc_list_xml, "{%s}isrc" % NS)
isrc_xml.set("{%s}id" % NS, isrc)
return ET.tostring(root, "utf-8")
================================================
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 ` user
as a dict with a 'collection-list' key."""
# Missing the count in the reply
return _do_mb_query("collection", '')
def _do_collection_query(collection, collection_type, limit, offset):
params = {}
if limit: params["limit"] = limit
if offset: params["offset"] = offset
return _do_mb_query("collection", "%s/%s" % (collection, collection_type), [], params)
def get_artists_in_collection(collection, limit=None, offset=None):
"""List the artists in a collection.
Returns a dict with a 'collection' key, which again has a 'artist-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "artists", limit, offset)
def get_releases_in_collection(collection, limit=None, offset=None):
"""List the releases in a collection.
Returns a dict with a 'collection' key, which again has a 'release-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "releases", limit, offset)
def get_events_in_collection(collection, limit=None, offset=None):
"""List the events in a collection.
Returns a dict with a 'collection' key, which again has a 'event-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "events", limit, offset)
def get_places_in_collection(collection, limit=None, offset=None):
"""List the places in a collection.
Returns a dict with a 'collection' key, which again has a 'place-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "places", limit, offset)
def get_recordings_in_collection(collection, limit=None, offset=None):
"""List the recordings in a collection.
Returns a dict with a 'collection' key, which again has a 'recording-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "recordings", limit, offset)
def get_works_in_collection(collection, limit=None, offset=None):
"""List the works in a collection.
Returns a dict with a 'collection' key, which again has a 'work-list'.
See `Browsing`_ for how to use `limit` and `offset`.
"""
return _do_collection_query(collection, "works", limit, offset)
# Submission methods
def submit_barcodes(release_barcode):
"""Submits a set of {release_id1: barcode, ...}"""
query = mbxml.make_barcode_request(release_barcode)
return _do_mb_post("release", query)
def submit_isrcs(recording_isrcs):
"""Submit ISRCs.
Submits a set of {recording-id1: [isrc1, ...], ...}
or {recording_id1: isrc, ...}.
"""
rec2isrcs = dict()
for (rec, isrcs) in recording_isrcs.items():
rec2isrcs[rec] = isrcs if isinstance(isrcs, list) else [isrcs]
query = mbxml.make_isrc_request(rec2isrcs)
return _do_mb_post("recording", query)
def submit_tags(**kwargs):
"""Submit user tags.
Takes parameters named e.g. 'artist_tags', 'recording_tags', etc.,
and of the form:
{entity_id1: [tag1, ...], ...}
If you only have one tag for an entity you can use a string instead
of a list.
The user's tags for each entity will be set to that list, adding or
removing tags as necessary. Submitting an empty list for an entity
will remove all tags for that entity by the user.
"""
for k, v in kwargs.items():
for id, tags in v.items():
kwargs[k][id] = tags if isinstance(tags, list) else [tags]
query = mbxml.make_tag_request(**kwargs)
return _do_mb_post("tag", query)
def submit_ratings(**kwargs):
"""Submit user ratings.
Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc.,
and of the form:
{entity_id1: rating, ...}
Ratings are numbers from 0-100, at intervals of 20 (20 per 'star').
Submitting a rating of 0 will remove the user's rating.
"""
query = mbxml.make_rating_request(**kwargs)
return _do_mb_post("rating", query)
def add_releases_to_collection(collection, releases=[]):
"""Add releases to a collection.
Collection and releases should be identified by their MBIDs
"""
# XXX: Maximum URI length of 16kb means we should only allow ~400 releases
releaselist = ";".join(releases)
return _do_mb_put("collection/%s/releases/%s" % (collection, releaselist))
def remove_releases_from_collection(collection, releases=[]):
"""Remove releases from a collection.
Collection and releases should be identified by their MBIDs
"""
releaselist = ";".join(releases)
return _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist))
================================================
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="", 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
================================================
Сергей Сергеевич ПрокофьевProkofiev, Sergei SergeyevichRussian composerMaleRU1891-04-271953-03-05trueProkofiefProkofieffProkofievProkofiev, SergeiProkofiev, SergejProkovieffS. ProkofievSerge ProkofieffSerge ProkofievSerge ProkofjevSerge ProkofjewSergei ProkofiefSergei ProkofieffSergei ProkofievSergei ProkofjefSergei ProkofjevSergei ProkovievSergei Sergeyevich ProkofievSergej ProkofjevSergej ProkofjewSergej Sergeevič Prokof'evSergey ProkofievSergey Sergeyevich ProkofievSerghei ProkofievSergi ProkofievSergueï ProkofievПрокофьев|Prokofievプロコフィエフ
================================================
FILE: test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml
================================================
ErrorsErrorsGB2004
================================================
FILE: test/data/artist/b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml
================================================
EXOEXOSouth Korean-Chinese boy groupKRSouth KoreaSouth KoreaKRSeoulSeoulKR-112011-12-23143472cb-de19-4da3-b8ac-8a7d01b6638dbackward20112015trueoriginal黄子韬Huang ZitaoTAO1fed07c6-adf1-4668-b34b-434ae9741763backward2011lead vocalsoriginalD.O.D.O.South Korean singer, member of EXOD.O.254658f7-f4eb-4c62-bafb-28f57707517bbackward2012EXO-KEXO-K2e675c4a-396c-49e8-96b7-a8a72361df84backward20112014trueoriginal吴亦凡Wu, Yi FanKRIS31e909fa-cdf6-4b7d-a7d3-8b928de4e0babackward2012EXO-MEXO-M36af49c3-7edf-44bf-b040-cf5d9b21ebe7backward2011original시우민XiuminXIUMIN439c9247-9291-47a8-8282-7f80bc3f369dbackward2011lead vocalsoriginal첸ChenCHEN53be8ba6-8be8-4e3b-8b20-f83c08ecf124backward2011-122014-10-10trueoriginal鹿晗Lu HanLUHAN5d7686b2-90d5-44c6-ab70-693e98506fb6backward2011originalLAYLAYEXO6afff86d-fc4a-4446-a41e-f88e1322a5bebackward2011original카이KaiEXOKAI7100af1a-1224-4636-83ad-7f7fcf0973d7backward2011original찬열ChanyeolCHANYEOL7593e0e2-fc1c-4855-a645-731c7504e16bbackward2011original백현BaekhyunBAEKHYUN9892db11-4c1b-4029-bed5-6aae508e7fcebackward2011original세훈SehunSEHUN9aba1d1e-c460-400d-88a7-35f08721d311backward2011original수호SuhoSUHOdb095c11-8b25-41b5-adda-d850d9001dcdbackward2016-10EXO-CBXEXO-CBX
================================================
FILE: test/data/collection/0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml
================================================
My CollectionJonnyJDEntitiesOfficialnormalCardboard/Paper Sleeveeng1992-10DE1992-10GermanyGermanyDE4013859210366German Mystic Sound Sampler, Volume III: Indie-Classics, Volume IVOfficialnormaleng1992-11-06DE1992-11-06GermanyGermanyDE0718759100529Boys Don’t CryOfficialnormaleng1986-04-29DE1986-04-29GermanyGermanyDE042281501128Zillo Romantic Sound Sampler: Indie Classics, Volume IIIOfficialnormaleng1991DE1991GermanyGermanyDE718759100222Bram Stoker's DraculaOfficialnormalJewel Caseeng1992-11-04US1992-11-04United StatesUnited StatesUS074645316529Black SabbathOfficialnormaleng1970DE1970GermanyGermanyDEFollow the BlindOfficialnormalJewel Caseeng1989DE1989GermanyGermanyDELive! Exile on Valletta StreetOfficialnormaleng1991-09-13DE1991-09-13GermanyGermanyDE731451117527German Mystic Sound Sampler, Volume I: Indie-Classics, Volume IPromotionnormalmul1991-05-17DE1991-05-17GermanyGermanyDE4012170902028FixedOfficialhighDigipakeng1992-12-07US1992-12-07United StatesUnited StatesUS606949609320Gothic RockOfficialnormalJewel Caseeng1992GB1992United KingdomUnited KingdomGB5013145203828Burning From the InsideOfficialnormalJewel Caseeng1988GB1988United KingdomUnited KingdomGB5012093004525HighOfficialnormalDigipakeng1992-02-27US1992-02-27United StatesUnited StatesUS075596643726Methods of SilenceOfficialnormalJewel Caseeng1989-09-12US1989-09-12United StatesUnited StatesUS075678200229Bouquet of DreamsOfficialnormalJewel Caseeng1991-08-19DE1991-08-19GermanyGermanyDE0718751108523German Mystic Sound Sampler, Volume II: Indie-Classics, Volume IIOfficialnormaleng1991DE1991GermanyGermanyDE0718759100123Haus der LügeOfficialnormaldeu1989US1989United StatesUnited StatesUS023138007123Front by FrontOfficialnormaleng1992US1992United StatesUnited StatesUS074645240626I: Lieder der Arbeiterklasse & Lieder aus dem spanischen BürgerkriegOfficialnormaldeu1989-05-02DE1989-05-02GermanyGermanyDE4007198839876Monarchie und AlltagOfficialnormaldeu1980DE1980GermanyGermanyDEMethods of SilenceOfficialnormalJewel Caseeng1989-06-05DE1989-06-05GermanyGermanyDE042283961326Basically SadOfficialnormalJewel Caseeng1986DE1986GermanyGermanyDE042283508224Electro RevengeOfficialnormaleng1991SE1991SwedenSwedenSE7391946035014Gold und LiebeOfficialnormaldeu1981-11DE1981-11GermanyGermanyDEFlags of RevolutionOfficialnormaleng1990DE1990GermanyGermanyDE
================================================
FILE: test/data/collection/20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml
================================================
event collectionalastairpT on the Fringe 20062006-08-042006-08-30
================================================
FILE: test/data/collection/2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml
================================================
work collectionalastairpMaggot Brain
================================================
FILE: test/data/collection/29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml
================================================
artist collectionalastairpQueenQueenUK rock group1970-06-27
================================================
FILE: test/data/collection/855b134e-9a3b-4717-8df8-8c4838d89924-places.xml
================================================
place collectionalastairpSan Francisco Bath Houseaka 'San Fran'171 Cuba Street, Wellington, New Zealand
================================================
FILE: test/data/collection/a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml
================================================
recording collectionalastairpMaggot Brain1201000
================================================
FILE: test/data/discid/f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml
================================================
217245179902645238762550527899096705109755126972137342156600171900188400203475GeräuschOfficialnormaldeu2003AT2003AustriaAustriaAT0602498655023B000UH8BIGfalse0falsefalseSchwarzes Geräusch1CD217245179902645238762550527899096705109755126972137342156600171900188400203475Rotes Geräusch2CD2153001501824534376457736290385481102576120412139696156229174924184535192889GeräuschOfficialnormalOtherdeu2003-09-29DE2003-09-29GermanyGermanyDE4019593899829B0000AN32Dtrue3truetrueSchwarzes Geräusch1CD217245179902645238762550527899096705109755126972137342156600171900188400203475Rotes Geräusch2CD21530015018245343764577362903854811025761204121396961562291749241845351928892171401501839834682462326351586246103494121483140920157606176455186219194727
================================================
FILE: test/data/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-.xml
================================================
21207518233322525977351098882136180169185187490Tales of EphidrinaOfficialnormalJewel Caseeng1993-07-05GB1993-07-05United KingdomUnited KingdomGB077778823827B000026GLQfalse0falsefalse1CD21211522233362526377355098922136220169225187530212043150332905256573478988501361481691531874582119121503315052428733409871513601516901518732321207518233322525977351098882136180169185187490Tales of EphidrinaOfficialnormaleng1993CA1993CanadaCanadaCAfalse0falsefalse1CD21211522233362526377355098922136220169225187530212043150332905256573478988501361481691531874582119121503315052428733409871513601516901518732321207518233322525977351098882136180169185187490Tales of EphidrinaOfficialnormaleng1993-07-30US1993-07-30United StatesUnited StatesUS017046610124B000003RVAfalse0falsefalse1CD21211522233362526377355098922136220169225187530212043150332905256573478988501361481691531874582119121503315052428733409871513601516901518732321207518233322525977351098882136180169185187490
================================================
FILE: test/data/event/770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml
================================================
1987-06-07: Rock am Ring, Nürburgring, Nürburg, Germany1987-06-071987-06-077643f13a-dcda-4db4-8196-3ffcc1b99ab7NürburgringNürburgring Boulevard 1, 53520 Nürburg50.335566.9475
================================================
FILE: test/data/event/e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml
================================================
Skunk D.F. @ Sala Arena, Madrid (Gira 20 aniversario)2014-12-142014-12-14* [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]f9113809-1403-4575-8c20-61bfa96b48dbbackwardSkunk D.F.Skunk D.F.redundant-title
================================================
FILE: test/data/instrument/01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml
================================================
kemencheVarious types of stringed bowed musical instruments having their origin in the Eastern Mediterranean04a21d03-535a-4ace-9098-12013867b8e5backwardfiddlead09a4ed-d1b6-47c3-ac85-acb531244a4dkemençe of the Black SeaTurkish box-shaped kemenche, mainly used for folk music.b9692581-c117-47f3-9524-3deeb69c6d3fclassical kemençeTurkish bowl-shaped kemenche, mainly used in classical Ottoman music
================================================
FILE: test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml
================================================
bassBass is a common but generic credit which refers to more than one instrument, the most common being the bass guitar and the double bass (a.k.a. contrabass, acoustic upright bass, wood bass). Please use the correct instrument if you know which one is intended.BassBassobaixobajo (genérico, no usar)basbasbasbasbassbassbassesbassoμπάσοベース
================================================
FILE: test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml
================================================
bassBass is a common but generic credit which refers to more than one instrument, the most common being the bass guitar and the double bass (a.k.a. contrabass, acoustic upright bass, wood bass). Please use the correct instrument if you know which one is intended.fixmenever use thisplease don't use this
================================================
FILE: test/data/instrument/9447c0af-5569-48f2-b4c5-241105d58c91.xml
================================================
bass saxophoneThe bass saxophone is the second largest existing member of the saxophone family (not counting the subcontrabass tubax). It is similar in design to a baritone saxophone, but it is larger, with a longer loop near the mouthpiece.
================================================
FILE: test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml
================================================
bullroarerA bullroarer consists of a piece of wood attached to a long cord which is then swung in a circle.Hornbostel-Sachs: 412.22
================================================
FILE: test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml
================================================
bullroarerA bullroarer consists of a piece of wood attached to a long cord which is then swung in a circle.http://en.wikisource.org/wiki/1911_Encyclop%C3%A6dia_Britannica/Bullroarerhttp://www.wikidata.org/wiki/Q666971https://commons.wikimedia.org/wiki/File:Bull_roarers.jpg
================================================
FILE: test/data/instrument/dabdeb41-560f-4d84-aa6a-cf22349326fe.xml
================================================
tarluteThe tar is a long-necked, waisted lute found in Azerbaijan, Iran, Armenia, Georgia, and other areas near the Caucasus region. Not to be confused with the drum of the same name.
================================================
FILE: test/data/label/022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml
================================================
================================================
FILE: test/data/label/e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml
================================================
================================================
FILE: test/data/place/0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml
================================================
All Saints' ChurchEast Finchley, Durham Road38 Durham Road, London N2 9DP, United Kingdom51.591812-0.159699LondonLondon1891All Saints' Durham Roadtype=church
================================================
FILE: test/data/place/browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml
================================================
440 StudiosAdrian Carr StudiosArt D'Lugoff's Top of the GateApex StudiosAtomic Heart StudiosArf MasteringAudioMASTER VargasNew York, NY 11367A&R Recording Studio322 West 48 Street40.761498-73.988169A.C. Pianocraft Recital HallAura Recording Studios136 West 52nd Street, New York, New York 100193rd Floora.l.l. digitalAllido Studiosstudio for Mark Ronson's Allido labelAllegro Sound Studios1650 Broadway, West 51st Street, 7th Avenuewas later renamed [http://www.discogs.com/label/267460-Generation-Sound-Studios|Generation Sound Studios], according to wikipedia: https://en.wikipedia.org/wiki/Brill_Building#1650_BroadwayAmerican Record Corporation Studios1776 Broadway, NYCAcademy of MusicNew York City125 East 14th Street, New York, NY 1000340.734568-73.98848918541926trueAvator StudioAvatar Studios, Studio A441 West 53rd Street, New York, NY 1001940.766389-73.989444Anderson Theater66 2nd Ave40.725979-73.989571957-081977true39th Street Music StudioAlgoRhythms321 StudiosApollo Theater253 West 125th Street, Manhattan, New York City, USA40.810047-73.950151914Aeolian Hall29-33 West 42nd Street, New York19121926trueAvatar Studios441 West 53rd Street, New York, NY 1001940.766389-73.9894441996-05
================================================
FILE: test/data/recording/f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml
================================================
Shades of Gray25960009b1131d-9803-4acf-8b3c-3438e6c2b9c7backwardTom JungJung, Tom09b1131d-9803-4acf-8b3c-3438e6c2b9c7backwardTom JungJung, Tom83c6ecce-ebc2-4064-ad28-49c7354469f4backwardpianoBilly BarberBarber, Billy83c6ecce-ebc2-4064-ad28-49c7354469f4backwardBilly BarberBarber, Billy
================================================
FILE: test/data/release/212895ca-ee36-439a-a824-d2620cd10461-recordings.xml
================================================
We♥TechPara -mission style-OfficialnormalKeep Caseeng2006JP2006JapanJapanJP4988064913695B000EIF602true11truetrue1
================================================
FILE: test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml
================================================
Ruined SubjectsOfficialNonenormalengJT BruceBruce, JT2011-08-09XW1
================================================
FILE: test/data/release/8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml
================================================
Sea of CowardsOfficialnormalJewel Caseeng2010-05-10GB2010-05-10United KingdomUnited KingdomGB093624966524BT00CHI1V2true1truefalse10035000[untitled]35000
================================================
FILE: test/data/release/9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml
================================================
Ramasser les morceaux : Le punk pour les nul-le-sOfficialnormalCardboard/Paper Sleevemul2013-05FR2013-05FranceFranceFRfalse0falsefalse1
================================================
FILE: test/data/release/a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml
================================================
Bored BoredOfficialnormaleng1978GB1
================================================
FILE: test/data/release/b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml
================================================
My Albumnormal1
================================================
FILE: test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml
================================================
Suzuki Piano School, Volume 2 (feat. piano: Haruko Katakoa)OfficialJewel CasenormalengSuzuki Method InternationalSuzuki Method InternationalUS029156150346087487498X1
================================================
FILE: test/data/release/fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml
================================================
UrkOfficialnormalDigipakeng2007-02-02DE2007-02-02GermanyGermanyDE886970007092B000LXHGR6false0falsefalse123
================================================
FILE: test/data/release-group/f52bc6a1-c848-49e6-85de-f8f53459a624.xml
================================================
Super Meat Boy!2010-10-27AlbumSoundtrack
================================================
FILE: test/data/search-artist.xml
================================================
Dynamo GoDynamo GoNZ2005-06Dynamo GotestfooDynamoDynamoDynamoDynamoDynamoFIFinnish punk bandDynamopunkfinlandDynamoDynamoDEgerman DIN label owner Torsten PröfrockDynamoDynamoDynamodesignerDynamo ProductionsDynamo ProductionsDynamo ProducionsDynamo ProductionsDynamo CityDynamo CityDynamo CityDynamo 5Dynamo 5Dynamo 5Kruunuhaan DynamoKruunuhaan DynamoKruunuhaan DynamoKidd DynamoKidd DynamoKidd DynamoOnslaught DynamoOnslaught DynamoOnslaught DynamoDynamo ElectrixDynamo ElectrixDynamo ElectrixDynamo SkaDynamo SkaDynamo SkaDynamo ChapelDynamo ChapelDynamo ChapelJohnny DynamoDynamo, JohnnyJohnny DynamoDynamo FrüchtebonusDynamo FrüchtebonusATDynamo FrüchtebonusDynamo and JPDynamo and JPDynamo and JPThe Driven DynamoDriven Dynamo, TheThe Driven DynamoTuttle & Dynamo LaboratoryTuttle & Laboratory, DynamoTuttle & Dynamo LaboratoryThe Dynamo HymnDynamo Hymn, TheThe Dynamo HymnGo!Go!USGo!GoGoGoGO!GO!7188GO!GO!7188JP1998-062012-02-10GOGO7188GO! GO! 7188GO!GO!7188GOGO 7188rockjapaneseGo Robot, Go!Go Robot, Go!Go Robot, Go!Gaijin A Go GoGaijin A Go GoGaijin A Go Go
================================================
FILE: test/data/search-event.xml
================================================
Woodstock 1999Woodstock Music & Art Fair19691969-08-151969-08-18backwardYasgur's FarmWoodstock 1999, Day 3, East Stage1999-07-241999-07-24backwardLimp BizkitLimp Bizkit
================================================
FILE: test/data/search-instrument.xml
================================================
bassBass is a common but generic credit which refers to more than one instrument, the most common being the bass guitar and the double bass (a.k.a. contrabass, acoustic upright bass, wood bass). Please use the correct instrument if you know which one is intended.ベースbaixobajo (genérico, no usar)basbasbasbasbassbassBassbassesbassoBassoμπάσοfixmenever use thisplease don't use thiskeyboard bassbajo tecladobaskeyboardbassklahvpillclavier bassekeyboard bassKeyboard-Basskosketinbassoπλήκτρα μπάσοbass guitarBass (modern, typically electrical, but not always)ベースギターbajobasgitaarbas gitarbas gitarabassBassgitarrebass guitarbasskitarrbassokitaragitara basowaguitare basseμπάσο κιθάραbass synthesizerA bass synthesizer is used to create sounds in the bass range.ベースシンセサイザーbas sintisajzerbassisüntesaatorbassosyntetisaattoribass synthesizerBass-Synthesizerbassynthesizerbas synthesizersynthétiseur basseμπάσο συνθεσάιζερбасовый синтезаторbass drumフィンガースナップbas bubanjbass davulbass drumbassorumpubasstrummbombogrosse caissegroße Trommelgrote tromμπάσο τύμπανοbass saxophoneThe bass saxophone is the second largest existing member of the saxophone family (not counting the subcontrabass tubax). It is similar in design to a baritone saxophone, but it is larger, with a longer loop near the mouthpiece.バスサクソフォーンbas saksofonbas saksofonbassaxofoonbassosaksofonibass-saksofonBasssaxophonbass saxophonesaxofón bajoSaxophone basseμπάσο σαξόφωνοdouble bassThe double bass, also known as contrabass or upright bass as well as many other names, is the largest and lowest-pitched bowed string instrument of the violin family in the modern symphony orchestra.ウッドベースストリングベースacoustic upright bassbasbass fiddlebass violbass violinbull fiddlecontrabajocontrabascontrabasscontrebassedoghouse bassdoublebassdouble basskontrabassKontrabassstaande basstand-up bassstring bassupright basswood bassκοντραμπάσοόρθιο μπάσοbass recorderバス・リコーダーbasblokfluitbas kljunasta flauta / f -bas kljunasta flautaBassblockflöteBass-BlockflöteBaßblockflöteBaß-Blockflötebassplokkflöötbass recorderf-bassonokkahuiluF-bass recorderflautín bajo (en fa)flûte à bec bassebass harmonicaThe bass harmonica is a type of octave harmonica where the lowest note (E) is the same as that on a bass guitar.armónica bajobas harmonikabas harmonikabasmondharmonicabass harmonicaBass-Mundharmonikabassohuuliharppubass-suupillharmonica basseμπάσο φυσαρμόνικαgreat bass recorder / c-bass recorderグレートバス・リコーダーc-bassonokkahuiluC-bass recorderflûte à bec grande bassegreat bass recordergreat bass recorder / c-bass recordergrootbasblokfluitGroßbassblockflöteGroßbass-BlockflöteGroßbaßblockflöteGroßbaß-Blockflötesuur bassplokkflöötelectric bass guitarエレクトリックベースbaixo elétricobajo eléctricoE-Bassel-basguitarelectric bass guitarelektribasskitarrelektrische basgitaarelektryczna gitara basowaguitare bassesähköbassoηλεκτρικό μπάσοwashtub bassContrebassineWaschwannenbasswashtub basswastobbe-basbass pedalsbas pedalebaspedalenbassipedaalidbassopedaalitBasspedalbass pedalsPédales bassesbass tromboneバストロンボーンbassopasuunaBassposaunebass trombonebasstromboonbas trombonbas trombonbastrombonetrombón bajotrombone basseμπάσο τρομπόνιfretless bassvariety of bass guitars without fretsフレットレスベースastmetraatideta (krihvideta) basskitarrbasse fretlessfretless bassfretloze basgitaarnauhaton bassoperdesiz basάταστο μπάσοbass clarinetThe bass clarinet is a clarinet, typically pitched an octave below the soprano B♭ clarinet.バス・クラリネットbasklarinetbas klarinetbas klarnetbass clarinetBassklarinettebassklarnetbassoklarinetticlarinete bajoclarinette basseμπάσο κλαρινέτοbass fluteThe bass flute is a flute, pitched one octave below the C concert flute, with a tube about 1.5 meters long.バス・フルートbas flautabasfluitbas flütbassflöötBassflötebass flutebassohuiluflauta bajoflûte basseμπάσο φλάουτοelectric upright bassエレクトリック・アップライト・ベースcontrabajo eléctricocontrebasse électriqueE-Kontrabasselectric upright basselektrikontrabasselektrische contrabasel-kontrabasηλεκτρικό όρθιο μπάσοgong bass drumA gong bass drum is a large single drumhead which resembles a gong.gong bass drumgong drumgrote gongtromtambour gongacoustic bass guitarアコースティック・ベース・ギターacoustic bass guitarakoestische basgitaarakustična bas gitaraakustik bas gitarakustiline basskitarrakustinen bassokitaraakustische Bassgitarreakustisk basguitarakustyczna gitara basowabaixo acústicobajo acústicoBasso acusticoguitare basse acoustiqueακουστικό μπάσοbass trumpetThe bass trumpet is a type of low trumpet similar to the valve trombone.bass oboeThe bass oboe is a double reed woodwind instrument which is about twice the size of a regular oboe.baritone oboehautbois barytondaruanThe daruan is a Chinese plucked lute.bass ruandaruandaruandaruanDaruandàruǎndàruǎn大阮
================================================
FILE: test/data/search-label.xml
================================================
================================================
FILE: test/data/search-place.xml
================================================
O2 Academy NewcastleO2 Academy Newcastle, Westgate Road, NE1 1SW54.97042-1.618654Newcastle upon TyneNewcastle upon Tyne2005falseO2 GuildhallSouthamptonWest Marlands Road, Civic Centre, SO14 7LP50.908154-1.406003SouthamptonSouthampton1937falseSouthampton GuildhallO₂ ArenaPrague, formerly Sazka ArenaOcelářská 460, 190 00 Praha 9, Czech Republic50.1067514.496647PrahaPraha2004-03-27falseO2 ArenaSazka ArenaO₂ Academy IslingtonAngel Central, 16 Parkfield Street, Islington, N1 0PS51.534805-0.105733IslingtonIslingtonfalseO2 Academy IslingtonO₂ Apollo ManchesterStockport Road Ardwick Green Manchester M12 6AP53.469566-2.222393ArdwickArdwick1938-08-29falseO2 Apollo ManchesterO2 Shepherd's Bush EmpireShepherd's Bush Green, London, W12 8TT51.50349-0.22433Hammersmith and FulhamHammersmith and FulhamfalseThe O2 ArenaDrawdock Road, North Greenwich, London, SE10 0BB England, UK51.5030.003133GreenwichGreenwich2007-06-24falseNorth Greenwich ArenaO2 Academy Glasgow121 Eglinton St, Glasgow G5 9NT55.8505-4.25906GlasgowGlasgowfalseO2 Academy Oxfordpreviously called The Zodiac190 Cowley Road, Oxford OX4 1UE51.74691-1.234472OxfordOxford1995falseCarling Academy OxfordThe ZodiacO2 Academy Leedsformerly Town and Country Club & Creation NightclubCookridge Street, Leeds, West Yorkshire, England53.802044-1.547069LeedsLeeds1992falseCreation NightclubLeeds AcademyTown and Country ClubO2 Academy Bournemouth570 Christchurch Rd, Bournemouth, BH1 4BH50.72671-1.839658BournemouthBournemouth1895-05-17falseAcademy NightclubBoscombe Grand TheatreBoscombe HippodromeOpera HouseStarkers Royal Arcade BallroomsO2 Academy Birmingham16-18 Horsefair, Birmingham B1 1DB52.473267-1.900201BirminghamBirmingham2009-09-10falseBrooklyn Bowl LondonBrooklyn Bowl London, The O2, Peninsula Square, London SE10 0DX51.503030.003139GreenwichGreenwich2014-01-17falseBarclaycard ArenaHamburg, formerly "Color Line Arena" and "O₂ World Hamburg"Sylvesterallee 10, 22525 Hamburg, Germany53.589179.899167HamburgHamburg2002-11-08falseColorline ArenaColor Line ArenaO2 World HamburgO₂ World Hamburg
================================================
FILE: test/data/search-recording.xml
================================================
Thief of Hearts198586Dynamo GoDynamo GoFolly, Vice & MadnessOfficial2006-11-03NZ51CDWaiting My Turn187706Dynamo GoDynamo GoFolly, Vice & MadnessOfficial2006-11-03NZ51CDJust a Victim226000Dynamo GoDynamo GoFolly, Vice & MadnessOfficial2006-11-03NZ51CDRetail Guru114493Dynamo GoDynamo GoFolly, Vice & MadnessOfficial2006-11-03NZ51CDIf this Beard Had Wings253346Dynamo GoDynamo GoFolly, Vice & MadnessOfficial2006-11-03NZ51CDHeadrush45973Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDFinal Reunion193253Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDNothing Ever Happens92013Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDJohnny the Punk190640Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDAlready Met Your Mother231706Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDRoute 17205106Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDSad Again181053Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDFountain City172760Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDCrying On My Street144306Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDFrozen to the Bone166920Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDWhat Went Wrong?148826Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDYou Fell From The Sky194826Dynamo GoDynamo GoThe Fool of Fountain CityOfficial2009-04-03NZ121CDPoor Alfred151000Dynamo GoDynamo GoPoor AlfredOfficial2010NZ21Digital MediaTalk Quickly120000Dynamo GoDynamo GoPoor AlfredOfficial2010NZ21Digital MediaSad Again275426Dynamo GoDynamo GoAffordable Pop MusicOfficial2008-07-04NZ41CDDead By Morning115933Dynamo GoDynamo GoAffordable Pop MusicOfficial2008-07-04NZ41CDNew Bed Science204613Dynamo GoDynamo GoAffordable Pop MusicOfficial2008-07-04NZ41CDMiddle Class200360Dynamo GoDynamo GoAffordable Pop MusicOfficial2008-07-04NZ41CDThief294826Third DayThird DayOfferingsOfficial2000-07-11US111CDThief306333CANCANGerman rock bandDelay 1968Official1981US71Delay 1968Official2006-05-29GB71SACDDelay 1968Official1981DE71VinylDelay 1968Official1989CH71CDto radiohead stop ruining can
================================================
FILE: test/data/search-release-group.xml
================================================
Affordable Pop MusicDynamo GoDynamo GoAffordable Pop MusicAffordable ArtSteve GoodmanGoodman, SteveAffordable ArtAffordable LuxurySpielerfrauSpielerfrauAffordable LuxuryPop MusicEva BraunEva BraunSerbian pop-rock bandPop MusicPop MusicThierry HazardHazard, ThierryPop MusicPop MusicPop MusicTodor KobakovKobakov, TodorPop MusicPop MusicIggy PopPop, IggyPop MusicStereophonic Pop Art MusicAlpha StoneAlpha StoneStereophonic Pop Art MusicPop Music for DancingTed Atking & His OrchestraTed Atking & His OrchestraPop Music for DancingThis Is Pop Music.Various ArtistsVarious Artistsadd compilations to this artistVarious ArtistsThis Is Pop Music.Pop The MusicTriimTriimPop The MusicBetter Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsBetter Pop MusicDance / Pop MusicNeil WatsonWatson, NeilMark SandellSandell, MarkDance / Pop MusicPepsi Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsPepsi Pop MusicKellogg's Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsKellogg's Pop MusicSwedish Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsSwedish Pop MusicPop Music HighlightsVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsPop Music United EPPaul AvionAvion, PaulPop Music UnitedThis Is Pop MusicEspen LindLind, EspenThis Is Pop MusicThis Is Pop MusicPop! Justice: 100% Solid Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsPop! Justice: 100% Solid Pop Musicbarbadian20 Years of Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists20 Years of Pop MusicProiect Special OMV: Pop MusicVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsProiect Special OMV: Pop MusicDance / Pop Music 4Various ArtistsVarious Artistsadd compilations to this artistVarious ArtistsDance / Pop Music 4Dance / Pop Music 2Neil WatsonWatson, NeilMark SandellSandell, MarkDance / Pop Music 2Dance / Pop Music 3Neil WatsonWatson, NeilMark SandellSandell, MarkDance / Pop Music 3
================================================
FILE: test/data/search-release.xml
================================================
Affordable Pop MusicOfficialengDynamo GoDynamo Go2008-07-04NZ9421021463277WS064CDAffordable LuxuryOfficialengSpielerfrauSpielerfrau2005US5Affordable ArtSteve GoodmanGoodman, Steve1983B000000DLV12Pop MusicOfficialfraThierry HazardHazard, Thierry1990-11FR5099746735425B00004UI3D467354 212CDPop MusicOfficialfraThierry HazardHazard, Thierry1990-11FR5099746735449467354 412CassettePop MusicOfficialengTodor KobakovKobakov, Todor2009-10-13CAB002MED6QW11Pop MusicOfficialengIggy PopPop, IggyB0000072ZP20Pop MusicOfficialsrpEva BraunEva BraunSerbian pop-rock band1995CS15CassetteThis Is Pop MusicOfficialengEspen LindLind, Espen2000NOB00005L90U10CDPop The MusicPromotionengTriimTriim2005FR1This Is Pop MusicengEspen LindLind, Espen2001JPB00005L90U11CDStereophonic Pop Art MusicOfficialAlpha StoneAlpha StoneB000003JGG8Pepsi Pop MusicPromotionengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists8Swedish Pop MusicPromotionengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists2009SE20CDBetter Pop MusicPromotionengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists2010-05-20GB19Digital MediaKellogg's Pop MusicPromotionengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists1998US634133052492DPSM 52492CDThis Is Pop Music.PromotionengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists5Pop Music UnitedOfficialengPaul AvionAvion, Paul2006US837101239905B000K97S8I8CDDance / Pop MusicOfficialengNeil WatsonWatson, NeilMark SandellSandell, MarkUSCHAPAV 4717Digital MediaPop Music for DancingOfficialengTed Atking & His OrchestraTed Atking & His Orchestra1970FRSPS 131112VinylPop! Justice: 100% Solid Pop MusicOfficialengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists2006-10-23GBB000JCESCU23Proiect Special OMV: Pop MusicPromotionengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists2007RO16CD20 Years of Pop MusicOfficialengVarious ArtistsVarious Artistsadd compilations to this artistVarious Artists2001-07-03USB00005LNIL15Cairo Cafe: Arabic Pop Musicara[unknown][unknown]Special Purpose Artist - Do not add releases here, if possible.200812CDDance / Pop Music 4OfficialengVarious ArtistsVarious Artistsadd compilations to this artistVarious ArtistsUSCHAPAV 10244Digital Media
================================================
FILE: test/data/search-work.xml
================================================
My Best FriendMy Best Friend (short cut)backwardMarushaMarushabackwardMarushaMarushaThe Girl of My Best FriendGirl of My Best FriendbackwardSam BobrickBobrick, SambackwardRoss ButlerButler, Ross(At Your Best) You Are LoveAt Your Best (You Are Love)backwardThe Isley BrothersIsley Brothers, ThebackwardThe Isley BrothersIsley Brothers, TheBestbackward양정승Yang, Jung-Seungbackward양정승Yang, Jung-SeungBest Guess at BestbackwardStephen RippyRippy, StephenBest BelievebackwardRedmanRedmanbackwardPete RockRock, PeteThe BestbackwardHolly KnightKnight, HollybackwardMichael ChapmanChapman, MichaelBest ForgottenbackwardMinco EggersmanEggersman, MincoBest Painbackward[K][K]backward[K][K]Best FriendbackwardHIROHIRObackwardHIROHIROBest FriendsbackwardHans ZimmerZimmer, HansbackwardHeitor PereiraPereira, HeitorbackwardRyeland AllisonAllison, RyelandbackwardJames S. LevineLevine, James S.Sunday BestBest FriendbackwardToni BraxtonBraxton, TonibackwardVance TaylorTaylor, VanceBest ManbackwardBryan-Michael CoxCox, Bryan-MichaelBest ThingbackwardJames YoungYoung, JamesbackwardDennis DeYoungDeYoung, DennisBest FriendsbackwardTimothy MosleyMosley, TimothybackwardMelissa ElliottElliott, MelissaBest DressbackwardJann ArdenArden, JannbackwardRussell BroomBroom, RussellBest DefensebackwardJames GulottaGulotta, JamesbackwardChris LykinsLykins, ChrisBest FuturebackwardBrad LanerLaner, BradMy BestT-702.448.462-8backwardNili HadidaHadida, NilibackwardBenjamin CottoCotto, Benjamin2nd BestbackwardMark Charles HeidingerHeidinger, Mark CharlesBest FriendT-101.813.896-8backward玉城千春Tamashiro, Chiharubackward玉城千春Tamashiro, ChiharuBest friendsbackward齋藤真也Saito, Shinyabackward黒崎真音Kurosaki, MaonBest FriendsAdam WarRock songSecond BestbackwardKevin HearnHearn, KevinbackwardSteven PagePage, StevenbackwardEd RobertsonRobertson, Ed
================================================
FILE: test/data/work/3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml
================================================
Symphony No. 2 in E minor, Op. 27: III. AdagioAdagio from Symphony No. 2 in E minor, Op. 27Adagio from Symphony No. 2 in E minor, Op. 27III. Adagio from Symphony No. 2 in E minor, Op. 27Sinfonie Nr. 2 e-moll, Op. 27: III. AdagioSymphonie No. 2 in E minor, Op. 27: III. AdagioSymphony No. 2 in E minor, Op. 27: III. AdagioSymphony No. 2 in E minor, Op. 27: III. AdagioSymphony No. 2 in E minor, Op. 27: III. AdagioSymphony No. 3 in A minor, Op. 44: II. Adagio ma non troppo교향곡 2번 3악장 "아다지오" [Symphony No. 2 in E minor, Op. 27: III. Adagio]
================================================
FILE: test/data/work/72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml
================================================
Cantata, BuxWV 1 "Accedite gentes, accurite populi"lat0790fa51-15d9-40a2-bca9-9c8eaaa96bef1backwardnumberBuxtehude-Werke-Verzeichnis
================================================
FILE: test/data/work/80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml
================================================
Symphony no. 3 in E-flat major, op. 55 "Eroica"zxxE-flat majorSymphonie Nr. 3 Es-Dur, Op. 55 "Eroica"Symphony No. 3, Op. 55 "Eroica"
================================================
FILE: test/data/work/8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml
================================================
Hüzzam PeşrevzxxHüzzamPeşrevFahte
================================================
FILE: test/test_browse.py
================================================
import unittest
import musicbrainzngs
from test import _common
class BrowseTest(unittest.TestCase):
def setUp(self):
self.opener = _common.FakeOpener()
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainzngs.set_useragent("a", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def test_browse(self):
area = "74e50e58-5deb-4b99-93a2-decbb365c07f"
musicbrainzngs.browse_events(area=area)
self.assertEqual("https://musicbrainz.org/ws/2/event/?area=74e50e58-5deb-4b99-93a2-decbb365c07f", self.opener.get_url())
def test_browse_includes(self):
area = "74e50e58-5deb-4b99-93a2-decbb365c07f"
musicbrainzngs.browse_events(area=area, includes=["aliases", "area-rels"])
self.assertEqual("https://musicbrainz.org/ws/2/event/?area=74e50e58-5deb-4b99-93a2-decbb365c07f&inc=aliases+area-rels", self.opener.get_url())
def test_browse_single_include(self):
area = "74e50e58-5deb-4b99-93a2-decbb365c07f"
musicbrainzngs.browse_events(area=area, includes="aliases")
self.assertEqual("https://musicbrainz.org/ws/2/event/?area=74e50e58-5deb-4b99-93a2-decbb365c07f&inc=aliases", self.opener.get_url())
def test_browse_multiple_by(self):
"""It is an error to choose multiple entities to browse by"""
self.assertRaises(Exception,
musicbrainzngs.browse_artists, recording="1", release="2")
def test_browse_limit_offset(self):
"""Limit and offset values"""
area = "74e50e58-5deb-4b99-93a2-decbb365c07f"
musicbrainzngs.browse_events(area=area, limit=50, offset=100)
self.assertEqual("https://musicbrainz.org/ws/2/event/?area=74e50e58-5deb-4b99-93a2-decbb365c07f&limit=50&offset=100", self.opener.get_url())
def test_browse_artist(self):
release = "9ace7c8c-55b4-4c5d-9aa8-e573a5dde9ad"
musicbrainzngs.browse_artists(release=release)
self.assertEqual("https://musicbrainz.org/ws/2/artist/?release=9ace7c8c-55b4-4c5d-9aa8-e573a5dde9ad", self.opener.get_url())
recording = "6da2cc31-9b12-4b66-9e26-074150f73406"
musicbrainzngs.browse_artists(recording=recording)
self.assertEqual("https://musicbrainz.org/ws/2/artist/?recording=6da2cc31-9b12-4b66-9e26-074150f73406", self.opener.get_url())
release_group = "44c90c72-76b5-3c13-890e-3d37f21c10c9"
musicbrainzngs.browse_artists(release_group=release_group)
self.assertEqual("https://musicbrainz.org/ws/2/artist/?release-group=44c90c72-76b5-3c13-890e-3d37f21c10c9", self.opener.get_url())
work = "deb27b88-cf41-4f7c-b3aa-bc3268bc3c02"
musicbrainzngs.browse_artists(work=work)
self.assertEqual("https://musicbrainz.org/ws/2/artist/?work=deb27b88-cf41-4f7c-b3aa-bc3268bc3c02", self.opener.get_url())
def test_browse_event(self):
area = "f03d09b3-39dc-4083-afd6-159e3f0d462f"
musicbrainzngs.browse_events(area=area)
self.assertEqual("https://musicbrainz.org/ws/2/event/?area=f03d09b3-39dc-4083-afd6-159e3f0d462f", self.opener.get_url())
artist = "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"
musicbrainzngs.browse_events(artist=artist)
self.assertEqual("https://musicbrainz.org/ws/2/event/?artist=0383dadf-2a4e-4d10-a46a-e9e041da8eb3", self.opener.get_url())
place = "8a6161bb-fb50-4234-82c5-1e24ab342499"
musicbrainzngs.browse_events(place=place)
self.assertEqual("https://musicbrainz.org/ws/2/event/?place=8a6161bb-fb50-4234-82c5-1e24ab342499", self.opener.get_url())
def test_browse_label(self):
release = "c9550260-b7ae-4670-ac24-731c19e76b59"
musicbrainzngs.browse_labels(release=release)
self.assertEqual("https://musicbrainz.org/ws/2/label/?release=c9550260-b7ae-4670-ac24-731c19e76b59", self.opener.get_url())
def test_browse_recording(self):
artist = "47f67b22-affe-4fe1-9d25-853d69bc0ee3"
musicbrainzngs.browse_recordings(artist=artist)
self.assertEqual("https://musicbrainz.org/ws/2/recording/?artist=47f67b22-affe-4fe1-9d25-853d69bc0ee3", self.opener.get_url())
release = "438042ef-7ccc-4d03-9391-4f66427b2055"
musicbrainzngs.browse_recordings(release=release)
self.assertEqual("https://musicbrainz.org/ws/2/recording/?release=438042ef-7ccc-4d03-9391-4f66427b2055", self.opener.get_url())
def test_browse_place(self):
area = "74e50e58-5deb-4b99-93a2-decbb365c07f"
musicbrainzngs.browse_places(area=area)
self.assertEqual("https://musicbrainz.org/ws/2/place/?area=74e50e58-5deb-4b99-93a2-decbb365c07f", self.opener.get_url())
def test_browse_release(self):
artist = "47f67b22-affe-4fe1-9d25-853d69bc0ee3"
musicbrainzngs.browse_releases(artist=artist)
self.assertEqual("https://musicbrainz.org/ws/2/release/?artist=47f67b22-affe-4fe1-9d25-853d69bc0ee3", self.opener.get_url())
musicbrainzngs.browse_releases(track_artist=artist)
self.assertEqual("https://musicbrainz.org/ws/2/release/?track_artist=47f67b22-affe-4fe1-9d25-853d69bc0ee3", self.opener.get_url())
label = "713c4a95-6616-442b-9cf6-14e1ddfd5946"
musicbrainzngs.browse_releases(label=label)
self.assertEqual("https://musicbrainz.org/ws/2/release/?label=713c4a95-6616-442b-9cf6-14e1ddfd5946", self.opener.get_url())
recording = "7484fcfd-1968-4401-a44d-d1edcc580518"
musicbrainzngs.browse_releases(recording=recording)
self.assertEqual("https://musicbrainz.org/ws/2/release/?recording=7484fcfd-1968-4401-a44d-d1edcc580518", self.opener.get_url())
release_group = "1c1b54f7-e56a-3ce8-b62c-e45c378e7f76"
musicbrainzngs.browse_releases(release_group=release_group)
self.assertEqual("https://musicbrainz.org/ws/2/release/?release-group=1c1b54f7-e56a-3ce8-b62c-e45c378e7f76", self.opener.get_url())
def test_browse_release_group(self):
artist = "47f67b22-affe-4fe1-9d25-853d69bc0ee3"
musicbrainzngs.browse_release_groups(artist=artist)
self.assertEqual("https://musicbrainz.org/ws/2/release-group/?artist=47f67b22-affe-4fe1-9d25-853d69bc0ee3", self.opener.get_url())
release = "438042ef-7ccc-4d03-9391-4f66427b2055"
musicbrainzngs.browse_release_groups(release=release)
self.assertEqual("https://musicbrainz.org/ws/2/release-group/?release=438042ef-7ccc-4d03-9391-4f66427b2055", self.opener.get_url())
release = "438042ef-7ccc-4d03-9391-4f66427b2055"
rel_type = "ep"
musicbrainzngs.browse_release_groups(release=release, release_type=rel_type)
self.assertEqual("https://musicbrainz.org/ws/2/release-group/?release=438042ef-7ccc-4d03-9391-4f66427b2055&type=ep", self.opener.get_url())
def test_browse_url(self):
resource = "http://www.queenonline.com"
musicbrainzngs.browse_urls(resource=resource)
self.assertEqual("https://musicbrainz.org/ws/2/url/?resource=http%3A%2F%2Fwww.queenonline.com", self.opener.get_url())
# Resource is urlencoded, including ? and =
resource = "http://www.splendidezine.com/review.html?reviewid=1109588405202831"
musicbrainzngs.browse_urls(resource=resource)
self.assertEqual("https://musicbrainz.org/ws/2/url/?resource=http%3A%2F%2Fwww.splendidezine.com%2Freview.html%3Freviewid%3D1109588405202831", self.opener.get_url())
def test_browse_work(self):
artist = "0383dadf-2a4e-4d10-a46a-e9e041da8eb3"
musicbrainzngs.browse_works(artist=artist)
self.assertEqual("https://musicbrainz.org/ws/2/work/?artist=0383dadf-2a4e-4d10-a46a-e9e041da8eb3", self.opener.get_url())
def test_browse_includes_is_subset_of_includes(self):
"""Check that VALID_BROWSE_INCLUDES is a strict subset of
VALID_INCLUDES"""
for entity, includes in musicbrainzngs.VALID_BROWSE_INCLUDES.items():
for i in includes:
self.assertTrue(i in musicbrainzngs.VALID_INCLUDES[entity], "entity %s, %s in BROWSE_INCLUDES but not VALID_INCLUDES" % (entity, i))
================================================
FILE: test/test_caa.py
================================================
import unittest
from musicbrainzngs import caa
from musicbrainzngs import compat
from musicbrainzngs.musicbrainz import _version
import musicbrainzngs
from test import _common
class CaaTest(unittest.TestCase):
def test_get_list(self):
# check the url and response for a listing
resp = b'{"images":[]}'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_image_list("8ec178f4-a8e8-4f22-bcba-1964466ef214")
self.assertEqual("https://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214", self.opener.myurl)
self.assertEqual(1, len(res))
self.assertTrue("images" in res)
def test_get_release_group_list(self):
# check the url and response for a listing
resp = b'{"images":[], "release": "foo"}'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_release_group_image_list("8ec178f4-a8e8-4f22-bcba-1964466ef214")
self.assertEqual("https://coverartarchive.org/release-group/8ec178f4-a8e8-4f22-bcba-1964466ef214", self.opener.myurl)
self.assertEqual(2, len(res))
self.assertTrue("images" in res)
self.assertEqual("foo", res["release"])
def test_list_none(self):
""" When CAA gives a 404 error, pass it through."""
exc = compat.HTTPError("", 404, "", "", _common.StringIO.StringIO(""))
self.opener = _common.FakeOpener(exception=musicbrainzngs.ResponseError(cause=exc))
musicbrainzngs.compat.build_opener = lambda *args: self.opener
try:
res = caa.get_image_list("8ec178f4-a8e8-4f22-bcba-19644XXXXXX")
self.assertTrue(False, "Expected an exception")
except musicbrainzngs.ResponseError as e:
self.assertEqual(e.cause.code, 404)
def test_list_baduuid(self):
exc = compat.HTTPError("", 400, "", "", _common.StringIO.StringIO(""))
self.opener = _common.FakeOpener(exception=musicbrainzngs.ResponseError(cause=exc))
musicbrainzngs.compat.build_opener = lambda *args: self.opener
try:
res = caa.get_image_list("8ec178f4-a8e8-4f22-bcba-19644XXXXXX")
self.assertTrue(False, "Expected an exception")
except musicbrainzngs.ResponseError as e:
self.assertEqual(e.cause.code, 400)
def test_set_useragent(self):
""" When a useragent is set it is sent with the request """
musicbrainzngs.set_useragent("caa-test", "0.1")
resp = b'{"images":[]}'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_image_list("8ec178f4-a8e8-4f22-bcba-1964466ef214")
headers = dict(self.opener.headers)
self.assertTrue("User-agent" in headers)
self.assertEqual("caa-test/0.1 python-musicbrainzngs/%s" % _version, headers["User-agent"])
def test_coverid(self):
resp = b'some_image'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_image("8ec178f4-a8e8-4f22-bcba-1964466ef214", "1234")
self.assertEqual("https://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/1234", self.opener.myurl)
self.assertEqual(resp, res)
def test_get_size(self):
resp = b'some_image'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_image("8ec178f4-a8e8-4f22-bcba-1964466ef214", "1234", 250)
self.assertEqual("https://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/1234-250", self.opener.myurl)
self.assertEqual(resp, res)
def test_front(self):
resp = b'front_image'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_image_front("8ec178f4-a8e8-4f22-bcba-1964466ef214")
self.assertEqual("https://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/front", self.opener.myurl)
self.assertEqual(resp, res)
def test_release_group_front(self):
resp = b'front_image'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_release_group_image_front("8ec178f4-a8e8-4f22-bcba-1964466ef214")
self.assertEqual("https://coverartarchive.org/release-group/8ec178f4-a8e8-4f22-bcba-1964466ef214/front", self.opener.myurl)
self.assertEqual(resp, res)
def test_back(self):
resp = b'back_image'
self.opener = _common.FakeOpener(resp)
musicbrainzngs.compat.build_opener = lambda *args: self.opener
res = caa.get_image_back("8ec178f4-a8e8-4f22-bcba-1964466ef214")
self.assertEqual("https://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/back", self.opener.myurl)
self.assertEqual(resp, res)
================================================
FILE: test/test_collection.py
================================================
import unittest
import musicbrainzngs
from musicbrainzngs import compat
from test import _common
class CollectionTest(unittest.TestCase):
""" Test that requesting collections works properly """
def setUp(self):
musicbrainzngs.set_useragent("a", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def test_auth_required(self):
""" Check the auth_required method in isolation """
ar = musicbrainzngs.musicbrainz._get_auth_type("collection", "", [])
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_YES, ar)
ar = musicbrainzngs.musicbrainz._get_auth_type("collection",
"foo/releases", [])
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_IFSET, ar)
ar = musicbrainzngs.musicbrainz._get_auth_type("artist", "5b11f4ce-a62d-471e-81fc-a69a8278c7da", [])
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_NO, ar)
ar = musicbrainzngs.musicbrainz._get_auth_type("artist", "5b11f4ce-a62d-471e-81fc-a69a8278c7da", ["user-tags"])
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_YES, ar)
ar = musicbrainzngs.musicbrainz._get_auth_type("artist", "5b11f4ce-a62d-471e-81fc-a69a8278c7da", ["aliases", "user-genres", "artist-rels"])
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_YES, ar)
def test_my_collections(self):
""" If you ask for your collections, you need to have
authenticated first."""
old_mb_request = musicbrainzngs.musicbrainz._mb_request
params = {}
def local_mb_request(path, method='GET',
auth_required=musicbrainzngs.musicbrainz.AUTH_NO,
client_required=False, args=None, data=None, body=None):
params["auth_required"] = auth_required
musicbrainzngs.musicbrainz._mb_request = local_mb_request
musicbrainzngs.get_collections()
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_YES,
params["auth_required"])
musicbrainzngs.musicbrainz._mb_request = old_mb_request
def test_other_collection(self):
""" If you ask for someone else's collection, you don't
need to be authenticated."""
old_mb_request = musicbrainzngs.musicbrainz._mb_request
params = {}
def local_mb_request(path, method='GET',
auth_required=musicbrainzngs.musicbrainz.AUTH_NO,
client_required=False, args=None, data=None, body=None):
params["auth_required"] = auth_required
musicbrainzngs.musicbrainz._mb_request = local_mb_request
musicbrainzngs.get_releases_in_collection(
"17905fdb-102d-40f0-91d3-eabcabc64fd3")
# If _get_auth_type() returns AUTH_IFSET, then _mb_request()
# should send the user credentials if they are set by auth()
# i.e., We use whether auth() has been executed to determine if
# the requested collection belongs to the user or not.
self.assertEqual(musicbrainzngs.musicbrainz.AUTH_IFSET,
params["auth_required"])
musicbrainzngs.musicbrainz._mb_request = old_mb_request
def test_no_collection(self):
""" If a collection doesn't exist, you get a 404 """
exc = compat.HTTPError("", 404, "", "", _common.StringIO.StringIO(""))
self.opener = _common.FakeOpener(exception=musicbrainzngs.ResponseError(cause=exc))
musicbrainzngs.compat.build_opener = lambda *args: self.opener
try:
res = musicbrainzngs.get_releases_in_collection("17905fdb-102d-40f0-91d3-eabcabc64f44")
self.assertTrue(False, "Expected an exception")
except musicbrainzngs.ResponseError as e:
self.assertEqual(e.cause.code, 404)
def test_private_collection(self):
""" If you ask for a collection that is private, you should
get a 401"""
exc = compat.HTTPError("", 401, "", "", _common.StringIO.StringIO(""))
self.opener = _common.FakeOpener(exception=musicbrainzngs.AuthenticationError(cause=exc))
musicbrainzngs.compat.build_opener = lambda *args: self.opener
try:
res = musicbrainzngs.get_releases_in_collection("17905fdb-102d-40f0-91d3-eabcabc64fd3")
self.assertTrue(False, "Expected an exception")
except musicbrainzngs.AuthenticationError as e:
self.assertEqual(e.cause.code, 401)
================================================
FILE: test/test_getentity.py
================================================
import unittest
import musicbrainzngs
from test import _common
class UrlTest(unittest.TestCase):
""" Test that the correct URL is generated when a search query is made """
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainzngs.set_useragent("a", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def testGetArtist(self):
artistid = "952a4205-023d-4235-897c-6fdb6f58dfaa"
musicbrainzngs.get_artist_by_id(artistid)
self.assertEqual("https://musicbrainz.org/ws/2/artist/952a4205-023d-4235-897c-6fdb6f58dfaa", self.opener.get_url())
# Test an include
musicbrainzngs.get_artist_by_id(artistid, "recordings")
self.assertEqual("https://musicbrainz.org/ws/2/artist/952a4205-023d-4235-897c-6fdb6f58dfaa?inc=recordings", self.opener.get_url())
# More than one include
musicbrainzngs.get_artist_by_id(artistid, ["recordings", "aliases"])
expected ="https://musicbrainz.org/ws/2/artist/952a4205-023d-4235-897c-6fdb6f58dfaa?inc=recordings+aliases"
self.assertEqual(expected, self.opener.get_url())
# with valid filters
musicbrainzngs.get_artist_by_id(artistid, ["release-groups"],
release_type=["album"])
self.assertTrue("type=album" in self.opener.get_url())
# with invalid filters
self.assertRaises(musicbrainzngs.UsageError,
musicbrainzngs.get_artist_by_id,
artistid, ["release-groups"], release_status=["official"])
def testGetEvent(self):
event_id = "a4a0927c-8ad7-48dd-883c-7126cc0b9c6b"
musicbrainzngs.get_event_by_id(event_id)
self.assertEqual("https://musicbrainz.org/ws/2/event/a4a0927c-8ad7-48dd-883c-7126cc0b9c6b", self.opener.get_url())
# one include
musicbrainzngs.get_event_by_id(event_id, ["artist-rels"])
self.assertEqual("https://musicbrainz.org/ws/2/event/a4a0927c-8ad7-48dd-883c-7126cc0b9c6b?inc=artist-rels", self.opener.get_url())
musicbrainzngs.get_event_by_id(event_id, ["artist-rels", "event-rels", "ratings", "tags"])
self.assertEqual("https://musicbrainz.org/ws/2/event/a4a0927c-8ad7-48dd-883c-7126cc0b9c6b?inc=artist-rels+event-rels+ratings+tags", self.opener.get_url())
def testGetPlace(self):
place_id = "43e166a5-a024-4cbb-9a1f-d4947b4ff489"
musicbrainzngs.get_place_by_id(place_id)
self.assertEqual("https://musicbrainz.org/ws/2/place/43e166a5-a024-4cbb-9a1f-d4947b4ff489", self.opener.get_url())
musicbrainzngs.get_place_by_id(place_id, ["event-rels"])
self.assertEqual("https://musicbrainz.org/ws/2/place/43e166a5-a024-4cbb-9a1f-d4947b4ff489?inc=event-rels", self.opener.get_url())
def testGetLabel(self):
label_id = "aab2e720-bdd2-4565-afc2-460743585f16"
musicbrainzngs.get_label_by_id(label_id)
self.assertEqual("https://musicbrainz.org/ws/2/label/aab2e720-bdd2-4565-afc2-460743585f16", self.opener.get_url())
# one include
musicbrainzngs.get_label_by_id(label_id, "releases")
self.assertEqual("https://musicbrainz.org/ws/2/label/aab2e720-bdd2-4565-afc2-460743585f16?inc=releases", self.opener.get_url())
# with valid filters
musicbrainzngs.get_label_by_id(label_id, ["releases"],
release_type=["ep", "single"], release_status=["official"])
self.assertTrue("type=ep%7Csingle" in self.opener.get_url())
self.assertTrue("status=official" in self.opener.get_url())
def testGetRecording(self):
musicbrainzngs.get_recording_by_id("93468a09-9662-4886-a227-56a2ad1c5246")
self.assertEqual("https://musicbrainz.org/ws/2/recording/93468a09-9662-4886-a227-56a2ad1c5246", self.opener.get_url())
# one include
musicbrainzngs.get_recording_by_id("93468a09-9662-4886-a227-56a2ad1c5246", includes=["artists"])
self.assertEqual("https://musicbrainz.org/ws/2/recording/93468a09-9662-4886-a227-56a2ad1c5246?inc=artists", self.opener.get_url())
def testGetReleasegroup(self):
musicbrainzngs.get_release_group_by_id("9377d65d-ffd5-35d6-b64d-43f86ef9188d")
self.assertEqual("https://musicbrainz.org/ws/2/release-group/9377d65d-ffd5-35d6-b64d-43f86ef9188d", self.opener.get_url())
# one include
release_group_id = "9377d65d-ffd5-35d6-b64d-43f86ef9188d"
musicbrainzngs.get_release_group_by_id(release_group_id,
includes=["artists"])
self.assertEqual("https://musicbrainz.org/ws/2/release-group/9377d65d-ffd5-35d6-b64d-43f86ef9188d?inc=artists", self.opener.get_url())
# with valid filters
musicbrainzngs.get_release_group_by_id(release_group_id,
release_type=["compilation", "live"])
self.assertTrue("type=compilation%7Clive" in self.opener.get_url())
# with invalid filters
self.assertRaises(musicbrainzngs.UsageError,
musicbrainzngs.get_release_group_by_id,
release_group_id, release_status=["official", "promotion"])
def testGetWork(self):
musicbrainzngs.get_work_by_id("c6dfad5a-f915-41c7-a1c0-e2b606948e69")
self.assertEqual("https://musicbrainz.org/ws/2/work/c6dfad5a-f915-41c7-a1c0-e2b606948e69", self.opener.get_url())
def testGetByDiscid(self):
musicbrainzngs.get_releases_by_discid("I5l9cCSFccLKFEKS.7wqSZAorPU-")
self.assertEqual("https://musicbrainz.org/ws/2/discid/I5l9cCSFccLKFEKS.7wqSZAorPU-", self.opener.get_url())
includes = ["artists"]
musicbrainzngs.get_releases_by_discid("I5l9cCSFccLKFEKS.7wqSZAorPU-", includes)
self.assertEqual("https://musicbrainz.org/ws/2/discid/I5l9cCSFccLKFEKS.7wqSZAorPU-?inc=artists", self.opener.get_url())
musicbrainzngs.get_releases_by_discid("discid", toc="toc")
self.assertEqual("https://musicbrainz.org/ws/2/discid/discid?toc=toc", self.opener.get_url())
musicbrainzngs.get_releases_by_discid("discid", toc="toc", cdstubs=False)
self.assertEqual("https://musicbrainz.org/ws/2/discid/discid?cdstubs=no&toc=toc", self.opener.get_url())
def testGetInstrument(self):
musicbrainzngs.get_instrument_by_id("6505f98c-f698-4406-8bf4-8ca43d05c36f")
self.assertEqual("https://musicbrainz.org/ws/2/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f", self.opener.get_url())
# Tags
musicbrainzngs.get_instrument_by_id("6505f98c-f698-4406-8bf4-8ca43d05c36f", includes="tags")
self.assertEqual("https://musicbrainz.org/ws/2/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f?inc=tags", self.opener.get_url())
# some rels
musicbrainzngs.get_instrument_by_id("6505f98c-f698-4406-8bf4-8ca43d05c36f", includes=["instrument-rels", "url-rels"])
self.assertEqual("https://musicbrainz.org/ws/2/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f?inc=instrument-rels+url-rels", self.opener.get_url())
# alias, annotation
musicbrainzngs.get_instrument_by_id("d00cec5f-f9bc-4235-a54f-6639a02d4e4c", includes=["aliases", "annotation"])
self.assertEqual("https://musicbrainz.org/ws/2/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c?inc=aliases+annotation", self.opener.get_url())
# Ratings are used on almost all other entites but instrument
self.assertRaises(musicbrainzngs.UsageError,
musicbrainzngs.get_instrument_by_id,
"dabdeb41-560f-4d84-aa6a-cf22349326fe", includes=["ratings"])
================================================
FILE: test/test_mbxml.py
================================================
import unittest
from musicbrainzngs import mbxml
class MbXML(unittest.TestCase):
def testMakeBarcode(self):
expected = (b''
b'12345'
b'')
xml = mbxml.make_barcode_request({'trid':'12345'})
self.assertEqual(expected, xml)
def test_make_tag_request(self):
expected = (b''
b''
b''
b'one'
b'two'
b''
b'')
xml = mbxml.make_tag_request(artist_tags={"mbid": ["one", "two"]})
self.assertEqual(expected, xml)
def test_read_error(self):
error = 'Invalid mbid.For usage, please see: http://musicbrainz.org/development/mmd'
parts = mbxml.get_error_message(error)
self.assertEqual(2, len(parts))
self.assertEqual("Invalid mbid.", parts[0])
self.assertEqual(True, parts[1].startswith("For usage"))
================================================
FILE: test/test_mbxml_artist.py
================================================
# Tests for parsing of artist queries
import unittest
import os
from test import _common
class GetArtistTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "artist")
def testArtistAliases(self):
res = _common.open_and_parse_test_data(self.datadir, "0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml")
aliases = res["artist"]["alias-list"]
self.assertEqual(len(aliases), 28)
a0 = aliases[0]
self.assertEqual(a0["alias"], "Prokofief")
self.assertEqual(a0["sort-name"], "Prokofief")
a17 = aliases[17]
self.assertEqual(a17["alias"], "Sergei Sergeyevich Prokofiev")
self.assertEqual(a17["sort-name"], "Prokofiev, Sergei Sergeyevich")
self.assertEqual(a17["locale"], "en")
self.assertEqual(a17["primary"], "primary")
res = _common.open_and_parse_test_data(self.datadir, "2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml")
self.assertFalse("alias-list" in res["artist"])
def testArtistTargets(self):
res = _common.open_and_parse_test_data(self.datadir, "b3785a55-2cf6-497d-b8e3-cfa21a36f997-artist-rels.xml")
self.assertTrue('target-credit' in res['artist']['artist-relation-list'][0])
self.assertEqual(res['artist']['artist-relation-list'][0]["target-credit"], "TAO")
================================================
FILE: test/test_mbxml_collection.py
================================================
# Tests for parsing of collection queries
import unittest
import os
import musicbrainzngs
from test import _common
class UrlTest(unittest.TestCase):
""" Test that the correct URL is generated when a query is made """
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainzngs.set_useragent("test", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def testGetCollection(self):
musicbrainzngs.get_releases_in_collection("0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac")
self.assertEqual("https://musicbrainz.org/ws/2/collection/0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac/releases", self.opener.get_url())
musicbrainzngs.get_works_in_collection("898676a6-bc79-4fe2-98ae-79c5940fe1a2")
self.assertEqual("https://musicbrainz.org/ws/2/collection/898676a6-bc79-4fe2-98ae-79c5940fe1a2/works", self.opener.get_url())
musicbrainzngs.get_events_in_collection("65cb5dda-44aa-44a8-9c0d-4f99a14ab944")
self.assertEqual("https://musicbrainz.org/ws/2/collection/65cb5dda-44aa-44a8-9c0d-4f99a14ab944/events", self.opener.get_url())
musicbrainzngs.get_places_in_collection("9dde4c3c-520a-4bfd-9aae-446c3a04ce0c")
self.assertEqual("https://musicbrainz.org/ws/2/collection/9dde4c3c-520a-4bfd-9aae-446c3a04ce0c/places", self.opener.get_url())
musicbrainzngs.get_recordings_in_collection("42bc6dd9-8deb-4bd7-83eb-5dacdb218b38")
self.assertEqual("https://musicbrainz.org/ws/2/collection/42bc6dd9-8deb-4bd7-83eb-5dacdb218b38/recordings", self.opener.get_url())
musicbrainzngs.get_artists_in_collection("7e582256-b3ce-421f-82ba-451b0ab080eb")
self.assertEqual("https://musicbrainz.org/ws/2/collection/7e582256-b3ce-421f-82ba-451b0ab080eb/artists", self.opener.get_url())
class GetCollectionTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "collection")
def testCollectionInfo(self):
"""
Test that the id, name and author are given.
"""
res = _common.open_and_parse_test_data(self.datadir, "0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml")
def testCollectionReleases(self):
"""
Test that the list of releases is given.
"""
res = _common.open_and_parse_test_data(self.datadir, "0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml")
coll = res["collection"]
self.assertEqual(coll["id"], "0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac")
self.assertEqual(coll["name"], "My Collection")
self.assertEqual(coll["editor"], "JonnyJD")
self.assertEqual(coll["entity-type"], "release")
self.assertEqual(coll["type"], "Release")
self.assertEqual(coll["release-count"], 400)
self.assertTrue("release-list" in res["collection"])
def testCollectionWorks(self):
res = _common.open_and_parse_test_data(self.datadir, "2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml")
coll = res["collection"]
self.assertEqual(coll["id"], "2326c2e8-be4b-4300-acc6-dbd0adf5645b")
self.assertEqual(coll["name"], "work collection")
self.assertEqual(coll["editor"], "alastairp")
self.assertEqual(coll["entity-type"], "work")
self.assertEqual(coll["type"], "Work")
self.assertEqual(coll["work-count"], 1)
self.assertEqual(len(coll["work-list"]), 1)
def testCollectionArtists(self):
res = _common.open_and_parse_test_data(self.datadir, "29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml")
coll = res["collection"]
self.assertEqual(coll["id"], "29611d8b-b3ad-4ffb-acb5-27f77342a5b0")
self.assertEqual(coll["name"], "artist collection")
self.assertEqual(coll["editor"], "alastairp")
self.assertEqual(coll["entity-type"], "artist")
self.assertEqual(coll["type"], "Artist")
self.assertEqual(coll["artist-count"], 1)
self.assertEqual(len(coll["artist-list"]), 1)
def testCollectionEvents(self):
res = _common.open_and_parse_test_data(self.datadir, "20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml")
coll = res["collection"]
self.assertEqual(coll["id"], "20562e36-c7cc-44fb-96b4-486d51a1174b")
self.assertEqual(coll["name"], "event collection")
self.assertEqual(coll["editor"], "alastairp")
self.assertEqual(coll["entity-type"], "event")
self.assertEqual(coll["type"], "Event")
self.assertEqual(coll["event-count"], 1)
self.assertEqual(len(coll["event-list"]), 1)
def testCollectionPlaces(self):
res = _common.open_and_parse_test_data(self.datadir, "855b134e-9a3b-4717-8df8-8c4838d89924-places.xml")
coll = res["collection"]
self.assertEqual(coll["id"], "855b134e-9a3b-4717-8df8-8c4838d89924")
self.assertEqual(coll["name"], "place collection")
self.assertEqual(coll["editor"], "alastairp")
self.assertEqual(coll["entity-type"], "place")
self.assertEqual(coll["type"], "Place")
self.assertEqual(coll["place-count"], 1)
self.assertEqual(len(coll["place-list"]), 1)
def testCollectionRecordings(self):
res = _common.open_and_parse_test_data(self.datadir, "a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml")
coll = res["collection"]
self.assertEqual(coll["id"], "a91320b2-fd2f-4a93-9e4e-603d16d514b6")
self.assertEqual(coll["name"], "recording collection")
self.assertEqual(coll["editor"], "alastairp")
self.assertEqual(coll["entity-type"], "recording")
self.assertEqual(coll["type"], "Recording")
self.assertEqual(coll["recording-count"], 1)
self.assertEqual(len(coll["recording-list"]), 1)
================================================
FILE: test/test_mbxml_discid.py
================================================
# Tests for parsing of discid queries
import unittest
import os
import musicbrainzngs
from test import _common
class UrlTest(unittest.TestCase):
""" Test that the correct URL is generated when a search query is made """
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainzngs.set_useragent("test", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def testGetDiscId(self):
musicbrainzngs.get_releases_by_discid("xp5tz6rE4OHrBafj0bLfDRMGK48-")
self.assertEqual("https://musicbrainz.org/ws/2/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-", self.opener.get_url())
# one include
musicbrainzngs.get_releases_by_discid("xp5tz6rE4OHrBafj0bLfDRMGK48-",
includes=["recordings"])
self.assertEqual("https://musicbrainz.org/ws/2/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-?inc=recordings", self.opener.get_url())
# more than one include
musicbrainzngs.get_releases_by_discid("xp5tz6rE4OHrBafj0bLfDRMGK48-", includes=["artists", "recordings", "artist-credits"])
expected = "https://musicbrainz.org/ws/2/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-?inc=artists+recordings+artist-credits"
self.assertEqual(expected, self.opener.get_url())
class GetDiscIdTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "discid")
def testDiscId(self):
"""
Test that the id attribute of the disc is read.
"""
res = _common.open_and_parse_test_data(self.datadir, "xp5tz6rE4OHrBafj0bLfDRMGK48-.xml")
self.assertEqual(res["disc"]["id"], "xp5tz6rE4OHrBafj0bLfDRMGK48-")
def testTrackCount(self):
"""
Test that the number of tracks (offset-count) is returned.
"""
# discid without pregap track
res = _common.open_and_parse_test_data(self.datadir, "xp5tz6rE4OHrBafj0bLfDRMGK48-.xml")
self.assertEqual(res["disc"]["offset-count"], 8)
# discid with pregap track
# (the number of tracks does not count the pregap "track")
res = _common.open_and_parse_test_data(self.datadir, "f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml")
self.assertEqual(res["disc"]["offset-count"], 13)
def testOffsets(self):
"""
Test that the correct list of offsets is returned.
"""
res = _common.open_and_parse_test_data(self.datadir, "xp5tz6rE4OHrBafj0bLfDRMGK48-.xml")
offsets_res = res["disc"]["offset-list"]
offsets_correct = [182, 33322, 52597, 73510, 98882, 136180, 169185, 187490]
for i in range(len(offsets_correct)):
self.assertEqual(offsets_res[i], offsets_correct[i])
self.assertTrue(isinstance(offsets_res[i], int))
def testReleaseList(self):
"""
Test that a release list of correct size is given.
"""
res = _common.open_and_parse_test_data(self.datadir, "xp5tz6rE4OHrBafj0bLfDRMGK48-.xml")
self.assertEqual(res["disc"]["release-count"], 3)
self.assertEqual(res["disc"]["release-count"], len(res["disc"]["release-list"]))
================================================
FILE: test/test_mbxml_event.py
================================================
# Tests for parsing of event results
import unittest
import os
from test import _common
class EventTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "event")
def testCorrectId(self):
event_id = "770fb0b4-0ad8-4774-9275-099b66627355"
res = _common.open_and_parse_test_data(self.datadir, "%s-place-rels.xml" % event_id)
self.assertEqual(event_id, res["event"]["id"])
def testPlace(self):
event_id = "770fb0b4-0ad8-4774-9275-099b66627355"
res = _common.open_and_parse_test_data(self.datadir, "%s-place-rels.xml" % event_id)
place = res["event"]["place-relation-list"][0]["place"]
self.assertEqual("7643f13a-dcda-4db4-8196-3ffcc1b99ab7", place["id"])
self.assertEqual("50.33556", place["coordinates"]["latitude"])
self.assertEqual("6.9475", place["coordinates"]["longitude"])
def testType(self):
event_id = "770fb0b4-0ad8-4774-9275-099b66627355"
res = _common.open_and_parse_test_data(self.datadir, "%s-place-rels.xml" % event_id)
self.assertEqual("Concert", res["event"]["type"])
def testEventElements(self):
filename = "e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml"
res = _common.open_and_parse_test_data(self.datadir, filename)
e = res["event"]
keys = ["name", "life-span", "time", "setlist", "artist-relation-list", "tag-list"]
for k in keys:
self.assertTrue(k in e, "key %s in dict" % (k, ))
================================================
FILE: test/test_mbxml_instrument.py
================================================
# -*- coding: UTF-8 -*-
# Tests for parsing instrument queries
import unittest
import os
from test import _common
class GetInstrumentTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "instrument")
def testData(self):
res = _common.open_and_parse_test_data(self.datadir, "9447c0af-5569-48f2-b4c5-241105d58c91.xml")
inst = res["instrument"]
self.assertEqual(inst["id"], "9447c0af-5569-48f2-b4c5-241105d58c91")
self.assertEqual(inst["name"], "bass saxophone")
self.assertEqual(inst["type"], "Wind instrument")
self.assertTrue(inst["description"].startswith("The bass saxophone"))
def testAliases(self):
res = _common.open_and_parse_test_data(self.datadir, "6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml")
inst = res["instrument"]
aliases = inst["alias-list"]
self.assertEqual(len(aliases), 14)
self.assertEqual(aliases[1]["locale"], "it")
self.assertEqual(aliases[1]["type"], "Instrument name")
self.assertEqual(aliases[1]["primary"], "primary")
self.assertEqual(aliases[1]["sort-name"], "Basso")
self.assertEqual(aliases[1]["alias"], "Basso")
def testTags(self):
res = _common.open_and_parse_test_data(self.datadir, "6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml")
inst = res["instrument"]
tags = inst["tag-list"]
self.assertEqual(len(tags), 3)
self.assertEqual(tags[0]["name"], "fixme")
self.assertEqual(tags[0]["count"], "1")
def testUrlRels(self):
res = _common.open_and_parse_test_data(self.datadir, "d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml")
inst = res["instrument"]
rels = inst["url-relation-list"]
self.assertEqual(len(rels), 3)
self.assertEqual(rels[0]["type"], "information page")
self.assertEqual(rels[0]["type-id"], "0e62afec-12f3-3d0f-b122-956207839854")
self.assertTrue(rels[0]["target"].startswith("http://en.wikisource"))
def testAnnotations(self):
res = _common.open_and_parse_test_data(self.datadir, "d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml")
inst = res["instrument"]
self.assertEqual(inst["annotation"]["text"], "Hornbostel-Sachs: 412.22")
def testInstrumentRels(self):
res = _common.open_and_parse_test_data(self.datadir, "01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml")
inst = res["instrument"]
rels = inst["instrument-relation-list"]
self.assertEqual(len(rels), 3)
self.assertEqual(rels[1]["type"], "children")
self.assertEqual(rels[1]["type-id"], "12678b88-1adb-3536-890e-9b39b9a14b2d")
self.assertEqual(rels[1]["target"], "ad09a4ed-d1b6-47c3-ac85-acb531244a4d")
self.assertEqual(rels[1]["instrument"]["id"], "ad09a4ed-d1b6-47c3-ac85-acb531244a4d")
self.assertTrue(rels[1]["instrument"]["name"].startswith(b"kemen\xc3\xa7e".decode("utf-8")))
def testDisambiguation(self):
res = _common.open_and_parse_test_data(self.datadir, "dabdeb41-560f-4d84-aa6a-cf22349326fe.xml")
inst = res["instrument"]
self.assertEqual(inst["disambiguation"], "lute")
================================================
FILE: test/test_mbxml_label.py
================================================
# Tests for parsing of label queries
import unittest
import os
from test import _common
class GetLabelTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "label")
def testLabelAliases(self):
res = _common.open_and_parse_test_data(self.datadir, "022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml")
aliases = res["label"]["alias-list"]
self.assertEqual(len(aliases), 4)
a0 = aliases[0]
self.assertEqual(a0["alias"], "EMI")
self.assertEqual(a0["sort-name"], "EMI")
a1 = aliases[1]
self.assertEqual(a1["alias"], "EMI Records (UK)")
self.assertEqual(a1["sort-name"], "EMI Records (UK)")
res = _common.open_and_parse_test_data(self.datadir, "e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml")
aliases = res["label"]["alias-list"]
self.assertEqual(len(aliases), 1)
a0 = aliases[0]
self.assertEqual(a0["alias"], "Ki/oon Records Inc.")
self.assertEqual(a0["sort-name"], "Ki/oon Records Inc.")
self.assertEqual(a0["begin-date"], "2001-10")
self.assertEqual(a0["end-date"], "2012-04")
================================================
FILE: test/test_mbxml_place.py
================================================
# Tests for parsing of place results
import unittest
import os
from test import _common
class PlaceTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "place")
def testPlace(self):
filename = "0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml"
res = _common.open_and_parse_test_data(self.datadir, filename)
p = res["place"]
self.assertEqual("All Saints' Church", p["name"])
self.assertEqual("East Finchley, Durham Road", p["disambiguation"])
self.assertEqual("38 Durham Road, London N2 9DP, United Kingdom", p["address"])
self.assertEqual({"latitude": "51.591812", "longitude": "-0.159699"}, p["coordinates"])
self.assertEqual("f03d09b3-39dc-4083-afd6-159e3f0d462f", p["area"]["id"])
self.assertEqual("1891", p["life-span"]["begin"])
self.assertEqual("All Saints' Durham Road", p["alias-list"][0]["alias"])
self.assertEqual("type=church", p["tag-list"][0]["name"])
self.assertEqual("1", p["tag-list"][0]["count"])
def testListFromBrowse(self):
filename = "browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml"
res = _common.open_and_parse_test_data(self.datadir, filename)
self.assertEqual(395, res["place-count"])
self.assertEqual(25, len(res["place-list"]))
self.assertTrue(res["place-list"][13]["annotation"]["text"].startswith("was later renamed"))
================================================
FILE: test/test_mbxml_recording.py
================================================
# coding=utf-8
# Tests for parsing of recording queries
import unittest
import os
from test import _common
class GetRecordingTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "recording")
def testRecordingRelationCreditedAs(self):
# some performance relations have a "credited-as" attribute
res = _common.open_and_parse_test_data(self.datadir, "f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml")
recording = res["recording"]
rels = recording["artist-relation-list"]
self.assertEqual(4, len(rels))
# Original attributes
attributes = rels[2]["attribute-list"]
self.assertEqual("piano", attributes[0])
# New attribute dict format
attributes = rels[2]["attributes"]
expected = {"attribute": "piano", "credited-as": "Yamaha and Steinway pianos"}
self.assertEqual(expected, attributes[0])
================================================
FILE: test/test_mbxml_release.py
================================================
# Tests for parsing of release queries
import unittest
import os
import musicbrainzngs
from test import _common
class UrlTest(unittest.TestCase):
""" Test that the correct URL is generated when a search query is made """
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainzngs.set_useragent("test", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def testGetRelease(self):
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b")
self.assertEqual("https://musicbrainz.org/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", self.opener.get_url())
# one include
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", includes=["artists"])
self.assertEqual("https://musicbrainz.org/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b?inc=artists", self.opener.get_url())
# more than one include
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", includes=["artists", "recordings", "artist-credits"])
expected = "https://musicbrainz.org/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b?inc=artists+recordings+artist-credits"
self.assertEqual(expected, self.opener.get_url())
class GetReleaseTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "release")
def testArtistCredit(self):
"""
If the artist credit is the same in the track and recording, make sure that
the information is replicated in both objects, otherwise have distinct ones.
"""
# If no artist-credit in the track, copy in the recording one
res = _common.open_and_parse_test_data(self.datadir, "833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xml")
tracks = res["release"]["medium-list"][0]["track-list"]
t1 = tracks[1]
self.assertEqual(t1["artist-credit"], t1["recording"]["artist-credit"])
self.assertEqual("JT Bruce", t1["artist-credit-phrase"])
self.assertEqual(t1["recording"]["artist-credit-phrase"], t1["artist-credit-phrase"])
# Recording AC is different to track AC
res = _common.open_and_parse_test_data(self.datadir, "fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xml")
tracks = res["release"]["medium-list"][0]["track-list"]
t1 = tracks[1]
self.assertNotEqual(t1["artist-credit"], t1["recording"]["artist-credit"])
self.assertEqual("H. Lichner", t1["artist-credit-phrase"])
self.assertNotEqual(t1["recording"]["artist-credit-phrase"], t1["artist-credit-phrase"])
def testTrackId(self):
"""
Test that the id attribute of tracks is read.
"""
res = _common.open_and_parse_test_data(self.datadir, "212895ca-ee36-439a-a824-d2620cd10461-recordings.xml")
tracks = res["release"]["medium-list"][0]["track-list"]
map(lambda t: self.assertTrue("id" in t), tracks)
def testTrackLength(self):
"""
Test that if there is a track length, then `track_or_recording_length` has
that, but if not then fill the value from the recording length
"""
res = _common.open_and_parse_test_data(self.datadir, "b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml")
tracks = res["release"]["medium-list"][0]["track-list"]
# No track length and recording length
t1 = tracks[0]
self.assertTrue("length" not in t1)
self.assertEqual("180000", t1["recording"]["length"])
self.assertEqual("180000", t1["track_or_recording_length"])
# Track length and recording length same
t2 = tracks[1]
self.assertEqual("279000", t2["length"])
self.assertEqual("279000", t2["recording"]["length"])
self.assertEqual("279000", t2["track_or_recording_length"])
# Track length and recording length different
t3 = tracks[2]
self.assertEqual("60000", t3["length"])
self.assertEqual("80000", t3["recording"]["length"])
self.assertEqual("60000", t3["track_or_recording_length"])
# No track lengths
t4 = tracks[3]
self.assertTrue("length" not in t4["recording"])
self.assertTrue("length" not in t4)
self.assertTrue("track_or_recording_length" not in t4)
def testTrackTitle(self):
pass
def testTrackNumber(self):
"""
Test that track number (number or text) and track position (always an increasing number)
are both read properly
"""
res = _common.open_and_parse_test_data(self.datadir, "212895ca-ee36-439a-a824-d2620cd10461-recordings.xml")
tracks = res["release"]["medium-list"][0]["track-list"]
# This release doesn't number intro tracks as numbered tracks,
# so position and number get 'out of sync'
self.assertEqual(['1', '2', '3'], [t["position"] for t in tracks[:3]])
self.assertEqual(['', '1', '2'], [t["number"] for t in tracks[:3]])
res = _common.open_and_parse_test_data(self.datadir, "a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml")
tracks = res["release"]["medium-list"][0]["track-list"]
self.assertEqual(['1', '2'], [t["position"] for t in tracks])
self.assertEqual(['A', 'B'], [t["number"] for t in tracks])
res = _common.open_and_parse_test_data(self.datadir, "9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml")
tracks = res["release"]["medium-list"][0]["data-track-list"]
self.assertEqual(list(map(str, range(1, 199))), [t["position"] for t in tracks])
self.assertEqual(list(map(str, range(1, 199))), [t["number"] for t in tracks])
def testVideo(self):
"""
Test that the video attribute is parsed.
"""
res = _common.open_and_parse_test_data(self.datadir, "fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml")
trackswithoutvideo = res["release"]["medium-list"][0]["track-list"]
trackswithvideo = res["release"]["medium-list"][2]["track-list"]
map(lambda t: self.assertTrue("video" not in ["recording"]), trackswithoutvideo)
map(lambda t: self.assertEqual("true", t["recording"]["video"]), trackswithvideo)
def testPregapTrack(self):
"""
Test that the pregap track is parsed if it exists.
"""
res = _common.open_and_parse_test_data(self.datadir, "8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml")
medium = res["release"]["medium-list"][0]
self.assertTrue("pregap" in medium)
self.assertEqual("0", medium["pregap"]["position"])
self.assertEqual("0", medium["pregap"]["number"])
self.assertEqual("35000", medium["pregap"]["length"])
self.assertEqual("[untitled]", medium["pregap"]["recording"]["title"])
def testDataTracklist(self):
"""
Test that data tracklist are parsed.
"""
res = _common.open_and_parse_test_data(self.datadir, "9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml")
medium = res["release"]["medium-list"][0]
self.assertTrue("data-track-list" in medium)
self.assertEqual(198, len(medium["data-track-list"]))
================================================
FILE: test/test_mbxml_release_group.py
================================================
# Tests for parsing of release queries
import unittest
import os
from test import _common
class GetReleaseGroupTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data",
"release-group")
def testTypesExist(self):
res = _common.open_and_parse_test_data(self.datadir,
"f52bc6a1-c848-49e6-85de-f8f53459a624.xml")
rg = res["release-group"]
self.assertTrue("type" in rg)
self.assertTrue("primary-type" in rg)
self.assertTrue("secondary-type-list" in rg)
def testTypesResult(self):
res = _common.open_and_parse_test_data(self.datadir,
"f52bc6a1-c848-49e6-85de-f8f53459a624.xml")
rg = res["release-group"]
self.assertEqual("Soundtrack", rg["type"])
self.assertEqual("Album", rg["primary-type"])
self.assertEqual(["Soundtrack"], rg["secondary-type-list"])
================================================
FILE: test/test_mbxml_search.py
================================================
import unittest
import os
from musicbrainzngs import mbxml
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
class SearchArtistTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-artist.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(25, len(res["artist-list"]))
self.assertEqual(349, res["artist-count"])
one = res["artist-list"][0]
self.assertEqual(9, len(one.keys()))
# Score is a key that is only in search results -
# so check for it here
self.assertEqual("100", one["ext:score"])
class SearchReleaseTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-release.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(25, len(res["release-list"]))
self.assertEqual(16739, res["release-count"])
one = res["release-list"][0]
self.assertEqual("100", one["ext:score"])
# search results have a medium-list/track-count element
self.assertEqual(4, one["medium-track-count"])
self.assertEqual(1, one["medium-count"])
self.assertEqual("CD", one["medium-list"][0]["format"])
class SearchReleaseGroupTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-release-group.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(25, len(res["release-group-list"]))
self.assertEqual(14641, res["release-group-count"])
one = res["release-group-list"][0]
self.assertEqual("100", one["ext:score"])
class SearchWorkTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-work.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(25, len(res["work-list"]))
self.assertEqual(174, res["work-count"])
one = res["work-list"][0]
self.assertEqual("100", one["ext:score"])
class SearchLabelTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-label.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(1, len(res["label-list"]))
self.assertEqual(1, res["label-count"])
one = res["label-list"][0]
self.assertEqual("100", one["ext:score"])
class SearchRecordingTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-recording.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(25, len(res["recording-list"]))
self.assertEqual(1258, res["recording-count"])
one = res["recording-list"][0]
self.assertEqual("100", one["ext:score"])
class SearchInstrumentTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-instrument.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(23, len(res["instrument-list"]))
self.assertEqual(23, res["instrument-count"])
one = res["instrument-list"][0]
self.assertEqual("100", one["ext:score"])
end = res["instrument-list"][-1]
self.assertEqual("29", end["ext:score"])
class SearchPlaceTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-place.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(14, res["place-count"])
self.assertEqual(14, len(res["place-list"]))
one = res["place-list"][0]
self.assertEqual("100", one["ext:score"])
two = res["place-list"][1]
self.assertEqual("63", two["ext:score"])
self.assertEqual("Southampton", two["disambiguation"])
class SearchEventTest(unittest.TestCase):
def testFields(self):
fn = os.path.join(DATA_DIR, "search-event.xml")
with open(fn, 'rb') as msg:
res = mbxml.parse_message(msg)
self.assertEqual(3, res["event-count"])
self.assertEqual(3, len(res["event-list"]))
one = res["event-list"][0]
self.assertEqual("100", one["ext:score"])
two = res["event-list"][1]
self.assertEqual(1, len(two["place-relation-list"]))
================================================
FILE: test/test_mbxml_work.py
================================================
# coding=utf-8
# Tests for parsing of work queries
import unittest
import os
from test import _common
class GetWorkTest(unittest.TestCase):
def setUp(self):
self.datadir = os.path.join(os.path.dirname(__file__), "data", "work")
def testWorkAliases(self):
res = _common.open_and_parse_test_data(self.datadir, "80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml")
aliases = res["work"]["alias-list"]
self.assertEqual(len(aliases), 2)
a0 = aliases[0]
self.assertEqual(a0["alias"], 'Symphonie Nr. 3 Es-Dur, Op. 55 "Eroica"')
self.assertEqual(a0["sort-name"], 'Symphonie Nr. 3 Es-Dur, Op. 55 "Eroica"')
a1 = aliases[1]
self.assertEqual(a1["alias"], 'Symphony No. 3, Op. 55 "Eroica"')
self.assertEqual(a1["sort-name"], 'Symphony No. 3, Op. 55 "Eroica"')
res = _common.open_and_parse_test_data(self.datadir, "3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml")
aliases = res["work"]["alias-list"]
self.assertEqual(len(aliases), 10)
a0 = aliases[0]
self.assertEqual(a0["alias"], "Adagio from Symphony No. 2 in E minor, Op. 27")
self.assertEqual(a0["sort-name"], "Adagio from Symphony No. 2 in E minor, Op. 27")
def testWorkAttributes(self):
res = _common.open_and_parse_test_data(self.datadir, "80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml")
work_attrs = res["work"]["attribute-list"]
self.assertEqual(len(work_attrs), 1)
attr = work_attrs[0]
expected = {"attribute": "Key", "value": "E-flat major"}
self.assertEqual(expected, attr)
res = _common.open_and_parse_test_data(self.datadir, "8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml")
work_attrs = res["work"]["attribute-list"]
self.assertEqual(len(work_attrs), 3)
expected = {"attribute": "Makam (Ottoman, Turkish)", "value": b"H\xc3\xbczzam".decode("utf-8")}
self.assertEqual(expected, work_attrs[0])
expected = {"attribute": "Form (Ottoman, Turkish)", "value": b"Pe\xc5\x9frev".decode("utf-8")}
self.assertEqual(expected, work_attrs[1])
expected = {"attribute": "Usul (Ottoman, Turkish)", "value": "Fahte"}
self.assertEqual(expected, work_attrs[2])
def testWorkRelationAttributes(self):
# Some relation attributes can contain attributes as well as text
res = _common.open_and_parse_test_data(self.datadir, "72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml")
work = res["work"]
rels = work["series-relation-list"]
self.assertEqual(1, len(rels))
# Original attributes
attributes = rels[0]["attribute-list"]
self.assertEqual("number", attributes[0])
# New attribute dict format
attributes = rels[0]["attributes"]
expected = {"attribute": "number", "value": "BuxWV 1"}
self.assertEqual(expected, attributes[0])
================================================
FILE: test/test_ratelimit.py
================================================
import unittest
import time
import musicbrainzngs
from musicbrainzngs import musicbrainz
from test._common import Timecop
class RateLimitArgumentTest(unittest.TestCase):
def test_invalid_args(self):
""" Passing invalid arguments to set_rate_limit should throw
an exception """
try:
musicbrainzngs.set_rate_limit(1, 0)
self.fail("Required exception wasn't raised")
except ValueError as e:
self.assertTrue("new_requests" in str(e))
try:
musicbrainzngs.set_rate_limit(0, 1)
self.fail("Required exception wasn't raised")
except ValueError as e:
self.assertTrue("limit_or_interval" in str(e))
try:
musicbrainzngs.set_rate_limit(1, -1)
self.fail("Required exception wasn't raised")
except ValueError as e:
self.assertTrue("new_requests" in str(e))
try:
musicbrainzngs.set_rate_limit(0, -1)
self.fail("Required exception wasn't raised")
except ValueError as e:
self.assertTrue("limit_or_interval" in str(e))
class RateLimitingTest(unittest.TestCase):
def setUp(self):
self.cop = Timecop()
self.cop.install()
@musicbrainz._rate_limit
def limited():
pass
self.func = limited
def tearDown(self):
self.cop.restore()
def test_do_not_wait_initially(self):
time1 = time.time()
self.func()
time2 = time.time()
self.assertAlmostEqual(time1, time2)
def test_second_rapid_query_waits(self):
""" Performing 2 queries should force a wait """
self.func()
time1 = time.time()
self.func()
time2 = time.time()
self.assertTrue(time2 - time1 >= 1.0)
def test_second_distant_query_does_not_wait(self):
""" If there is a gap between queries, don't force a wait """
self.func()
time.sleep(1.0)
time1 = time.time()
self.func()
time2 = time.time()
self.assertAlmostEqual(time1, time2)
class BatchedRateLimitingTest(unittest.TestCase):
def setUp(self):
musicbrainzngs.set_rate_limit(3, 3)
self.cop = Timecop()
self.cop.install()
@musicbrainz._rate_limit
def limited():
pass
self.func = limited
def tearDown(self):
musicbrainzngs.set_rate_limit(1, 1)
self.cop.restore()
def test_initial_rapid_queries_not_delayed(self):
time1 = time.time()
self.func()
self.func()
self.func()
time2 = time.time()
self.assertAlmostEqual(time1, time2)
def test_overage_query_delayed(self):
time1 = time.time()
self.func()
self.func()
self.func()
self.func()
time2 = time.time()
self.assertTrue(time2 - time1 >= 1.0)
class NoRateLimitingTest(unittest.TestCase):
""" Disable rate limiting """
def setUp(self):
musicbrainzngs.set_rate_limit(False)
self.cop = Timecop()
self.cop.install()
@musicbrainz._rate_limit
def limited():
pass
self.func = limited
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
self.cop.restore()
def test_initial_rapid_queries_not_delayed(self):
time1 = time.time()
self.func()
self.func()
self.func()
time2 = time.time()
self.assertAlmostEqual(time1, time2)
================================================
FILE: test/test_requests.py
================================================
import unittest
import musicbrainzngs
from musicbrainzngs import musicbrainz
from test import _common
class ArgumentTest(unittest.TestCase):
"""Tests request methods to ensure they're enforcing general parameters
(useragent, authentication)."""
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener.add_handlers_and_return(args)
musicbrainz.set_rate_limit(False)
def tearDown(self):
musicbrainz.set_rate_limit(True)
def test_no_client(self):
musicbrainzngs.set_useragent("testapp", "0.1", "test@example.org")
musicbrainz._mb_request(path="foo", client_required=False)
self.assertFalse("testapp" in self.opener.myurl)
def test_client(self):
musicbrainzngs.set_useragent("testapp", "0.1", "test@example.org")
musicbrainz._mb_request(path="foo", client_required=True)
self.assertTrue("testapp" in self.opener.myurl)
def test_false_useragent(self):
self.assertRaises(ValueError, musicbrainzngs.set_useragent, "", "0.1",
"test@example.org")
self.assertRaises(ValueError, musicbrainzngs.set_useragent, "test", "",
"test@example.org")
def test_missing_auth(self):
musicbrainz.auth("", "")
musicbrainz._useragent = "test"
self.assertRaises(musicbrainzngs.UsageError,
musicbrainz._mb_request, path="foo",
auth_required=musicbrainz.AUTH_YES)
def test_missing_useragent(self):
musicbrainz._useragent = ""
self.assertRaises(musicbrainzngs.musicbrainz.UsageError,
musicbrainz._mb_request, path="foo")
def test_auth_headers(self):
musicbrainz._useragent = "test"
musicbrainz.auth("user", "password")
req = musicbrainz._mb_request(path="foo", auth_required=musicbrainz.AUTH_YES)
assert(any([isinstance(handler, musicbrainz._DigestAuthHandler) for handler in self.opener.handlers]))
def test_auth_headers_ifset(self):
musicbrainz._useragent = "test"
musicbrainz.auth("user", "password")
req = musicbrainz._mb_request(path="foo", auth_required=musicbrainz.AUTH_IFSET)
assert(any([isinstance(handler, musicbrainz._DigestAuthHandler) for handler in self.opener.handlers]))
def test_auth_headers_ifset_no_user(self):
musicbrainz._useragent = "test"
musicbrainz.auth("", "")
# if no user and password, auth is not set for AUTH_IFSET
req = musicbrainz._mb_request(path="foo", auth_required=musicbrainz.AUTH_IFSET)
assert(not any([isinstance(handler, musicbrainz._DigestAuthHandler) for handler in self.opener.handlers]))
class MethodTest(unittest.TestCase):
"""Tests the various _do_mb_* methods to ensure they're setting the
using the correct HTTP method."""
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainz.auth("user", "password")
musicbrainz.set_rate_limit(False)
def tearDown(self):
musicbrainz.set_rate_limit(False)
def test_invalid_method(self):
self.assertRaises(ValueError, musicbrainz._mb_request, path="foo",
method="HUG")
def test_delete(self):
musicbrainz._do_mb_delete("foo")
self.assertEqual("DELETE", self.opener.request.get_method())
def test_put(self):
musicbrainz._do_mb_put("foo")
self.assertEqual("PUT", self.opener.request.get_method())
def test_post(self):
musicbrainz._do_mb_post("foo", "body")
self.assertEqual("POST", self.opener.request.get_method())
def test_get(self):
musicbrainz._do_mb_query("artist", 1234, [], [])
self.assertEqual("GET", self.opener.request.get_method())
class HostnameTest(unittest.TestCase):
"""Test that the protocol, hostname, and port are set as expected"""
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainz.set_rate_limit(False)
def tearDown(self):
musicbrainz.set_rate_limit(True)
musicbrainzngs.set_hostname("musicbrainz.org", use_https=True)
def test_default_musicbrainz_https(self):
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b")
self.assertEqual("https://musicbrainz.org/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", self.opener.get_url())
def test_set_http(self):
musicbrainzngs.set_hostname("beta.musicbrainz.org")
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b")
self.assertEqual("http://beta.musicbrainz.org/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", self.opener.get_url())
def test_set_https(self):
musicbrainzngs.set_hostname("mbmirror.org", use_https=True)
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b")
self.assertEqual("https://mbmirror.org/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", self.opener.get_url())
def test_set_port(self):
musicbrainzngs.set_hostname("localhost:8000", use_https=False)
musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b")
self.assertEqual("http://localhost:8000/ws/2/release/5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b", self.opener.get_url())
================================================
FILE: test/test_search.py
================================================
import unittest
import musicbrainzngs
from test import _common
class SearchUrlTest(unittest.TestCase):
""" Test that the correct URL is generated when a search query is made """
def setUp(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
musicbrainzngs.set_useragent("a", "1")
musicbrainzngs.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.set_rate_limit(True)
def test_search_annotations(self):
musicbrainzngs.search_annotations("Pieds")
self.assertEqual("https://musicbrainz.org/ws/2/annotation/?query=Pieds", self.opener.get_url())
# Query fields
musicbrainzngs.search_annotations(entity="bdb24cb5-404b-4f60-bba4-7b730325ae47")
# TODO: We escape special characters and then urlencode all query parameters, which may
# not be necessary, but MusicBrainz accepts it and appears to return the same value as without
expected_query = r'entity:(bdb24cb5\-404b\-4f60\-bba4\-7b730325ae47)'
expected = 'https://musicbrainz.org/ws/2/annotation/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_annotations(foo="value")
def test_search_artists(self):
musicbrainzngs.search_artists("Dynamo Go")
self.assertEqual("https://musicbrainz.org/ws/2/artist/?query=Dynamo+Go", self.opener.get_url())
musicbrainzngs.search_artists(artist="Dynamo Go")
expected_query = 'artist:(dynamo go)'
expected = 'https://musicbrainz.org/ws/2/artist/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_artists(foo="value")
def test_search_events(self):
musicbrainzngs.search_events("woodstock")
self.assertEqual("https://musicbrainz.org/ws/2/event/?query=woodstock", self.opener.get_url())
musicbrainzngs.search_events(event="woodstock")
expected_query = 'event:(woodstock)'
expected = 'https://musicbrainz.org/ws/2/event/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_events(foo="value")
def test_search_labels(self):
musicbrainzngs.search_labels("Waysafe")
self.assertEqual("https://musicbrainz.org/ws/2/label/?query=Waysafe", self.opener.get_url())
musicbrainzngs.search_labels(label="Waysafe")
expected_query = 'label:(waysafe)'
expected = 'https://musicbrainz.org/ws/2/label/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_labels(foo="value")
def test_search_places(self):
musicbrainzngs.search_places("Fillmore")
self.assertEqual("https://musicbrainz.org/ws/2/place/?query=Fillmore", self.opener.get_url())
musicbrainzngs.search_places(place="Fillmore")
expected_query = 'place:(fillmore)'
expected = 'https://musicbrainz.org/ws/2/place/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_places(foo="value")
def test_search_releases(self):
musicbrainzngs.search_releases("Affordable Pop Music")
self.assertEqual("https://musicbrainz.org/ws/2/release/?query=Affordable+Pop+Music", self.opener.get_url())
musicbrainzngs.search_releases(release="Affordable Pop Music")
expected_query = 'release:(affordable pop music)'
expected = 'https://musicbrainz.org/ws/2/release/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_releases(foo="value")
def test_search_release_groups(self):
musicbrainzngs.search_release_groups("Affordable Pop Music")
self.assertEqual("https://musicbrainz.org/ws/2/release-group/?query=Affordable+Pop+Music", self.opener.get_url())
musicbrainzngs.search_release_groups(releasegroup="Affordable Pop Music")
expected_query = 'releasegroup:(affordable pop music)'
expected = 'https://musicbrainz.org/ws/2/release-group/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_release_groups(foo="value")
def test_search_recordings(self):
musicbrainzngs.search_recordings("Thief of Hearts")
self.assertEqual("https://musicbrainz.org/ws/2/recording/?query=Thief+of+Hearts", self.opener.get_url())
musicbrainzngs.search_recordings(recording="Thief of Hearts")
expected_query = 'recording:(thief of hearts)'
expected = 'https://musicbrainz.org/ws/2/recording/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_recordings(foo="value")
def test_search_works(self):
musicbrainzngs.search_works("Fountain City")
self.assertEqual("https://musicbrainz.org/ws/2/work/?query=Fountain+City", self.opener.get_url())
musicbrainzngs.search_works(work="Fountain City")
expected_query = 'work:(fountain city)'
expected = 'https://musicbrainz.org/ws/2/work/?query=%s' % musicbrainzngs.compat.quote_plus(expected_query)
self.assertEqual(expected, self.opener.get_url())
# Invalid query field
with self.assertRaises(musicbrainzngs.InvalidSearchFieldError):
musicbrainzngs.search_works(foo="value")
================================================
FILE: test/test_submit.py
================================================
import unittest
import musicbrainzngs
from musicbrainzngs import musicbrainz
from test import _common
class SubmitTest(unittest.TestCase):
def setUp(self):
self.orig_opener = musicbrainzngs.compat.build_opener
musicbrainz.set_useragent("test_client", "1.0")
musicbrainz.auth("user", "password")
musicbrainz.set_rate_limit(False)
def tearDown(self):
musicbrainzngs.compat.build_opener = self.orig_opener
musicbrainz._useragent = ""
musicbrainz._client = ""
musicbrainz.user = ""
musicbrainz.password = ""
musicbrainz.set_rate_limit(True)
def test_submit_tags(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
def make_xml(**kwargs):
self.assertEqual({'artist_tags': {'mbid': ['one', 'two']}}, kwargs)
oldmake_tag_request = musicbrainz.mbxml.make_tag_request
musicbrainz.mbxml.make_tag_request = make_xml
musicbrainz.submit_tags(artist_tags={"mbid": ["one", "two"]})
musicbrainz.mbxml.make_tag_request = oldmake_tag_request
def test_submit_single_tag(self):
self.opener = _common.FakeOpener("")
musicbrainzngs.compat.build_opener = lambda *args: self.opener
def make_xml(**kwargs):
self.assertEqual({'artist_tags': {'mbid': ['single']}}, kwargs)
oldmake_tag_request = musicbrainz.mbxml.make_tag_request
musicbrainz.mbxml.make_tag_request = make_xml
musicbrainz.submit_tags(artist_tags={"mbid": "single"})
musicbrainz.mbxml.make_tag_request = oldmake_tag_request
================================================
FILE: tox.ini
================================================
[tox]
envlist=py27,py37,py38,py39,py310
[testenv]
commands=pytest
deps=pytest
[gh-actions]
python =
2.7: py27
3.7: py37
3.8: py38
3.9: py39
3.10: py310