musicbrainzngs-0.6/0000775000175000017500000000000012702713133016040 5ustar alastairalastair00000000000000musicbrainzngs-0.6/docs/0000775000175000017500000000000012702713133016770 5ustar alastairalastair00000000000000musicbrainzngs-0.6/docs/api.rst0000664000175000017500000001325512672234247020313 0ustar alastairalastair00000000000000API ~~~ .. module:: musicbrainzngs This is a shallow python binding of the MusicBrainz web service so you should read :musicbrainz:`Development/XML Web Service/Version 2` to understand how that web service works in general. All requests that fetch data return the data in the form of a :class:`dict`. Attributes and elements both map to keys in the dict. List entities are of type :class:`list`. This part will give an overview of available functions. Have a look at :doc:`usage` for examples on how to use them. General ------- .. autofunction:: auth .. autofunction:: set_rate_limit .. autofunction:: set_useragent .. autofunction:: set_hostname .. autofunction:: set_caa_hostname .. autofunction:: set_parser .. autofunction:: set_format Getting Data ------------ All of these functions will fetch a MusicBrainz entity or a list of entities as a dict. You can specify a list of `includes` to get more data and you can filter on `release_status` and `release_type`. See :const:`musicbrainz.VALID_RELEASE_STATUSES` and :const:`musicbrainz.VALID_RELEASE_TYPES`. The valid includes are listed for each function. .. autofunction:: get_area_by_id .. autofunction:: get_artist_by_id .. autofunction:: get_event_by_id .. autofunction:: get_instrument_by_id .. autofunction:: get_label_by_id .. autofunction:: get_place_by_id .. autofunction:: get_recording_by_id .. autofunction:: get_recordings_by_isrc .. autofunction:: get_release_group_by_id .. autofunction:: get_release_by_id .. autofunction:: get_releases_by_discid .. autofunction:: get_series_by_id .. autofunction:: get_work_by_id .. autofunction:: get_works_by_iswc .. autofunction:: get_url_by_id .. autofunction:: get_collections .. autofunction:: get_releases_in_collection .. autodata:: musicbrainzngs.musicbrainz.VALID_RELEASE_TYPES .. autodata:: musicbrainzngs.musicbrainz.VALID_RELEASE_STATUSES .. _caa_api: Cover Art --------- .. autofunction:: get_image_list .. autofunction:: get_release_group_image_list .. autofunction:: get_image .. autofunction:: get_image_front .. autofunction:: get_release_group_image_front .. autofunction:: get_image_back .. _search_api: Searching --------- For all of these search functions you can use any of the allowed search fields as parameter names. The documentation of what these fields do is on :musicbrainz:`Development/XML Web Service/Version 2/Search`. You can also set the `query` parameter to any lucene query you like. When you use any of the search fields as parameters, special characters are escaped in the `query`. By default the elements are concatenated with spaces in between, so lucene essentially does a fuzzy search. That search might include results that don't match the complete query, though these will be ranked lower than the ones that do. If you want all query elements to match for all results, you have to set `strict=True`. By default the web service returns 25 results per request and you can set a `limit` of up to 100. You have to use the `offset` parameter to set how many results you have already seen so the web service doesn't give you the same results again. .. autofunction:: search_annotations .. autofunction:: search_areas .. autofunction:: search_artists .. autofunction:: search_events .. autofunction:: search_instruments .. autofunction:: search_labels .. autofunction:: search_places .. autofunction:: search_recordings .. autofunction:: search_release_groups .. autofunction:: search_releases .. autofunction:: search_series .. autofunction:: search_works Browsing -------- You can browse entitities of a certain type linked to one specific entity. That is you can browse all recordings by an artist, for example. These functions can be used to to include more than the maximum of 25 linked entities returned by the functions in `Getting Data`_. You can set a `limit` as high as 100. The default is still 25. Similar to the functions in `Searching`_, you have to specify an `offset` to see the results you haven't seen yet. You have to provide exactly one MusicBrainz ID to these functions. .. autofunction:: browse_artists .. autofunction:: browse_events .. autofunction:: browse_labels .. autofunction:: browse_places .. autofunction:: browse_recordings .. autofunction:: browse_release_groups .. autofunction:: browse_releases .. autofunction:: browse_urls .. _api_submitting: Submitting ---------- These are the only functions that write to the MusicBrainz database. They take one or more dicts with multiple entities as keys, which take certain values or a list of values. You have to use :func:`auth` before using any of these functions. .. autofunction:: submit_barcodes .. autofunction:: submit_isrcs .. autofunction:: submit_tags .. autofunction:: submit_ratings .. autofunction:: add_releases_to_collection .. autofunction:: remove_releases_from_collection Exceptions ---------- These are the main exceptions that are raised by functions in musicbrainzngs. You might want to catch some of these at an appropriate point in your code. Some of these might have subclasses that are not listed here. .. autoclass:: MusicBrainzError .. autoclass:: UsageError :show-inheritance: .. autoclass:: WebServiceError :show-inheritance: .. autoclass:: AuthenticationError :show-inheritance: .. autoclass:: NetworkError :show-inheritance: .. autoclass:: ResponseError :show-inheritance: Logging ------- `musicbrainzngs` logs debug and informational messages using Python's :mod:`logging` module. All logging is done in the logger with the name `musicbrainzngs`. You can enable this output in your application with:: import logging logging.basicConfig(level=logging.DEBUG) # optionally restrict musicbrainzngs output to INFO messages logging.getLogger("musicbrainzngs").setLevel(logging.INFO) musicbrainzngs-0.6/docs/usage.rst0000664000175000017500000002036312551472207020640 0ustar alastairalastair00000000000000Usage ~~~~~ In general you need to set a useragent for your application, start searches to get to know corresponding MusicBrainz IDs and then retrieve information about these entities. The data is returned in form of a :class:`dict`. If you also want to submit data, then you must authenticate as a MusicBrainz user. This part of the documentation will give you usage examples. For an overview of available functions you can have a look at the :doc:`api`. Identification -------------- To access the MusicBrainz webservice through this library, you `need to identify your application `_ by setting the useragent header made in HTTP requests to one that is unique to your application. To ease this, the convenience function :meth:`musicbrainzngs.set_useragent` is provided which automatically sets the useragent based on information about the application name, version and contact information to the format `recommended by MusicBrainz `_. If a request is made without setting the useragent beforehand, a :exc:`musicbrainzngs.UsageError` will be raised. Authentication -------------- Certain calls to the webservice require user authentication prior to the call itself. The affected functions state this requirement in their documentation. The user and password used for authentication are the same as for the MusicBrainz website itself and can be set with the :meth:`musicbrainzngs.auth` method. After calling this function, the credentials will be saved and automaticall used by all functions requiring them. If a method requiring authentication is called without authenticating, a :exc:`musicbrainzngs.UsageError` will be raised. If the credentials provided are wrong and the server returns a status code of 401, a :exc:`musicbrainzngs.AuthenticationError` will be raised. Getting Data ------------ Regular MusicBrainz Data ^^^^^^^^^^^^^^^^^^^^^^^^ You can get MusicBrainz entities as a :class:`dict` when retrieving them with some form of identifier. An example using :func:`musicbrainzngs.get_artist_by_id`:: artist_id = "c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6" try: result = musicbrainzngs.get_artist_by_id(artist_id) except WebServiceError as exc: print("Something went wrong with the request: %s" % exc) else: artist = result["artist"] print("name:\t\t%s" % artist["name"]) print("sort name:\t%s" % artist["sort-name"]) You can get more information about entities connected to the artist with adding `includes` and you filter releases and release_groups:: result = musicbrainzngs.get_artist_by_id(artist_id, includes=["release-groups"], release_type=["album", "ep"]) for release_group in result["artist"]["release-group-list"]: print("{title} ({type})".format(title=release_group["title"], type=release_group["type"])) .. tip:: Compilations are also of primary type "album". You have to filter these out manually if you don't want them. .. note:: You can only get at most 25 release groups using this method. If you want to fetch all release groups you will have to `browse `_. Cover Art Data ^^^^^^^^^^^^^^ This library includes a few methods to access data from the `Cover Art Archive `_ which has a `documented API documentation `_ Both :func:`musicbrainzngs.get_image_list` and :func:`musicbrainzngs.get_release_group_image_list` return the deserialized cover art listing for a `release `_ or `release group `_. To find out whether a release has an approved front image, you could use the following example code:: release_id = "46a48e90-819b-4bed-81fa-5ca8aa33fbf3" data = musicbrainzngs.get_cover_art_list("46a48e90-819b-4bed-81fa-5ca8aa33fbf3") for image in data["images"]: if "Front" in image["types"] and image["approved"]: print "%s is an approved front image!" % image["thumbnails"]["large"] break To retrieve an image itself, use :func:`musicbrainzngs.get_image`. A few convenience functions like :func:`musicbrainzngs.get_image_front` are provided to allow easy access to often requested images. .. warning:: There is no upper bound for the size of images uploaded to the Cover Art Archive and downloading an image will return the binary data in memory. Consider using the :py:mod:`tempfile` module or similar techniques to save images to disk as soon as possible. Searching --------- When you don't know the MusicBrainz IDs yet, you have to start a search. Using :func:`musicbrainzngs.search_artists`:: result = musicbrainzngs.search_artists(artist="xx", type="group", country="GB") for artist in result['artist-list']: print(u"{id}: {name}".format(id=artist['id'], name=artist["name"])) .. tip:: Musicbrainzngs returns unicode strings. It's up to you to make sure Python (2) doesn't try to convert these to ascii again. In the example we force a unicode literal for print. Python 3 works without fixes like these. You can also use the query without specifying the search fields:: musicbrainzngs.search_release_groups("the clash london calling") The query and the search fields can also be used at the same time. Browsing -------- When you want to fetch a list of entities greater than 25, you have to use one of the browse functions. Not only can you specify a `limit` as high as 100, but you can also specify an `offset` to get the complete list in multiple requests. An example would be using :func:`musicbrainzngs.browse_release_groups` to get all releases for a label:: label = "71247f6b-fd24-4a56-89a2-23512f006f0c" limit = 100 offset = 0 releases = [] page = 1 print("fetching page number %d.." % page) result = musicbrainzngs.browse_releases(label=label, includes=["labels"], release_type=["album"], limit=limit) page_releases = result['release-list'] releases += page_releases # release-count is only available starting with musicbrainzngs 0.5 if "release-count" in result: count = result['release-count'] print("") while len(page_releases) >= limit: offset += limit page += 1 print("fetching page number %d.." % page) result = musicbrainzngs.browse_releases(label=label, includes=["labels"], release_type=["album"], limit=limit, offset=offset) page_releases = result['release-list'] releases += page_releases print("") for release in releases: for label_info in release['label-info-list']: catnum = label_info.get('catalog-number') if label_info['label']['id'] == label and catnum: print("{catnum:>17}: {date:10} {title}".format(catnum=catnum, date=release['date'], title=release['title'])) print("\n%d releases on %d pages" % (len(releases), page)) .. tip:: You should always try to filter in the query, when possible, rather than fetching everything and filtering afterwards. This will make your application faster since web service requests are throttled. In the example we filter by `release_type`. Submitting ---------- You can also submit data using musicbrainzngs. Please use :func:`musicbrainzngs.set_hostname` to set the host to test.musicbrainz.org when testing the submission part of your application. `Authentication`_ is necessary to submit any data to MusicBrainz. An example using :func:`musicbrainzngs.submit_barcodes` looks like this:: musicbrainzngs.set_hostname("test.musicbrainz.org") musicbrainzngs.auth("test", "mb") barcodes = { "174a5513-73d1-3c9d-a316-3c1c179e35f8": "5099749534728", "838952af-600d-3f51-84d5-941d15880400": "602517737280" } musicbrainzngs.submit_barcodes(barcodes) See :ref:`api_submitting` in the API for other possibilites. More Examples ------------- You can find some examples for using `musicbrainzngs` in the `examples directory `_. musicbrainzngs-0.6/docs/Makefile0000664000175000017500000001305712117613714020442 0ustar alastairalastair00000000000000# 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." musicbrainzngs-0.6/docs/conf.py0000664000175000017500000002124312274714562020304 0ustar alastairalastair00000000000000# -*- 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': ('http://musicbrainz.org/doc/%s', ''), } intersphinx_mapping = { 'python': ('http://python.readthedocs.org/en/latest/', None), 'python2': ('http://python.readthedocs.org/en/v2.7.2/', None), 'discid': ('http://python-discid.readthedocs.org/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' musicbrainzngs-0.6/docs/index.rst0000664000175000017500000000120112160273072020624 0ustar alastairalastair00000000000000musicbrainzngs |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` musicbrainzngs-0.6/docs/make.bat0000664000175000017500000001177112041524063020402 0ustar alastairalastair00000000000000@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 musicbrainzngs-0.6/docs/installation.rst0000664000175000017500000000155212274714562022241 0ustar alastairalastair00000000000000Installation ~~~~~~~~~~~~ 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 musicbrainzngs-0.6/test/0000775000175000017500000000000012702713133017017 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/test_mbxml_discid.py0000664000175000017500000000647512621332023023075 0ustar alastairalastair00000000000000# Tests for parsing of discid queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 testGetDiscId(self): musicbrainzngs.get_releases_by_discid("xp5tz6rE4OHrBafj0bLfDRMGK48-") self.assertEqual("http://musicbrainz.org/ws/2/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-", self.opener.get_url()) # one include musicbrainzngs.get_releases_by_discid("xp5tz6rE4OHrBafj0bLfDRMGK48-", includes=["recordings"]) self.assertEqual("http://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 = "http://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"])) musicbrainzngs-0.6/test/test_requests.py0000664000175000017500000000513612551472207022316 0ustar alastairalastair00000000000000import unittest import os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 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): self.assertRaises(musicbrainzngs.UsageError, musicbrainz._mb_request, path="foo", auth_required=musicbrainz.AUTH_YES) def test_missing_useragent(self): musicbrainz._useragent = "" self.assertRaises(musicbrainzngs.UsageError, musicbrainz._mb_request, path="foo") 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") 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()) musicbrainzngs-0.6/test/test_ratelimit.py0000664000175000017500000000710012041524063022416 0ustar alastairalastair00000000000000import unittest import os import sys import time sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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) musicbrainzngs-0.6/test/test_mbxml_label.py0000664000175000017500000000256212274714652022726 0ustar alastairalastair00000000000000# Tests for parsing of label queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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") musicbrainzngs-0.6/test/test_mbxml_search.py0000664000175000017500000001464712672234247023122 0ustar alastairalastair00000000000000import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import musicbrainzngs from musicbrainzngs import mbxml from test import _common DATA_DIR = os.path.join(os.path.dirname(__file__), "data") 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 testSearchArtist(self): musicbrainzngs.search_artists("Dynamo Go") self.assertEqual("http://musicbrainz.org/ws/2/artist/?query=Dynamo+Go", self.opener.get_url()) def testSearchEvent(self): musicbrainzngs.search_events("woodstock") self.assertEqual("http://musicbrainz.org/ws/2/event/?query=woodstock", self.opener.get_url()) def testSearchLabel(self): musicbrainzngs.search_labels("Waysafe") self.assertEqual("http://musicbrainz.org/ws/2/label/?query=Waysafe", self.opener.get_url()) def testSearchPlace(self): musicbrainzngs.search_places("Fillmore") self.assertEqual("http://musicbrainz.org/ws/2/place/?query=Fillmore", self.opener.get_url()) def testSearchRelease(self): musicbrainzngs.search_releases("Affordable Pop Music") self.assertEqual("http://musicbrainz.org/ws/2/release/?query=Affordable+Pop+Music", self.opener.get_url()) def testSearchReleaseGroup(self): musicbrainzngs.search_release_groups("Affordable Pop Music") self.assertEqual("http://musicbrainz.org/ws/2/release-group/?query=Affordable+Pop+Music", self.opener.get_url()) def testSearchRecording(self): musicbrainzngs.search_recordings("Thief of Hearts") self.assertEqual("http://musicbrainz.org/ws/2/recording/?query=Thief+of+Hearts", self.opener.get_url()) def testSearchWork(self): musicbrainzngs.search_works("Fountain City") self.assertEqual("http://musicbrainz.org/ws/2/work/?query=Fountain+City", self.opener.get_url()) class SearchArtistTest(unittest.TestCase): def testFields(self): fn = os.path.join(DATA_DIR, "search-artist.xml") with open(fn) 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) 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) 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) 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) 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) 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) 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) 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) 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"])) musicbrainzngs-0.6/test/_common.py0000664000175000017500000000370112651435613021030 0ustar alastairalastair00000000000000"""Common support for the test cases.""" import time import musicbrainzngs from os.path import join try: from urllib2 import OpenerDirector except ImportError: from urllib.request import OpenerDirector try: import StringIO except ImportError: import io as StringIO class FakeOpener(OpenerDirector): """ A URL Opener that saves the URL requested and returns a dummy response or raises an exception """ def __init__(self, response="", exception=None): self.myurl = None self.headers = None self.response = response self.exception = exception def open(self, request, body=None): self.myurl = request.get_full_url() self.headers = request.header_items() self.request = request if self.exception: raise self.exception else: return StringIO.StringIO(self.response) def get_url(self): return self.myurl # Mock timing. class Timecop(object): """Mocks the timing system (namely time() and sleep()) for testing. Inspired by the Ruby timecop library. """ def __init__(self): self.now = time.time() def time(self): return self.now def sleep(self, amount): self.now += amount def install(self): self.orig = { 'time': time.time, 'sleep': time.sleep, } time.time = self.time time.sleep = self.sleep def restore(self): time.time = self.orig['time'] time.sleep = self.orig['sleep'] def open_and_parse_test_data(datadir, filename): """ Opens an XML file dumped from the MusicBrainz web service and returns the parses it. :datadir: The directory containing the file :filename: The filename of the XML file :returns: The parsed representation of the XML files content """ with open(join(datadir, filename)) as msg: res = musicbrainzngs.mbxml.parse_message(msg) return res musicbrainzngs-0.6/test/__init__.py0000664000175000017500000000000012041524063021114 0ustar alastairalastair00000000000000musicbrainzngs-0.6/test/test_mbxml_artist.py0000664000175000017500000000234212274714652023151 0ustar alastairalastair00000000000000# Tests for parsing of artist queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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"]) musicbrainzngs-0.6/test/test_caa.py0000664000175000017500000001211112326467636021170 0ustar alastairalastair00000000000000import unittest import os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 = '{"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("http://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 = '{"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("http://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 = '{"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 = '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("http://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/1234", self.opener.myurl) self.assertEqual(resp, res) def test_get_size(self): resp = '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("http://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/1234-250", self.opener.myurl) self.assertEqual(resp, res) def test_front(self): resp = '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("http://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/front", self.opener.myurl) self.assertEqual(resp, res) def test_release_group_front(self): resp = '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("http://coverartarchive.org/release-group/8ec178f4-a8e8-4f22-bcba-1964466ef214/front", self.opener.myurl) self.assertEqual(resp, res) def test_back(self): resp = '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("http://coverartarchive.org/release/8ec178f4-a8e8-4f22-bcba-1964466ef214/back", self.opener.myurl) self.assertEqual(resp, res) musicbrainzngs-0.6/test/test_mbxml_release_group.py0000664000175000017500000000222612274714652024500 0ustar alastairalastair00000000000000# Tests for parsing of release queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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"]) musicbrainzngs-0.6/test/test_mbxml.py0000664000175000017500000000302512672234247021561 0ustar alastairalastair00000000000000import unittest import os import sys sys.path.append(os.path.abspath("..")) 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")) musicbrainzngs-0.6/test/test_mbxml_release.py0000664000175000017500000001654712621332023023257 0ustar alastairalastair00000000000000# Tests for parsing of release queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 testGetRelease(self): musicbrainzngs.get_release_by_id("5e3524ca-b4a1-4e51-9ba5-63ea2de8f49b") self.assertEqual("http://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("http://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 = "http://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"])) musicbrainzngs-0.6/test/test_browse.py0000664000175000017500000002002512672234247021742 0ustar alastairalastair00000000000000import unittest import os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import musicbrainzngs from musicbrainzngs import musicbrainz 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 test_browse(self): area = "74e50e58-5deb-4b99-93a2-decbb365c07f" musicbrainzngs.browse_events(area=area) self.assertEqual("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://musicbrainz.org/ws/2/release/?artist=47f67b22-affe-4fe1-9d25-853d69bc0ee3", self.opener.get_url()) musicbrainzngs.browse_releases(track_artist=artist) self.assertEqual("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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)) musicbrainzngs-0.6/test/test_mbxml_collection.py0000664000175000017500000001357712672234247024011 0ustar alastairalastair00000000000000# Tests for parsing of collection queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 testGetCollection(self): musicbrainzngs.get_releases_in_collection("0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac") self.assertEqual("http://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("http://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("http://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("http://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("http://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("http://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) musicbrainzngs-0.6/test/test_mbxml_work.py0000664000175000017500000000611112702703341022610 0ustar alastairalastair00000000000000# coding=utf-8 # Tests for parsing of work queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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]) musicbrainzngs-0.6/test/test_getentity.py0000664000175000017500000001713512672234247022465 0ustar alastairalastair00000000000000import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 testGetArtist(self): artistid = "952a4205-023d-4235-897c-6fdb6f58dfaa" musicbrainzngs.get_artist_by_id(artistid) self.assertEqual("http://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("http://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 ="http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://musicbrainz.org/ws/2/discid/I5l9cCSFccLKFEKS.7wqSZAorPU-", self.opener.get_url()) includes = ["artists"] musicbrainzngs.get_releases_by_discid("I5l9cCSFccLKFEKS.7wqSZAorPU-", includes) self.assertEqual("http://musicbrainz.org/ws/2/discid/I5l9cCSFccLKFEKS.7wqSZAorPU-?inc=artists", self.opener.get_url()) musicbrainzngs.get_releases_by_discid("discid", toc="toc") self.assertEqual("http://musicbrainz.org/ws/2/discid/discid?toc=toc", self.opener.get_url()) musicbrainzngs.get_releases_by_discid("discid", toc="toc", cdstubs=False) self.assertEqual("http://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("http://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("http://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("http://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("http://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"]) musicbrainzngs-0.6/test/test_mbxml_recording.py0000664000175000017500000000221712702700144023603 0ustar alastairalastair00000000000000# coding=utf-8 # Tests for parsing of recording queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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]) musicbrainzngs-0.6/test/test_mbxml_instrument.py0000664000175000017500000000663212642723062024053 0ustar alastairalastair00000000000000# -*- coding: UTF-8 -*- # Tests for parsing instrument queries import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from test import _common import musicbrainzngs 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") musicbrainzngs-0.6/test/data/0000775000175000017500000000000012702713133017730 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/discid/0000775000175000017500000000000012702713133021167 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/discid/f7agNZK1HMQ2WUWq9bwDymw9aHA-.xml0000664000175000017500000001314412621332023026416 0ustar alastairalastair00000000000000217245179902645238762550527899096705109755126972137342156600171900188400203475GeräuschOfficialnormaldeu2003AT2003AustriaAustriaAT0602498655023B000UH8BIGfalse0falsefalseSchwarzes Geräusch1CD217245179902645238762550527899096705109755126972137342156600171900188400203475Rotes Geräusch2CD2153001501824534376457736290385481102576120412139696156229174924184535192889GeräuschOfficialnormalOtherdeu2003-09-29DE2003-09-29GermanyGermanyDE4019593899829B0000AN32Dtrue3truetrueSchwarzes Geräusch1CD217245179902645238762550527899096705109755126972137342156600171900188400203475Rotes Geräusch2CD21530015018245343764577362903854811025761204121396961562291749241845351928892171401501839834682462326351586246103494121483140920157606176455186219194727musicbrainzngs-0.6/test/data/discid/xp5tz6rE4OHrBafj0bLfDRMGK48-.xml0000664000175000017500000001730312621332023026361 0ustar alastairalastair0000000000000021207518233322525977351098882136180169185187490Tales of EphidrinaOfficialnormalJewel Caseeng1993-07-05GB1993-07-05United KingdomUnited KingdomGB077778823827B000026GLQfalse0falsefalse1CD21211522233362526377355098922136220169225187530212043150332905256573478988501361481691531874582119121503315052428733409871513601516901518732321207518233322525977351098882136180169185187490Tales of EphidrinaOfficialnormaleng1993CA1993CanadaCanadaCAfalse0falsefalse1CD21211522233362526377355098922136220169225187530212043150332905256573478988501361481691531874582119121503315052428733409871513601516901518732321207518233322525977351098882136180169185187490Tales of EphidrinaOfficialnormaleng1993-07-30US1993-07-30United StatesUnited StatesUS017046610124B000003RVAfalse0falsefalse1CD21211522233362526377355098922136220169225187530212043150332905256573478988501361481691531874582119121503315052428733409871513601516901518732321207518233322525977351098882136180169185187490musicbrainzngs-0.6/test/data/search-event.xml0000664000175000017500000000231312672234247023047 0ustar alastairalastair00000000000000Woodstock 1999Woodstock Music & Art Fair19691969-08-151969-08-18backwardYasgur's FarmWoodstock 1999, Day 3, East Stage1999-07-241999-07-24backwardLimp BizkitLimp Bizkitmusicbrainzngs-0.6/test/data/search-place.xml0000664000175000017500000002164612672234247023024 0ustar alastairalastair00000000000000O2 Academy Newcastle
O2 Academy Newcastle, Westgate Road, NE1 1SW
54.97042-1.618654Newcastle upon TyneNewcastle upon Tyne2005false
O2 GuildhallSouthampton
West Marlands Road, Civic Centre, SO14 7LP
50.908154-1.406003SouthamptonSouthampton1937falseSouthampton Guildhall
O₂ ArenaPrague, formerly Sazka Arena
Ocelářská 460, 190 00 Praha 9, Czech Republic
50.1067514.496647PrahaPraha2004-03-27falseO2 ArenaSazka Arena
O₂ Academy Islington
Angel Central, 16 Parkfield Street, Islington, N1 0PS
51.534805-0.105733IslingtonIslingtonfalseO2 Academy Islington
O₂ Apollo Manchester
Stockport Road Ardwick Green Manchester M12 6AP
53.469566-2.222393ArdwickArdwick1938-08-29falseO2 Apollo Manchester
O2 Shepherd's Bush Empire
Shepherd's Bush Green, London, W12 8TT
51.50349-0.22433Hammersmith and FulhamHammersmith and Fulhamfalse
The O2 Arena
Drawdock Road, North Greenwich, London, SE10 0BB England, UK
51.5030.003133GreenwichGreenwich2007-06-24falseNorth Greenwich Arena
O2 Academy Glasgow
121 Eglinton St, Glasgow G5 9NT
55.8505-4.25906GlasgowGlasgowfalse
O2 Academy Oxfordpreviously called The Zodiac
190 Cowley Road, Oxford OX4 1UE
51.74691-1.234472OxfordOxford1995falseCarling Academy OxfordThe Zodiac
O2 Academy Leedsformerly Town and Country Club & Creation Nightclub
Cookridge Street, Leeds, West Yorkshire, England
53.802044-1.547069LeedsLeeds1992falseCreation NightclubLeeds AcademyTown and Country Club
O2 Academy Bournemouth
570 Christchurch Rd, Bournemouth, BH1 4BH
50.72671-1.839658BournemouthBournemouth1895-05-17falseAcademy NightclubBoscombe Grand TheatreBoscombe HippodromeOpera HouseStarkers Royal Arcade Ballrooms
O2 Academy Birmingham
16-18 Horsefair, Birmingham B1 1DB
52.473267-1.900201BirminghamBirmingham2009-09-10false
Brooklyn Bowl London
Brooklyn Bowl London, The O2, Peninsula Square, London SE10 0DX
51.503030.003139GreenwichGreenwich2014-01-17false
Barclaycard ArenaHamburg, formerly "Color Line Arena" and "O₂ World Hamburg"
Sylvesterallee 10, 22525 Hamburg, Germany
53.589179.899167HamburgHamburg2002-11-08falseColorline ArenaColor Line ArenaO2 World HamburgO₂ World Hamburg
musicbrainzngs-0.6/test/data/search-work.xml0000664000175000017500000004041012041524063022674 0ustar alastairalastair00000000000000 My Best Friend My Best Friend (short cut) backward Marusha Marusha backward Marusha Marusha The Girl of My Best Friend Girl of My Best Friend backward Sam Bobrick Bobrick, Sam backward Ross Butler Butler, Ross (At Your Best) You Are Love At Your Best (You Are Love) backward The Isley Brothers Isley Brothers, The backward The Isley Brothers Isley Brothers, The Best backward 양정승 Yang, Jung-Seung backward 양정승 Yang, Jung-Seung Best Guess at Best backward Stephen Rippy Rippy, Stephen Best Believe backward Redman Redman backward Pete Rock Rock, Pete The Best backward Holly Knight Knight, Holly backward Michael Chapman Chapman, Michael Best Forgotten backward Minco Eggersman Eggersman, Minco Best Pain backward [K] [K] backward [K] [K] Best Friend backward HIRO HIRO backward HIRO HIRO Best Friends backward Hans Zimmer Zimmer, Hans backward Heitor Pereira Pereira, Heitor backward Ryeland Allison Allison, Ryeland backward James S. Levine Levine, James S. Sunday Best Best Friend backward Toni Braxton Braxton, Toni backward Vance Taylor Taylor, Vance Best Man backward Bryan-Michael Cox Cox, Bryan-Michael Best Thing backward James Young Young, James backward Dennis DeYoung DeYoung, Dennis Best Friends backward Timothy Mosley Mosley, Timothy backward Melissa Elliott Elliott, Melissa Best Dress backward Jann Arden Arden, Jann backward Russell Broom Broom, Russell Best Defense backward James Gulotta Gulotta, James backward Chris Lykins Lykins, Chris Best Future backward Brad Laner Laner, Brad My Best T-702.448.462-8 backward Nili Hadida Hadida, Nili backward Benjamin Cotto Cotto, Benjamin 2nd Best backward Mark Charles Heidinger Heidinger, Mark Charles Best Friend T-101.813.896-8 backward 玉城千春 Tamashiro, Chiharu backward 玉城千春 Tamashiro, Chiharu Best friends backward 齋藤真也 Saito, Shinya backward 黒崎真音 Kurosaki, Maon Best Friends Adam WarRock song Second Best backward Kevin Hearn Hearn, Kevin backward Steven Page Page, Steven backward Ed Robertson Robertson, Ed musicbrainzngs-0.6/test/data/instrument/0000775000175000017500000000000012702713133022140 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/instrument/01ba56a2-4306-493d-8088-c7e9b671c74e-instrument-rels.xml0000664000175000017500000000243312642723062032473 0ustar alastairalastair00000000000000kemencheVarious 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 musicmusicbrainzngs-0.6/test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-annotation.xml0000664000175000017500000000062212642723062032035 0ustar alastairalastair00000000000000bullroarerA bullroarer consists of a piece of wood attached to a long cord which is then swung in a circle.Hornbostel-Sachs: 412.22musicbrainzngs-0.6/test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-tags.xml0000664000175000017500000000122312642723062030353 0ustar alastairalastair00000000000000bassBass 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 thismusicbrainzngs-0.6/test/data/instrument/dabdeb41-560f-4d84-aa6a-cf22349326fe.xml0000664000175000017500000000070212642723062027741 0ustar alastairalastair00000000000000tarluteThe 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.musicbrainzngs-0.6/test/data/instrument/9447c0af-5569-48f2-b4c5-241105d58c91.xml0000664000175000017500000000073112642723062027243 0ustar alastairalastair00000000000000bass 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.musicbrainzngs-0.6/test/data/instrument/6505f98c-f698-4406-8bf4-8ca43d05c36f-aliases.xml0000664000175000017500000000346712642723062031052 0ustar alastairalastair00000000000000bassBass 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μπάσοベースmusicbrainzngs-0.6/test/data/instrument/d00cec5f-f9bc-4235-a54f-6639a02d4e4c-url-rels.xml0000664000175000017500000000173512642723062031436 0ustar alastairalastair00000000000000bullroarerA 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.jpgmusicbrainzngs-0.6/test/data/release/0000775000175000017500000000000012702713133021350 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/release/8eb2b179-643d-3507-b64c-29fcc6745156-recordings.xml0000664000175000017500000000750412551472207030702 0ustar alastairalastair00000000000000Sea of CowardsOfficialnormalJewel Caseeng2010-05-10GB2010-05-10United KingdomUnited KingdomGB093624966524BT00CHI1V2true1truefalse10035000[untitled]3500011202306Blue Blood Blues20230622225666Hustle and Cuss22566633217320The Difference Between Us21732044I'm Mad196226I’m Mad19622655209213Die by the Drop20921366I Can't Hear You215093I Can’t Hear You21509377164840Gasoline16484088169053No Horse16905399162026Looking at the Invisible Man1620261010178040Jawbreaker1780401111172973Old Mary172973musicbrainzngs-0.6/test/data/release/a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml0000664000175000017500000000141212041524063031012 0ustar alastairalastair00000000000000 Bored BoredOfficialnormaleng1978GB11ABored Bored2BTime Warpmusicbrainzngs-0.6/test/data/release/b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml0000664000175000017500000000200412041524063031072 0ustar alastairalastair00000000000000 My Albumnormal111Some track18000022279000Another track2790003360000One more8000044Last track././@LongLink0000000000000000000000000000015000000000000011211 Lustar 00000000000000musicbrainzngs-0.6/test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xmlmusicbrainzngs-0.6/test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.0000664000175000017500000002273012041524063033304 0ustar alastairalastair00000000000000 Suzuki Piano School, Volume 2 (feat. piano: Haruko Katakoa)OfficialJewel CasenormalengSuzuki Method InternationalSuzuki Method InternationalUS029156150346087487498X111195040J. A. HummelJohann Nepomuk HummelHummel, Johann NepomukEcossaise195000Haruko Kataoka片岡春子Kataoka, Haruko22211133H. LichnerHeinrich LichnerLichner, HeinrichGerman composerA Short Story211000Haruko Kataoka片岡春子Kataoka, Haruko33176960R. SchumannRobert SchumannSchumann, RobertGerman classical composerThe Happy Farmer177000Haruko Kataoka片岡春子Kataoka, Haruko44286040J.S. BachJohann Sebastian BachBach, Johann SebastianMinuet 1286000Haruko Kataoka片岡春子Kataoka, Haruko55434200J.S. BachJohann Sebastian BachBach, Johann SebastianMinuet 2434000Haruko Kataoka片岡春子Kataoka, Haruko66379600J.S. BachJohann Sebastian BachBach, Johann SebastianMinuet 3380000Haruko Kataoka片岡春子Kataoka, Haruko77217333J. S. BachJohann Sebastian BachBach, Johann SebastianMinuet217000Haruko Kataoka片岡春子Kataoka, Haruko88126360C.M. von WeberCarl Maria von WeberWeber, Carl Maria vonCradle Song126000Haruko Kataoka片岡春子Kataoka, Haruko99177800W.A. MozartWolfgang Amadeus MozartMozart, Wolfgang Amadeusclassical composerMinuet178000Haruko Kataoka片岡春子Kataoka, Haruko1010280173W.A. MozartWolfgang Amadeus MozartMozart, Wolfgang Amadeusclassical composerArietta280000Haruko Kataoka片岡春子Kataoka, Haruko1111232333R. SchumannRobert SchumannSchumann, RobertGerman classical composerMelody232000Haruko Kataoka片岡春子Kataoka, Haruko1212807293L. van BeethovenLudwig van BeethovenBeethoven, Ludwig vanSonatina807000Haruko Kataoka片岡春子Kataoka, Haruko1313257106J.S. BachJohann Sebastian BachBach, Johann SebastianMusette257000Haruko Kataoka片岡春子Kataoka, Haruko1414388760J.S. BachJohann Sebastian BachBach, Johann SebastianMinuet389000Haruko Kataoka片岡春子Kataoka, Harukomusicbrainzngs-0.6/test/data/release/212895ca-ee36-439a-a824-d2620cd10461-recordings.xml0000664000175000017500000001200212274714652030643 0ustar alastairalastair00000000000000 We♥TechPara -mission style-OfficialnormalKeep Caseeng2006JP2006JapanJapanJP4988064913695B000EIF602true11truetrue1133000[intro]3300021188000WILD BOY <MISSION"B"RE-EDIT>32170000FAIRY DUST43178000BRAVO54167000VIERNES <MISSION"B"RE-EDIT>65105000GUESS WHO'S BACK76140000BABY BABY・・・DON'T STOP!87130000DESTINO <MISSION"B"REMIX POWER -UP VERSION>98150000SO HIGH109191000BAILAN MUY BIEN1110146000HYPER TECHNO fairy1211120000A LOVE AT FIRST SIGHT <MISSION"HMX"REMIX>1312173000BLUE EYES1413137000MADE IN NEWYORK <DJ KEN-BOW EDIT>1514159000BILLY JIVE (WITH WILLY'S WIFE) <Y & Co. REMIX>1615204000U TURN ME ON <SUPER RAVE REMIX>17246000[credits / behind the scenes]246000181699000TEMPO <ONLY THE SHORT TechPara SHOW VERSION>././@LongLink0000000000000000000000000000015000000000000011211 Lustar 00000000000000musicbrainzngs-0.6/test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xmlmusicbrainzngs-0.6/test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.0000664000175000017500000002021012041524063033061 0ustar alastairalastair00000000000000 Ruined SubjectsOfficialNonenormalengJT BruceBruce, JT2011-08-09XW111246000Pollux246000JT BruceBruce, JT2296000Vega96000JT BruceBruce, JT33132000Deneb132000JT BruceBruce, JT44536000Sirius536000JT BruceBruce, JT55132000Descent132000JT BruceBruce, JT6644000The Grand Machine44000JT BruceBruce, JT77In the Clounds68000In The Clounds68000JT BruceBruce, JT88148000Paranoia148000JT BruceBruce, JT99160000Cubic160000JT BruceBruce, JT1010251000Separation251000JT BruceBruce, JT1111239000Retarded Retard239000JT BruceBruce, JT1212129000Umlaut Ampersand129000JT BruceBruce, JT131316000Trees16000JT BruceBruce, JT141486000The Multiverse86000JT BruceBruce, JT1515171000Flux's Curiosity171000JT BruceBruce, JT1616116000Infinimarch116000JT BruceBruce, JT1717152000Deathboat152000JT BruceBruce, JT1818105000Painter's Vista105000JT BruceBruce, JT1919194000The City194000JT BruceBruce, JT2020123000New Beginning123000JT BruceBruce, JT212154000Memories of Onus54000JT BruceBruce, JTmusicbrainzngs-0.6/test/data/release/9ce41d09-40e4-4d33-af0c-7fed1e558dba-recordings.xml0000664000175000017500000014002512621332023031304 0ustar alastairalastair00000000000000 Ramasser les morceaux : Le punk pour les nul-le-sOfficialnormalCardboard/Paper Sleevemul2013-05FR2013-05FranceFranceFRfalse0falsefalse111172000Kick Out the Jams17230622199000My Generation19900033205000Because the Night20206644351000Politicians in My Eyes35100055379000Hero37900066Fire Engine193000Fire Engine (studio: 1975-08-xx: New York City, NY, USA)19300077152000The KKK Took My Baby Away15150688261000TV Eye26100099216000Transmission2162661010149000P381490001111142000Identity1427731212186000Warhead185640single version1313158000One Chord Wonders1550001414120000We’re Desperate1200661515127000Lexicon Devil1270001616124000Parece que somos lo que no somos pero queremos ser1240001717175000Utopia1750001818114000System Fails but the Law Prevails1140001919286000This Land2860002020197000Berkertex Bribe1970002121108000Beautiful Pictures1080002222100000Toilet Stories1000002323106000שמיקה1060002424145000Venganza1450002525195000Romeo’s Distress1950002626Marian344000Marian (version)3448661985 original mix2727243000This Time2427602828249000Eve at the Mansion2490002929228000La ciega2280003030284000Siren2840003131171000All Women Are Bitches1710003232230000Ask Nicely2300003333147000Eating Toothpaste146946343493000No Make Up Tips930003535100000Coffee, Tea, and Misery10000036361740001411740003737181000Pressure Drop1790003838239000I Heard It Through the Grapevine2390003939244000(White Man) In Hammersmith Palais2410004040127000Reclaim the Streets1270004141230000City City2300004242347000Murder on the Orange St.3470004343173000A Message to You Rudy1732264444188000Too Nice to Talk To1878934545201000Missing Words2010004646347000Setki dwie...3470004747147000POD1470004848156000Looking Back1560004949375000Go to a War3750005050116000X1157205151287000British Justice2870005252445000Loonitsch4450005353163000Back to the Bark1630005454345000Black Vomit3450005555140000Six Pack1404405656120000Riot Squad1196661991 remaster5757181000California über alles1824665858116000I Remember1186405959121000Young ’til I Die1210006060140000Good Guys (Don't Wear White)1400006161288000No Fuckin' War2880006262165000Dura Rotina1650006363143000Sally1430006464123000Gata1230006565132000Borstal Breakout1310006666224000We Are the Firm2240006767192000Self Conscious Over You1920006868La victoire sommeille205000La Victoire sommeille2050006969135000Latinos migrantes1350007070186000Vi odio1860007171125000You1250007272327000Last Will & Testament3270007373188000Out of My Mind1879067474154000Oak Tree154000757590000La plaga desplazadora900007676170000Alting går i sort1700007777345000Coming Home3446667878183000Here Today182000797992000Police Bastard92000808099000Traitor990008181172000Complacency Is Dead1720008282142000Ums Ganze142000838382000Nazi Punks Fuck Off810008484161000I'm a Bloody Fool1610008585185000Dead Smart184506868682000Evacuation820008787142000Jeden cel a tak daleko142000888851000Social Nihilism510008989160000Still Jolly After All These Years1600009090185000Sequoia Song1850009191126000June Flew1260009292236000Nosebleed of Dawn2360009393186000No One Dies Alone186000949437000Youth Attack!37000959589000Eran Profit, Våran Död89000969682000Kom82000979750000Meatman47000989890000Hardcore by the Book900009999357000Civilized Worm357000100100484000The Beginning and the End481960101101263000To Walk Alone262000102102408000Angel Ranger408000103103268000Zakrztuś Się Władzą268000104104146000Inquisition146000105105230000Waiting for Mary230000106106275000Sacrifice275000107107309000Now309000108108294000Jassica294000109109296000TV Show (Hang the Police)296000110110254000Whoever Has Such a Treasure Is Not Always Worried About Those Who Do Not254000111111138000Deeper Than Inside137400112112158000Money157893113113256000Turnover256026114114202000Wiederholungszwang202000115115324000Dad's Wings324000116116173000Choose173000117117181000Left Hand181000118118134000New Jersey vs. Valhalla134000119119158000Wishful Thinking15800012012092000Above Grey Clouds92000121121231000What Do You Mean?231000122122240000Невозможно молчать240000123123193000Under the Radar193000124124209000Dreaming Drowning209000125125276000El último aliento276000126126147000Half Lit World147000127127156000Τελευταίες Κραυγές156000128128209000The End Is Near209000129129134000Caravan134000130130137000Banzai Washout137000131131150000Moon Dawg150000132132158000Nessuno mi può giudicare158000133133155000Escape From Gulag 17155000134134152000Hochzeit in Bistritja152000135135160000The Witch161000136136150000Heart Full of Soul14900013713787000Boys Are Boys and Girls Are Choice86000138138120000Nonverbal Adjective Agreement120000139139239000Soul Hunters239000140140186000The Walnut Tree186000141141231000Queen of Pain230360142142183000When Death Rides a Horse183000143143179000Rot in Hell!177200144144338000Born, Raised, Passed Away (In East L.A.)3380001451451490008 Millions God149000146146239000Lonely Old Soul239000147147311000Dracula Mountain311000148148260000To Parter26000014914972000Moth‐Eaten Deer Head72000150150104000Itsuwari104000151151173000La saponatrice di ferrara173000152152135000Nightmares on Crack St.135000153153157000Promises155000154154119000I Hate You, I Love You119000155155177000My Right177000156156186000Not Happiness186000157157188000Blind18800015815873000Desquicio73000159159345000Wipeout Beat345000160160215000Mongoloid215000161161196000Assassin196200162162168000Siniaki168000163163226000Flooded Grass226000164164262000Намести фризура и гладувај261866165165153000Streams of Whiskey152960166166176000Girlz on the Fiddle176000167167190000Are You Drinkin’ With Me Jesus190000168168226000Warsaw Is Khelm225000169169330000I Can't Love This Country330000170170299000Самокрутка29900017117195000All You Fascists95000172172171000Bratříčku, zavírej vrátka171000173173164000Особый резон164000174174154000To Have and to Have Not153493175175265000Forever at Sea265000176176346000Amor Enkapuchado346000177177143000I Wouldn't Want to Live in a World Without Grudges143000178178274000Sex Changes274000179179190000Hystérie connective190000180180165000J'suis punk165000181181106000Killer Man106000182182281000Hôpital (live)281000183183212000Edith Nylon212000184184221000Né pour crever22100018518594000Ton meilleur ami94000186186231000Espoirs déçus23100018718796000Varsovie93773188188213000Crazy Voodoo Woman213000189189166000Pile ou face164000190190207000Qui jettera la dernière pierre ?207000191191243000Okinawa242800192192199000Leyla199000193193197000L'amour est enfant de bohême197000194194204000Avorter n’est pas tuer204000195195289000Atlantique Nord289000196196123000Espèce humaine123000197197254000Exilés254000198198Radio Paris119000Radio-Paris119000musicbrainzngs-0.6/test/data/release/fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml0000664000175000017500000003112112274714652030725 0ustar alastairalastair00000000000000 UrkOfficialnormalDigipakeng2007-02-02DE2007-02-02GermanyGermanyDE886970007092B000LXHGR6false0falsefalse111184000The Train18400022298466Adieu Sweet Bahnhof29846633197960J.O.S. Days19796044299666Sketches of Spain29966655215400In the Dutch Mountains21540066264400The Dream26440077310466The Swimmer31046688242133The House24213399451373Two Skaters4513731010198800Cabins1988001111356493Nescio3564931212272306Pelican & Penguin2723061313157026Telephone Song1570261414296666Dapperstreet296666211259000Port of Amsterdam25900022279133Bike in Head27913333276000Mountain Jan27600044212493Walter & Connie21249355217306A Touch of Henry Moore21730666185666The Bauhaus Chair18566677251666Under a Canoe25166688195826Shadow of a Doubt19582699351000Mask3510001010205000Home Before Dark2050001111239240The Panorama Man2392401212232026Slip of the Tongue2320261313280840An Eating House2808401414139426Red Tape1394261515208706Tons of Ink20870631169000[intro]6900022265000The Dream26500033446000Two Skaters44600044295000Sketches of Spain29500055297000Adieu Sweet Bahnhof29700066241000A Touch of Henry Moore24100077279000An Eating House27900088281000Nescio28100099269000The Hat2690001010243000Port of Amsterdam2430001111361000Mask3610001212330000In the Dutch Mountains3300001313202000Cabins2020001414305000Tons of Ink3050001515303000The Swimmer3030001616255000Blue2550001717231000Slip of the Tongue2310001818227000The House2270001919168000Telephone Songs168000musicbrainzngs-0.6/test/data/work/0000775000175000017500000000000012702713133020712 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/work/80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml0000664000175000017500000000111612551472207027604 0ustar alastairalastair00000000000000 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"musicbrainzngs-0.6/test/data/work/72c9aad2-3c95-4e3e-8a01-3974f8fef8eb-series-rels.xml0000664000175000017500000000131312702700144030707 0ustar alastairalastair00000000000000Cantata, BuxWV 1 "Accedite gentes, accurite populi"lat0790fa51-15d9-40a2-bca9-9c8eaaa96bef1backwardnumberBuxtehude-Werke-Verzeichnismusicbrainzngs-0.6/test/data/work/3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml0000664000175000017500000000305212107170162030226 0ustar alastairalastair00000000000000Symphony 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]musicbrainzngs-0.6/test/data/work/8e134b32-99b8-4e96-ae5c-426f3be85f4c-attributes.xml0000664000175000017500000000065212702700144030510 0ustar alastairalastair00000000000000Hüzzam PeşrevzxxHüzzamPeşrevFahtemusicbrainzngs-0.6/test/data/label/0000775000175000017500000000000012702713133021007 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/label/022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml0000664000175000017500000000106212107170162027640 0ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/label/e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml0000664000175000017500000000101512107170162030071 0ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/search-instrument.xml0000664000175000017500000007301312642723062024136 0ustar alastairalastair00000000000000bassBass 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大阮musicbrainzngs-0.6/test/data/search-artist.xml0000664000175000017500000001615212041524063023226 0ustar alastairalastair00000000000000 Dynamo Go Dynamo Go NZ 2005-06 Dynamo Go testfoo Dynamo Dynamo Dynamo Dynamo Dynamo FI Finnish punk band Dynamo punk finland Dynamo Dynamo DE german DIN label owner Torsten Pröfrock Dynamo Dynamo Dynamo designer Dynamo Productions Dynamo Productions Dynamo Producions Dynamo Productions Dynamo City Dynamo City Dynamo City Dynamo 5 Dynamo 5 Dynamo 5 Kruunuhaan Dynamo Kruunuhaan Dynamo Kruunuhaan Dynamo Kidd Dynamo Kidd Dynamo Kidd Dynamo Onslaught Dynamo Onslaught Dynamo Onslaught Dynamo Dynamo Electrix Dynamo Electrix Dynamo Electrix Dynamo Ska Dynamo Ska Dynamo Ska Dynamo Chapel Dynamo Chapel Dynamo Chapel Johnny Dynamo Dynamo, Johnny Johnny Dynamo Dynamo Früchtebonus Dynamo Früchtebonus AT Dynamo Früchtebonus Dynamo and JP Dynamo and JP Dynamo and JP The Driven Dynamo Driven Dynamo, The The Driven Dynamo Tuttle & Dynamo Laboratory Tuttle & Laboratory, Dynamo Tuttle & Dynamo Laboratory The Dynamo Hymn Dynamo Hymn, The The Dynamo Hymn Go! Go! US Go! Go Go Go GO!GO!7188 GO!GO!7188 JP 1998-06 2012-02-10 GOGO7188 GO! GO! 7188 GO!GO!7188 GOGO 7188 rock japanese Go Robot, Go! Go Robot, Go! Go Robot, Go! Gaijin A Go Go Gaijin A Go Go Gaijin A Go Go musicbrainzngs-0.6/test/data/release-group/0000775000175000017500000000000012702713133022502 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/release-group/f52bc6a1-c848-49e6-85de-f8f53459a624.xml0000664000175000017500000000061312041524063030037 0ustar alastairalastair00000000000000 Super Meat Boy!2010-10-27AlbumSoundtrackmusicbrainzngs-0.6/test/data/artist/0000775000175000017500000000000012702713133021236 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml0000664000175000017500000000043512107170162030241 0ustar alastairalastair00000000000000ErrorsErrorsGB2004musicbrainzngs-0.6/test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml0000664000175000017500000000442612107170162030252 0ustar alastairalastair00000000000000Сергей Сергеевич Прокофьев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プロコフィエフmusicbrainzngs-0.6/test/data/recording/0000775000175000017500000000000012702713133021704 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/recording/f606f733-c1eb-43f3-93c1-71994ea611e3-artist-rels.xml0000664000175000017500000000275012702700144031406 0ustar alastairalastair00000000000000Shades 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, Billymusicbrainzngs-0.6/test/data/recording/0708f241-f654-4b3b-98f1-64ae31e1d3ac-artist-rels.xml0000664000175000017500000000112712672245510031457 0ustar alastairalastair00000000000000Battle DEILFinition 20153618576031277c6-5b54-457a-8da9-91ad01d499b5backwardDJ DeilfDeilf, DJDj DEILFmusicbrainzngs-0.6/test/data/place/0000775000175000017500000000000012702713133021014 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/place/0c79cdbb-acd6-4e30-aaa3-a5c8d6b36a48-aliases-tags.xml0000664000175000017500000000137712672234247031337 0ustar alastairalastair00000000000000All Saints' ChurchEast Finchley, Durham Road
38 Durham Road, London N2 9DP, United Kingdom
51.591812-0.159699LondonLondon1891All Saints' Durham Roadtype=church
musicbrainzngs-0.6/test/data/place/browse-area-74e50e58-5deb-4b99-93a2-decbb365c07f-annotation.xml0000664000175000017500000001044312672234247033146 0ustar alastairalastair00000000000000440 StudiosAdrian Carr StudiosArt D'Lugoff's Top of the GateApex StudiosAtomic Heart StudiosArf MasteringAudioMASTER Vargas
New York, NY 11367
A&R Recording Studio
322 West 48 Street
40.761498-73.988169
A.C. Pianocraft Recital HallAura Recording Studios
136 West 52nd Street, New York, New York 10019
3rd Floora.l.l. digitalAllido Studiosstudio for Mark Ronson's Allido labelAllegro Sound Studios
1650 Broadway, West 51st Street, 7th Avenue
was 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_Broadway
American Record Corporation Studios
1776 Broadway, NYC
Academy of MusicNew York City
125 East 14th Street, New York, NY 10003
40.734568-73.98848918541926true
Avator StudioAvatar Studios, Studio A
441 West 53rd Street, New York, NY 10019
40.766389-73.989444
Anderson Theater
66 2nd Ave
40.725979-73.989571957-081977true
39th Street Music StudioAlgoRhythms321 StudiosApollo Theater
253 West 125th Street, Manhattan, New York City, USA
40.810047-73.950151914
Aeolian Hall
29-33 West 42nd Street, New York
19121926true
Avatar Studios
441 West 53rd Street, New York, NY 10019
40.766389-73.9894441996-05
musicbrainzngs-0.6/test/data/event/0000775000175000017500000000000012702713133021051 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/event/e921686d-ba86-4122-bc3b-777aec90d231-tags-artist-rels.xml0000664000175000017500000000365612672234247031603 0ustar alastairalastair00000000000000Skunk 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-titlemusicbrainzngs-0.6/test/data/event/770fb0b4-0ad8-4774-9275-099b66627355-place-rels.xml0000664000175000017500000000132712621332023030043 0ustar alastairalastair000000000000001987-06-07: Rock am Ring, Nürburgring, Nürburg, Germany1987-06-071987-06-077643f13a-dcda-4db4-8196-3ffcc1b99ab7Nürburgring
Nürburgring Boulevard 1, 53520 Nürburg
50.335566.9475
musicbrainzngs-0.6/test/data/search-release.xml0000664000175000017500000006506512041524063023347 0ustar alastairalastair00000000000000 Affordable Pop Music Official eng Dynamo Go Dynamo Go 2008-07-04 NZ 9421021463277 WS06 4 CD Affordable Luxury Official eng Spielerfrau Spielerfrau 2005 US 5 Affordable Art Steve Goodman Goodman, Steve 1983 B000000DLV 12 Pop Music Official fra Thierry Hazard Hazard, Thierry 1990-11 FR 5099746735425 B00004UI3D 467354 2 12 CD Pop Music Official fra Thierry Hazard Hazard, Thierry 1990-11 FR 5099746735449 467354 4 12 Cassette Pop Music Official eng Todor Kobakov Kobakov, Todor 2009-10-13 CA B002MED6QW 11 Pop Music Official eng Iggy Pop Pop, Iggy B0000072ZP 20 Pop Music Official srp Eva Braun Eva Braun Serbian pop-rock band 1995 CS 15 Cassette This Is Pop Music Official eng Espen Lind Lind, Espen 2000 NO B00005L90U 10 CD Pop The Music Promotion eng Triim Triim 2005 FR 1 This Is Pop Music eng Espen Lind Lind, Espen 2001 JP B00005L90U 11 CD Stereophonic Pop Art Music Official Alpha Stone Alpha Stone B000003JGG 8 Pepsi Pop Music Promotion eng Various Artists Various Artists add compilations to this artist Various Artists 8 Swedish Pop Music Promotion eng Various Artists Various Artists add compilations to this artist Various Artists 2009 SE 20 CD Better Pop Music Promotion eng Various Artists Various Artists add compilations to this artist Various Artists 2010-05-20 GB 19 Digital Media Kellogg's Pop Music Promotion eng Various Artists Various Artists add compilations to this artist Various Artists 1998 US 634133052492 DPSM 5249 2 CD This Is Pop Music. Promotion eng Various Artists Various Artists add compilations to this artist Various Artists 5 Pop Music United Official eng Paul Avion Avion, Paul 2006 US 837101239905 B000K97S8I 8 CD Dance / Pop Music Official eng Neil Watson Watson, Neil Mark Sandell Sandell, Mark US CHAPAV 47 17 Digital Media Pop Music for Dancing Official eng Ted Atking & His Orchestra Ted Atking & His Orchestra 1970 FR SPS 1311 12 Vinyl Pop! Justice: 100% Solid Pop Music Official eng Various Artists Various Artists add compilations to this artist Various Artists 2006-10-23 GB B000JCESCU 23 Proiect Special OMV: Pop Music Promotion eng Various Artists Various Artists add compilations to this artist Various Artists 2007 RO 16 CD 20 Years of Pop Music Official eng Various Artists Various Artists add compilations to this artist Various Artists 2001-07-03 US B00005LNIL 15 Cairo Cafe: Arabic Pop Music ara [unknown] [unknown] Special Purpose Artist - Do not add releases here, if possible. 2008 12 CD Dance / Pop Music 4 Official eng Various Artists Various Artists add compilations to this artist Various Artists US CHAPAV 102 44 Digital Media musicbrainzngs-0.6/test/data/search-release-group.xml0000664000175000017500000004227512041524063024477 0ustar alastairalastair00000000000000 Affordable Pop Music Dynamo Go Dynamo Go Affordable Pop Music Affordable Art Steve Goodman Goodman, Steve Affordable Art Affordable Luxury Spielerfrau Spielerfrau Affordable Luxury Pop Music Eva Braun Eva Braun Serbian pop-rock band Pop Music Pop Music Thierry Hazard Hazard, Thierry Pop Music Pop Music Pop Music Todor Kobakov Kobakov, Todor Pop Music Pop Music Iggy Pop Pop, Iggy Pop Music Stereophonic Pop Art Music Alpha Stone Alpha Stone Stereophonic Pop Art Music Pop Music for Dancing Ted Atking & His Orchestra Ted Atking & His Orchestra Pop Music for Dancing This Is Pop Music. Various Artists Various Artists add compilations to this artist Various Artists This Is Pop Music. Pop The Music Triim Triim Pop The Music Better Pop Music Various Artists Various Artists add compilations to this artist Various Artists Better Pop Music Dance / Pop Music Neil Watson Watson, Neil Mark Sandell Sandell, Mark Dance / Pop Music Pepsi Pop Music Various Artists Various Artists add compilations to this artist Various Artists Pepsi Pop Music Kellogg's Pop Music Various Artists Various Artists add compilations to this artist Various Artists Kellogg's Pop Music Swedish Pop Music Various Artists Various Artists add compilations to this artist Various Artists Swedish Pop Music Pop Music Highlights Various Artists Various Artists add compilations to this artist Various Artists Pop Music United EP Paul Avion Avion, Paul Pop Music United This Is Pop Music Espen Lind Lind, Espen This Is Pop Music This Is Pop Music Pop! Justice: 100% Solid Pop Music Various Artists Various Artists add compilations to this artist Various Artists Pop! Justice: 100% Solid Pop Music barbadian 20 Years of Pop Music Various Artists Various Artists add compilations to this artist Various Artists 20 Years of Pop Music Proiect Special OMV: Pop Music Various Artists Various Artists add compilations to this artist Various Artists Proiect Special OMV: Pop Music Dance / Pop Music 4 Various Artists Various Artists add compilations to this artist Various Artists Dance / Pop Music 4 Dance / Pop Music 2 Neil Watson Watson, Neil Mark Sandell Sandell, Mark Dance / Pop Music 2 Dance / Pop Music 3 Neil Watson Watson, Neil Mark Sandell Sandell, Mark Dance / Pop Music 3 musicbrainzngs-0.6/test/data/search-recording.xml0000664000175000017500000007515412041524063023703 0ustar alastairalastair00000000000000 Thief of Hearts 198586 Dynamo Go Dynamo Go Folly, Vice & Madness Official 2006-11-03 NZ 5 1 CD Thief of Hearts Waiting My Turn 187706 Dynamo Go Dynamo Go Folly, Vice & Madness Official 2006-11-03 NZ 5 1 CD Waiting My Turn Just a Victim 226000 Dynamo Go Dynamo Go Folly, Vice & Madness Official 2006-11-03 NZ 5 1 CD Just a Victim Retail Guru 114493 Dynamo Go Dynamo Go Folly, Vice & Madness Official 2006-11-03 NZ 5 1 CD Retail Guru If this Beard Had Wings 253346 Dynamo Go Dynamo Go Folly, Vice & Madness Official 2006-11-03 NZ 5 1 CD If this Beard Had Wings Headrush 45973 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Headrush Final Reunion 193253 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Final Reunion Nothing Ever Happens 92013 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Nothing Ever Happens Johnny the Punk 190640 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Johnny the Punk Already Met Your Mother 231706 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Already Met Your Mother Route 17 205106 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Route 17 Sad Again 181053 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Sad Again Fountain City 172760 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Fountain City Crying On My Street 144306 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Crying On My Street Frozen to the Bone 166920 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD Frozen to the Bone What Went Wrong? 148826 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD What Went Wrong? You Fell From The Sky 194826 Dynamo Go Dynamo Go The Fool of Fountain City Official 2009-04-03 NZ 12 1 CD You Fell From The Sky Poor Alfred 151000 Dynamo Go Dynamo Go Poor Alfred Official 2010 NZ 2 1 Digital Media Poor Alfred Talk Quickly 120000 Dynamo Go Dynamo Go Poor Alfred Official 2010 NZ 2 1 Digital Media Talk Quickly Sad Again 275426 Dynamo Go Dynamo Go Affordable Pop Music Official 2008-07-04 NZ 4 1 CD Sad Again Dead By Morning 115933 Dynamo Go Dynamo Go Affordable Pop Music Official 2008-07-04 NZ 4 1 CD Dead By Morning New Bed Science 204613 Dynamo Go Dynamo Go Affordable Pop Music Official 2008-07-04 NZ 4 1 CD New Bed Science Middle Class 200360 Dynamo Go Dynamo Go Affordable Pop Music Official 2008-07-04 NZ 4 1 CD Middle Class Thief 294826 Third Day Third Day Offerings Official 2000-07-11 US 11 1 CD Thief Thief 306333 CAN CAN German rock band Delay 1968 Official 1981 US 7 1 Thief Delay 1968 Official 2006-05-29 GB 7 1 SACD Thief Delay 1968 Official 1981 DE 7 1 Vinyl Thief Delay 1968 Official 1989 CH 7 1 CD Thief to radiohead stop ruining can musicbrainzngs-0.6/test/data/search-label.xml0000664000175000017500000000075012041524063022774 0ustar alastairalastair00000000000000 musicbrainzngs-0.6/test/data/collection/0000775000175000017500000000000012702713133022063 5ustar alastairalastair00000000000000musicbrainzngs-0.6/test/data/collection/855b134e-9a3b-4717-8df8-8c4838d89924-places.xml0000664000175000017500000000075412672234247030500 0ustar alastairalastair00000000000000place collectionalastairpSan Francisco Bath Houseaka 'San Fran'
171 Cuba Street, Wellington, New Zealand
musicbrainzngs-0.6/test/data/collection/29611d8b-b3ad-4ffb-acb5-27f77342a5b0-artists.xml0000664000175000017500000000076012672234247031302 0ustar alastairalastair00000000000000artist collectionalastairpQueenQueenUK rock group1970-06-27musicbrainzngs-0.6/test/data/collection/20562e36-c7cc-44fb-96b4-486d51a1174b-events.xml0000664000175000017500000000070312672234247030622 0ustar alastairalastair00000000000000event collectionalastairpT on the Fringe 20062006-08-042006-08-30musicbrainzngs-0.6/test/data/collection/a91320b2-fd2f-4a93-9e4e-603d16d514b6-recordings.xml0000664000175000017500000000065412672234247031534 0ustar alastairalastair00000000000000recording collectionalastairpMaggot Brain1201000musicbrainzngs-0.6/test/data/collection/2326c2e8-be4b-4300-acc6-dbd0adf5645b-works.xml0000664000175000017500000000056112672234247031022 0ustar alastairalastair00000000000000work collectionalastairpMaggot Brainmusicbrainzngs-0.6/test/data/collection/0b15c97c-8eb8-4b4f-81c3-0eb24266a2ac-releases.xml0000664000175000017500000003641512621332023031323 0ustar alastairalastair00000000000000My 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 RevolutionOfficialnormaleng1990DE1990GermanyGermanyDEmusicbrainzngs-0.6/test/test_collection.py0000664000175000017500000000766112551472207022603 0ustar alastairalastair00000000000000import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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 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) 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) musicbrainzngs-0.6/test/test_mbxml_event.py0000664000175000017500000000333012672234247022761 0ustar alastairalastair00000000000000# Tests for parsing of event results import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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, )) musicbrainzngs-0.6/test/test_submit.py0000664000175000017500000000241712672234247021751 0ustar alastairalastair00000000000000import unittest import os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import musicbrainzngs from musicbrainzngs import musicbrainz from test import _common class SubmitTest(unittest.TestCase): 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 musicbrainzngs-0.6/test/test_mbxml_place.py0000664000175000017500000000324612672234247022732 0ustar alastairalastair00000000000000# Tests for parsing of place results import unittest import os import sys # Insert .. at the beginning of path so we use this version instead # of something that's already been installed sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 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.assertEquals("All Saints' Church", p["name"]) self.assertEquals("East Finchley, Durham Road", p["disambiguation"]) self.assertEquals("38 Durham Road, London N2 9DP, United Kingdom", p["address"]) self.assertEquals({"latitude": "51.591812", "longitude": "-0.159699"}, p["coordinates"]) self.assertEquals("f03d09b3-39dc-4083-afd6-159e3f0d462f", p["area"]["id"]) self.assertEquals("1891", p["life-span"]["begin"]) self.assertEquals("All Saints' Durham Road", p["alias-list"][0]["alias"]) self.assertEquals("type=church", p["tag-list"][0]["name"]) self.assertEquals("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")) musicbrainzngs-0.6/setup.py0000664000175000017500000000415612621332023017553 0ustar alastairalastair00000000000000#!/usr/bin/env python from distutils.core import setup from distutils.core import Command from musicbrainzngs import musicbrainz class test(Command): description = "run automated tests" user_options = [ ("tests=", None, "list of tests to run (default all)"), ("verbosity=", "v", "verbosity"), ] def initialize_options(self): self.tests = [] self.verbosity = 1 def finalize_options(self): if self.tests: self.tests = self.tests.split(",") if self.verbosity: self.verbosity = int(self.verbosity) def run(self): import os.path import glob import sys import unittest build = self.get_finalized_command('build') self.run_command ('build') sys.path.insert(0, build.build_purelib) sys.path.insert(0, build.build_platlib) names = [] for filename in glob.glob("test/test_*.py"): name = os.path.splitext(os.path.basename(filename))[0] if not self.tests or name in self.tests: names.append("test." + name) tests = unittest.defaultTestLoader.loadTestsFromNames(names) t = unittest.TextTestRunner(verbosity=self.verbosity) result = t.run(tests) sys.exit(not result.wasSuccessful()) setup( name="musicbrainzngs", version=musicbrainz._version, description="Python bindings for the MusicBrainz NGS and" " the Cover Art Archive webservices", author="Alastair Porter", author_email="alastair@porter.net.nz", url="https://python-musicbrainzngs.readthedocs.org/", packages=['musicbrainzngs'], cmdclass={'test': test }, 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" ] ) musicbrainzngs-0.6/COPYING0000664000175000017500000000410412160273072017073 0ustar alastairalastair00000000000000Copyright 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. musicbrainzngs-0.6/query.py0000664000175000017500000000227012621332023017553 0ustar alastairalastair00000000000000import 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_puid("070359fc-8219-e62b-7bfd-5a01e742b490") #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}) #m.submit_echoprints({"e97f805a-ab48-4c52-855e-07049142113d": "anechoprint1234567"}) if __name__ == "__main__": main() musicbrainzngs-0.6/CHANGES0000664000175000017500000001174112702701351017036 0ustar alastairalastair000000000000000.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 musicbrainzngs-0.6/README.rst0000664000175000017500000000461712642723013017540 0ustar alastairalastair00000000000000Musicbrainz 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. musicbrainzngs-0.6/examples/0000775000175000017500000000000012702713133017656 5ustar alastairalastair00000000000000musicbrainzngs-0.6/examples/collection.py0000775000175000017500000001577712672234247022421 0ustar alastairalastair00000000000000#!/usr/bin/env python """View and modify your MusicBrainz collections. To show a list of your collections: $ ./collection.py USERNAME Password for USERNAME: All collections for this user: My Collection by USERNAME (4137a646-a104-4031-b549-da4e1f36a463) To show the releases in a collection: $ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463 Password for USERNAME: Releases in My Collection: None Shall Pass (b0885908-cbe2-4e51-95d8-c4f3b9721ad6) ... To add a release to a collection or remove one: $ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463 --add 0d432d8b-8865-4ae9-8479-3a197620a37b $ ./collection.py USERNAME 4137a646-a104-4031-b549-da4e1f36a463 --remove 0d432d8b-8865-4ae9-8479-3a197620a37b """ from __future__ import print_function from __future__ import unicode_literals import musicbrainzngs import getpass from optparse import OptionParser import sys try: user_input = raw_input except NameError: user_input = input musicbrainzngs.set_useragent( "python-musicbrainzngs-example", "0.1", "https://github.com/alastair/python-musicbrainzngs/", ) def show_collections(): """Fetch and display the current user's collections. """ result = musicbrainzngs.get_collections() print('All collections for this user:') for collection in result['collection-list']: # entity-type only available starting with musicbrainzngs 0.6 if "entity-type" in collection: print('"{name}" by {editor} ({cat}, {count} {entity}s)\n\t{mbid}' .format( name=collection['name'], editor=collection['editor'], cat=collection['type'], entity=collection['entity-type'], count=collection[collection['entity-type']+'-count'], mbid=collection['id'] )) else: print('"{name}" by {editor}\n\t{mbid}'.format( name=collection['name'], editor=collection['editor'], mbid=collection['id'] )) def show_collection(collection_id, ctype): """Show a given collection. """ if ctype == "release": result = musicbrainzngs.get_releases_in_collection( collection_id, limit=0) elif ctype == "artist": result = musicbrainzngs.get_artists_in_collection( collection_id, limit=0) elif ctype == "event": result = musicbrainzngs.get_events_in_collection( collection_id, limit=0) elif ctype == "place": result = musicbrainzngs.get_places_in_collection( collection_id, limit=0) elif ctype == "recording": result = musicbrainzngs.get_recordings_in_collection( collection_id, limit=0) elif ctype == "work": result = musicbrainzngs.get_works_in_collection( collection_id, limit=0) collection = result['collection'] # entity-type only available starting with musicbrainzngs 0.6 if "entity-type" in collection: print('{mbid}\n"{name}" by {editor} ({cat}, {entity})'.format( name=collection['name'], editor=collection['editor'], cat=collection['type'], entity=collection['entity-type'], mbid=collection['id'] )) else: print('{mbid}\n"{name}" by {editor}'.format( name=collection['name'], editor=collection['editor'], mbid=collection['id'] )) print('') # release count is only available starting with musicbrainzngs 0.5 if "release-count" in collection: print('{} releases'.format(collection['release-count'])) if "artist-count" in collection: print('{} artists'.format(collection['artist-count'])) if "event-count" in collection: print('{} events'.format(collection['release-count'])) if "place-count" in collection: print('{} places'.format(collection['place-count'])) if "recording-count" in collection: print('{} recordings'.format(collection['recording-count'])) if "work-count" in collection: print('{} works'.format(collection['work-count'])) print('') if "release-list" in collection: show_releases(collection) else: pass # TODO def show_releases(collection): result = musicbrainzngs.get_releases_in_collection(collection_id, limit=25) release_list = result['collection']['release-list'] print('Releases:') releases_fetched = 0 while len(release_list) > 0: print("") releases_fetched += len(release_list) for release in release_list: print('{title} ({mbid})'.format( title=release['title'], mbid=release['id'] )) if user_input("Would you like to display more releases? [y/N] ") != "y": break; # fetch next batch of releases result = musicbrainzngs.get_releases_in_collection(collection_id, limit=25, offset=releases_fetched) collection = result['collection'] release_list = collection['release-list'] print("") print("Number of fetched releases: %d" % releases_fetched) if __name__ == '__main__': parser = OptionParser(usage="%prog [options] USERNAME [COLLECTION-ID]") parser.add_option('-a', '--add', metavar="RELEASE-ID", help="add a release to the collection") parser.add_option('-r', '--remove', metavar="RELEASE-ID", help="remove a release from the collection") parser.add_option('-t', '--type', metavar="TYPE", default="release", help="type of the collection (default: release)") options, args = parser.parse_args() if not args: parser.error('no username specified') username = args.pop(0) # Input the password. password = getpass.getpass('Password for {}: '.format(username)) # Call musicbrainzngs.auth() before making any API calls that # require authentication. musicbrainzngs.auth(username, password) if args: # Actions for a specific collction. collection_id = args[0] if options.add: if option.type == "release": musicbrainzngs.add_releases_to_collection( collection_id, [options.add] ) else: sys.exit("only release collections can be modified ATM") elif options.remove: if option.type == "release": musicbrainzngs.remove_releases_from_collection( collection_id, [options.remove] ) else: sys.exit("only release collections can be modified ATM") else: # Print out the collection's contents. print("") show_collection(collection_id, options.type) else: # Show all collections. print("") show_collections() musicbrainzngs-0.6/examples/releasesearch.py0000775000175000017500000000365012274714562023060 0ustar alastairalastair00000000000000#!/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() musicbrainzngs-0.6/examples/find_disc.py0000775000175000017500000000510712621332023022153 0ustar alastairalastair00000000000000#!/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") musicbrainzngs-0.6/musicbrainzngs/0000775000175000017500000000000012702713133021076 5ustar alastairalastair00000000000000musicbrainzngs-0.6/musicbrainzngs/caa.py0000664000175000017500000001375012551472207022210 0ustar alastairalastair00000000000000# 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 hostname = "coverartarchive.org" def set_caa_hostname(new_hostname): """Set the base hostname for Cover Art Archive requests. Defaults to 'coverartarchive.org'.""" global hostname hostname = new_hostname 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 :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(( '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 return json.loads(resp) def get_image_list(releaseid): """Get the list of cover art associated with a release. The return value is the deserialized response of the `JSON listing `_ returned by the Cover Art Archive API. If an error occurs then a :class:`~musicbrainzngs.ResponseError` will be raised with one of the following HTTP codes: * 400: `Releaseid` is not a valid UUID * 404: No release exists with an MBID of `releaseid` * 503: Ratelimit exceeded """ return _caa_request(releaseid) def get_release_group_image_list(releasegroupid): """Get the list of cover art associated with a release group. The return value is the deserialized response of the `JSON listing `_ returned by the Cover Art Archive API. If an error occurs then a :class:`~musicbrainzngs.ResponseError` will be raised with one of the following HTTP codes: * 400: `Releaseid` is not a valid UUID * 404: No release exists with an MBID of `releaseid` * 503: Ratelimit exceeded """ return _caa_request(releasegroupid, entitytype="release-group") def get_release_group_image_front(releasegroupid, size=None): """Download the front cover art for a release group. The `size` argument and the possible error conditions are the same as for :meth:`get_image`. """ return get_image(releasegroupid, "front", size=size, entitytype="release-group") def get_image_front(releaseid, size=None): """Download the front cover art for a release. The `size` argument and the possible error conditions are the same as for :meth:`get_image`. """ return get_image(releaseid, "front", size=size) def get_image_back(releaseid, size=None): """Download the back cover art for a release. The `size` argument and the possible error conditions are the same as for :meth:`get_image`. """ return get_image(releaseid, "back", size=size) def get_image(mbid, coverid, size=None, entitytype="release"): """Download cover art for a release. The coverart file to download is specified by the `coverid` argument. If `size` is not specified, download the largest copy present, which can be very large. If an error occurs then a :class:`~musicbrainzngs.ResponseError` will be raised with one of the following HTTP codes: * 400: `Releaseid` is not a valid UUID or `coverid` is invalid * 404: No release exists with an MBID of `releaseid` * 503: Ratelimit exceeded :param coverid: ``front``, ``back`` or a number from the listing obtained with :meth:`get_image_list` :type coverid: int or str :param size: 250, 500 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) musicbrainzngs-0.6/musicbrainzngs/compat.py0000664000175000017500000000326412041524063022736 0ustar alastairalastair00000000000000# -*- 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,\ build_opener from httplib import BadStatusLine, HTTPException from urlparse import urlunparse from urllib import urlencode 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 unicode = str bytes = bytes basestring = (str,bytes) musicbrainzngs-0.6/musicbrainzngs/__init__.py0000664000175000017500000000011212326467636023221 0ustar alastairalastair00000000000000from musicbrainzngs.musicbrainz import * from musicbrainzngs.caa import * musicbrainzngs-0.6/musicbrainzngs/util.py0000664000175000017500000000262412117613714022435 0ustar alastairalastair00000000000000# 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 musicbrainzngs-0.6/musicbrainzngs/mbxml.py0000664000175000017500000006725512702700144022604 0ustar alastairalastair00000000000000# 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 musicbrainzngs import util try: from ET import fixtag except: # Python < 2.7 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([], {"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, "echoprint-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") musicbrainzngs-0.6/musicbrainzngs/musicbrainz.py0000664000175000017500000013722012702702563024010 0ustar alastairalastair00000000000000# 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.6" _log = logging.getLogger("musicbrainzngs") LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' # Constants for validation. RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'event', 'recording', 'release', 'release-group', 'series', 'url', 'work', 'instrument'] RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES] TAG_INCLUDES = ["tags", "user-tags"] RATING_INCLUDES = ["ratings", "user-ratings"] VALID_INCLUDES = { 'area' : ["aliases", "annotation"] + RELATION_INCLUDES, 'artist': [ "recordings", "releases", "release-groups", "works", # Subqueries "various-artists", "discids", "media", "isrcs", "aliases", "annotation" ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES, 'annotation': [ ], 'instrument': ["aliases", "annotation" ] + RELATION_INCLUDES + TAG_INCLUDES, 'label': [ "releases", # Subqueries "discids", "media", "aliases", "annotation" ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES, 'place' : ["aliases", "annotation"] + RELATION_INCLUDES + TAG_INCLUDES, 'event' : ["aliases"] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES, 'recording': [ "artists", "releases", # Subqueries "discids", "media", "artist-credits", "isrcs", "annotation", "aliases" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'release': [ "artists", "labels", "recordings", "release-groups", "media", "artist-credits", "discids", "puids", "isrcs", "recording-level-rels", "work-level-rels", "annotation", "aliases" ] + TAG_INCLUDES + RELATION_INCLUDES, 'release-group': [ "artists", "releases", "discids", "media", "artist-credits", "annotation", "aliases" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'series': [ "annotation", "aliases" ] + RELATION_INCLUDES, 'work': [ "artists", # Subqueries "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", "puids", "isrcs", "recording-level-rels", "work-level-rels", "annotation", "aliases" ] + RELATION_INCLUDES, 'isrc': ["artists", "releases", "puids", "isrcs"], 'iswc': ["artists"], 'collection': ['releases'], } VALID_BROWSE_INCLUDES = { 'artist': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'event': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'label': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'recording': ["artist-credits", "isrcs"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'release': ["artist-credits", "labels", "recordings", "isrcs", "release-groups", "media", "discids"] + RELATION_INCLUDES, 'place': ["aliases"] + TAG_INCLUDES + RELATION_INCLUDES, 'release-group': ["artist-credits"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'url': RELATION_INCLUDES, 'work': ["aliases", "annotation"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, } #: These can be used to filter whenever releases are includes or browsed VALID_RELEASE_TYPES = [ "nat", "album", "single", "ep", "broadcast", "other", # primary types "compilation", "soundtrack", "spokenword", "interview", "audiobook", "live", "remix", "dj-mix", "mixtape/street", # secondary types ] #: These can be used to filter whenever releases or release-groups are involved VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"] VALID_SEARCH_FIELDS = { 'annotation': [ 'entity', 'name', 'text', 'type' ], 'area': [ 'aid', 'area', 'alias', 'begin', 'comment', 'end', 'ended', 'iso', 'iso1', 'iso2', 'iso3', 'type' ], 'artist': [ 'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment', 'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type', 'area', 'beginarea', 'endarea' ], 'label': [ 'alias', 'begin', 'code', 'comment', 'country', 'end', 'ended', 'ipi', 'label', 'labelaccent', 'laid', 'sortname', 'type', 'tag', 'area' ], 'recording': [ 'arid', 'artist', 'artistname', 'creditname', 'comment', 'country', 'date', 'dur', 'format', 'isrc', 'number', 'position', 'primarytype', 'puid', 'qdur', 'recording', 'recordingaccent', 'reid', 'release', 'rgid', 'rid', 'secondarytype', 'status', 'tnum', 'tracks', 'tracksrelease', 'tag', 'type', 'video' ], 'release-group': [ 'arid', 'artist', 'artistname', 'comment', 'creditname', 'primarytype', 'rgid', 'releasegroup', 'releasegroupaccent', 'releases', 'release', 'reid', 'secondarytype', 'status', 'tag', 'type' ], 'release': [ 'arid', 'artist', 'artistname', 'asin', 'barcode', 'creditname', 'catno', 'comment', 'country', 'creditname', 'date', 'discids', 'discidsmedium', 'format', 'laid', 'label', 'lang', 'mediums', 'primarytype', 'puid', 'quality', 'reid', 'release', 'releaseaccent', 'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks', 'tracksmedium', 'type' ], 'series': [ 'alias', 'comment', 'sid', 'series', 'type' ], 'work': [ 'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag', 'type', 'wid', 'work', 'workaccent' ], } # Constants class AUTH_YES: pass class AUTH_NO: pass class AUTH_IFSET: pass # Exceptions. class MusicBrainzError(Exception): """Base class for all exceptions related to MusicBrainz.""" pass class UsageError(MusicBrainzError): """Error related to misuse of the module API.""" pass class InvalidSearchFieldError(UsageError): pass class InvalidIncludeError(UsageError): def __init__(self, msg='Invalid Includes', reason=None): super(InvalidIncludeError, self).__init__(self) self.msg = msg self.reason = reason def __str__(self): return self.msg class InvalidFilterError(UsageError): def __init__(self, msg='Invalid Includes', reason=None): super(InvalidFilterError, self).__init__(self) self.msg = msg self.reason = reason def __str__(self): return self.msg class WebServiceError(MusicBrainzError): """Error related to MusicBrainz API requests.""" def __init__(self, message=None, cause=None): """Pass ``cause`` if this exception was caused by another exception. """ self.message = message self.cause = cause def __str__(self): if self.message: msg = "%s, " % self.message else: msg = "" msg += "caused by: %s" % str(self.cause) return msg class NetworkError(WebServiceError): """Problem communicating with the MB server.""" pass class ResponseError(WebServiceError): """Bad response sent by the MB server.""" pass class AuthenticationError(WebServiceError): """Received a HTTP 401 response while accessing a protected resource.""" pass # Helpers for validating and formatting allowed sets. def _check_includes_impl(includes, valid_includes): for i in includes: if i not in valid_includes: raise InvalidIncludeError("Bad includes: " "%s is not a valid include" % i) def _check_includes(entity, inc): _check_includes_impl(inc, VALID_INCLUDES[entity]) def _check_filter(values, valid): for v in values: if v not in valid: raise InvalidFilterError(v) def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[]): """Check that the status or type values are valid. Then, check that the filters can be used with the given includes. Return a params dict that can be passed to _do_mb_query. """ if isinstance(release_status, compat.basestring): release_status = [release_status] if isinstance(release_type, compat.basestring): release_type = [release_type] _check_filter(release_status, VALID_RELEASE_STATUSES) _check_filter(release_type, VALID_RELEASE_TYPES) if (release_status and "releases" not in includes and entity != "release"): raise InvalidFilterError("Can't have a status with no release include") if (release_type and "release-groups" not in includes and "releases" not in includes and entity not in ["release-group", "release"]): raise InvalidFilterError("Can't have a release type " "with no releases or release-groups involved") # Build parameters. params = {} if len(release_status): params["status"] = "|".join(release_status) if len(release_type): params["type"] = "|".join(release_type) return params def _docstring_get(entity): includes = list(VALID_INCLUDES.get(entity, [])) return _docstring_impl("includes", includes) def _docstring_browse(entity): includes = list(VALID_BROWSE_INCLUDES.get(entity, [])) return _docstring_impl("includes", includes) def _docstring_search(entity): search_fields = list(VALID_SEARCH_FIELDS.get(entity, [])) return _docstring_impl("fields", search_fields) def _docstring_impl(name, values): def _decorator(func): # puids are allowed so nothing breaks, but not documented if "puids" in values: values.remove("puids") 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" _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): """Set the hostname for MusicBrainz webservice requests. Defaults to 'musicbrainz.org'. You can also include a port: 'localhost:8000'.""" global hostname hostname = new_hostname # Rate limiting. limit_interval = 1.0 limit_requests = 1 do_rate_limit = True def set_rate_limit(limit_or_interval=1.0, new_requests=1): """Sets the rate limiting behavior of the module. Must be invoked before the first Web service call. If the `limit_or_interval` parameter is set to False then rate limiting will be disabled. If it is a number then only a set number of requests (`new_requests`) will be made per given interval (`limit_or_interval`). """ global limit_interval global limit_requests global do_rate_limit if isinstance(limit_or_interval, bool): do_rate_limit = limit_or_interval else: if limit_or_interval <= 0.0: raise ValueError("limit_or_interval can't be less than 0") if new_requests <= 0: raise ValueError("new_requests can't be less than 0") do_rate_limit = True limit_interval = limit_or_interval limit_requests = new_requests class _rate_limit(object): """A decorator that limits the rate at which the function may be called. The rate is controlled by the `limit_interval` and `limit_requests` global variables. The limiting is thread-safe; only one thread may be in the function at a time (acts like a monitor in this sense). The globals must be set before the first call to the limited function. """ def __init__(self, fun): self.fun = fun self.last_call = 0.0 self.lock = threading.Lock() self.remaining_requests = None # Set on first invocation. def _update_remaining(self): """Update remaining requests based on the elapsed time since they were last calculated. """ # On first invocation, we have the maximum number of requests # available. if self.remaining_requests is None: self.remaining_requests = float(limit_requests) else: since_last_call = time.time() - self.last_call self.remaining_requests += since_last_call * \ (limit_requests / limit_interval) self.remaining_requests = min(self.remaining_requests, float(limit_requests)) self.last_call = time.time() def __call__(self, *args, **kwargs): with self.lock: if do_rate_limit: self._update_remaining() # Delay if necessary. while self.remaining_requests < 0.999: time.sleep((1.0 - self.remaining_requests) * (limit_requests / limit_interval)) self._update_remaining() # Call the original function, "paying" for this call. self.remaining_requests -= 1.0 return self.fun(*args, **kwargs) # From pymb2 class _RedirectPasswordMgr(compat.HTTPPasswordMgr): def __init__(self): self._realms = { } def find_user_password(self, realm, uri): # ignoring the uri parameter intentionally try: return self._realms[realm] except KeyError: return (None, None) def add_password(self, realm, uri, username, password): # ignoring the uri parameter intentionally self._realms[realm] = (username, password) class _DigestAuthHandler(compat.HTTPDigestAuthHandler): def get_authorization (self, req, chal): qop = chal.get ('qop', None) if qop and ',' in qop and 'auth' in qop.split (','): chal['qop'] = 'auth' return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal) def _encode_utf8(self, msg): """The MusicBrainz server also accepts UTF-8 encoded passwords.""" encoding = sys.stdin.encoding or locale.getpreferredencoding() try: # This works on Python 2 (msg in bytes) msg = msg.decode(encoding) except AttributeError: # on Python 3 (msg is already in unicode) pass return msg.encode("utf-8") def get_algorithm_impls(self, algorithm): # algorithm should be case-insensitive according to RFC2617 algorithm = algorithm.upper() # lambdas assume digest modules are imported at the top level if algorithm == 'MD5': H = lambda x: hashlib.md5(self._encode_utf8(x)).hexdigest() elif algorithm == 'SHA': H = lambda x: hashlib.sha1(self._encode_utf8(x)).hexdigest() # XXX MD5-sess KD = lambda s, d: H("%s:%s" % (s, d)) return H, KD class _MusicbrainzHttpRequest(compat.Request): """ A custom request handler that allows DELETE and PUT""" def __init__(self, method, url, data=None): compat.Request.__init__(self, url, data) allowed_m = ["GET", "POST", "DELETE", "PUT"] if method not in allowed_m: raise ValueError("invalid method: %s" % method) self.method = method def get_method(self): return self.method # Core (internal) functions for calling the MB API. def _safe_read(opener, req, body=None, max_retries=8, retry_delay_delta=2.0): """Open an HTTP request with a given URL opener and (optionally) a request body. Transient errors lead to retries. Permanent errors and repeated errors are translated into a small set of handleable exceptions. Return a bytestring. """ last_exc = None for retry_num in range(max_retries): if retry_num: # Not the first try: delay an increasing amount. _log.info("retrying after delay (#%i)" % retry_num) time.sleep(retry_num * retry_delay_delta) try: if body: f = opener.open(req, body) else: f = opener.open(req) return f.read() except compat.HTTPError as exc: if exc.code in (400, 404, 411): # Bad request, not found, etc. raise ResponseError(cause=exc) elif exc.code in (503, 502, 500): # Rate limiting, internal overloading... _log.info("HTTP error %i" % exc.code) elif exc.code in (401, ): raise AuthenticationError(cause=exc) else: # Other, unknown error. Should handle more cases, but # retrying for now. _log.info("unknown HTTP error %i" % exc.code) last_exc = exc except compat.BadStatusLine as exc: _log.info("bad status line") last_exc = exc except compat.HTTPException as exc: _log.info("miscellaneous HTTP exception: %s" % str(exc)) last_exc = exc except compat.URLError as exc: if isinstance(exc.reason, socket.error): code = exc.reason.errno if code == 104: # "Connection reset by peer." continue raise NetworkError(cause=exc) except socket.timeout as exc: _log.info("socket timeout") last_exc = exc except socket.error as exc: if exc.errno == 104: continue raise NetworkError(cause=exc) except IOError as exc: raise NetworkError(cause=exc) # Out of retries! raise NetworkError("retried %i times" % max_retries, last_exc) # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7 # and ElementTree 1.3. if hasattr(etree, 'ParseError'): ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError) else: ETREE_EXCEPTIONS = (expat.ExpatError) # Parsing setup def mb_parser_null(resp): """Return the raw response (XML)""" return resp def mb_parser_xml(resp): """Return a Python dict representing the XML response""" # Parse the response. try: return mbxml.parse_message(resp) except UnicodeError as exc: raise ResponseError(cause=exc) except Exception as exc: if isinstance(exc, ETREE_EXCEPTIONS): raise ResponseError(cause=exc) else: raise # Defaults parser_fun = mb_parser_xml ws_format = "xml" def set_parser(new_parser_fun=None): """Sets the function used to parse the response from the MusicBrainz web service. If no parser is given, the parser is reset to the default parser :func:`mb_parser_xml`. """ global parser_fun if new_parser_fun is None: new_parser_fun = mb_parser_xml if not callable(new_parser_fun): raise ValueError("new_parser_fun must be callable") parser_fun = new_parser_fun def set_format(fmt="xml"): """Sets the format that should be returned by the Web Service. The server currently supports `xml` and `json`. This method will set a default parser for the specified format, but you can modify it with :func:`set_parser`. .. warning:: The json format used by the server is different from the json format returned by the `musicbrainzngs` internal parser when using the `xml` format! This format may change at any time. """ global ws_format if fmt == "xml": ws_format = fmt set_parser() # set to default elif fmt == "json": ws_format = fmt warn("The json format is non-official and may change at any time") set_parser(json.loads) else: raise ValueError("invalid format: %s" % fmt) @_rate_limit def _mb_request(path, method='GET', auth_required=AUTH_NO, client_required=False, args=None, data=None, body=None): """Makes a request for the specified `path` (endpoint) on /ws/2 on the globally-specified hostname. Parses the responses and returns the resulting object. `auth_required` and `client_required` control whether exceptions should be raised if the username/password and client are left unspecified, respectively. """ global parser_fun if args is None: args = {} else: args = dict(args) or {} if _useragent == "": raise UsageError("set a proper user-agent with " "set_useragent(\"application name\", \"application version\", \"contact info (preferably URL or email for your application)\")") if client_required: args["client"] = _client if ws_format != "xml": args["fmt"] = ws_format # Convert args from a dictionary to a list of tuples # so that the ordering of elements is stable for easy # testing (in this case we order alphabetically) # Encode Unicode arguments using UTF-8. newargs = [] for key, value in sorted(args.items()): if isinstance(value, compat.unicode): value = value.encode('utf8') newargs.append((key, value)) # Construct the full URL for the request, including hostname and # query string. url = compat.urlunparse(( 'http', hostname, '/ws/2/%s' % path, '', compat.urlencode(newargs), '' )) _log.debug("%s request for %s" % (method, url)) # Set up HTTP request handler and URL opener. httpHandler = compat.HTTPHandler(debuglevel=0) handlers = [httpHandler] # Add credentials if required. add_auth = False if auth_required == AUTH_YES: _log.debug("Auth required for %s" % url) if not user: raise UsageError("authorization required; " "use auth(user, pass) first") add_auth = True if auth_required == AUTH_IFSET and user: _log.debug("Using auth for %s because user and pass is set" % url) add_auth = True if add_auth: passwordMgr = _RedirectPasswordMgr() authHandler = _DigestAuthHandler(passwordMgr) authHandler.add_password("musicbrainz.org", (), user, password) handlers.append(authHandler) opener = compat.build_opener(*handlers) # Make request. req = _MusicbrainzHttpRequest(method, url, data) req.add_header('User-Agent', _useragent) _log.debug("requesting with UA %s" % _useragent) if body: req.add_header('Content-Type', 'application/xml; charset=UTF-8') elif not data and not req.has_header('Content-Length'): # Explicitly indicate zero content length if no request data # will be sent (avoids HTTP 411 error). req.add_header('Content-Length', '0') resp = _safe_read(opener, req, body) return parser_fun(resp) def _get_auth_type(entity, id, includes): """ Some calls require authentication. This returns True if a call does, False otherwise """ if "user-tags" in includes or "user-ratings" in includes: return AUTH_YES elif entity.startswith("collection"): if not id: return AUTH_YES else: return AUTH_IFSET else: return AUTH_NO def _do_mb_query(entity, id, includes=[], params={}): """Make a single GET call to the MusicBrainz XML API. `entity` is a string indicated the type of object to be retrieved. The id may be empty, in which case the query is a search. `includes` is a list of strings that must be valid includes for the entity type. `params` is a dictionary of additional parameters for the API call. The response is parsed and returned. """ # Build arguments. if not isinstance(includes, list): includes = [includes] _check_includes(entity, includes) auth_required = _get_auth_type(entity, id, includes) args = dict(params) if len(includes) > 0: inc = " ".join(includes) args["inc"] = inc # Build the endpoint components. path = '%s/%s' % (entity, id) return _mb_request(path, 'GET', auth_required, args=args) def _do_mb_search(entity, query='', fields={}, limit=None, offset=None, strict=False): """Perform a full-text search on the MusicBrainz search server. `query` is a lucene query string when no fields are set, but is escaped when any fields are given. `fields` is a dictionary of key/value query parameters. They keys in `fields` must be valid for the given entity type. """ # Encode the query terms as a Lucene query string. query_parts = [] if query: clean_query = util._unicode(query) if fields: clean_query = re.sub(LUCENE_SPECIAL, r'\\\1', clean_query) if strict: query_parts.append('"%s"' % clean_query) else: query_parts.append(clean_query.lower()) else: query_parts.append(clean_query) for key, value in fields.items(): # Ensure this is a valid search field. if key not in VALID_SEARCH_FIELDS[entity]: raise InvalidSearchFieldError( '%s is not a valid search field for %s' % (key, entity) ) elif key == "puid": warn("PUID support was removed from server\n" "the 'puid' field is ignored", Warning, stacklevel=2) # 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_echoprint(echoprint, includes=[], release_status=[], release_type=[]): """Search for recordings with an `echoprint `_. (not available on server)""" warn("Echoprints were never introduced\n" "and will not be found (404)", Warning, stacklevel=2) raise ResponseError(cause=compat.HTTPError( None, 404, "Not Found", None, None)) @_docstring_get("recording") def get_recordings_by_puid(puid, includes=[], release_status=[], release_type=[]): """Search for recordings with a :musicbrainz:`PUID`. (not available on server)""" warn("PUID support was removed from the server\n" "and no PUIDs will be found (404)", Warning, stacklevel=2) raise ResponseError(cause=compat.HTTPError( None, 404, "Not Found", None, None)) @_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_puids(recording_puids): """Submit PUIDs. (Functionality removed from server) """ warn("PUID support was dropped at the server\n" "nothing will be submitted", Warning, stacklevel=2) return {'message': {'text': 'OK'}} def submit_echoprints(recording_echoprints): """Submit echoprints. (Functionality removed from server) """ warn("Echoprints were never introduced\n" "nothing will be submitted", Warning, stacklevel=2) return {'message': {'text': 'OK'}} 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)) musicbrainzngs-0.6/PKG-INFO0000664000175000017500000000134312702713133017136 0ustar alastairalastair00000000000000Metadata-Version: 1.1 Name: musicbrainzngs Version: 0.6 Summary: Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices Home-page: https://python-musicbrainzngs.readthedocs.org/ Author: Alastair Porter Author-email: alastair@porter.net.nz License: BSD 2-clause Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Intended Audience :: Developers Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules