musicbrainzngs-0.5/0000775000175000017500000000000012274730671016051 5ustar alastairalastair00000000000000musicbrainzngs-0.5/docs/0000775000175000017500000000000012274730671017001 5ustar alastairalastair00000000000000musicbrainzngs-0.5/docs/api.rst0000664000175000017500000001202712274714562020307 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_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_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_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 .. _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_artists .. autofunction:: search_labels .. autofunction:: search_recordings .. autofunction:: search_release_groups .. autofunction:: search_releases 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_labels .. 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.5/docs/usage.rst0000664000175000017500000001505212274715132020635 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 ------------ 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: 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 `_. Searching --------- When you don't know the MusicBrainz IDs yet, you have to start a search. Using :func:`musicbrainzngs.search_artist`:: 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. musicbrainzngs-0.5/docs/Makefile0000664000175000017500000001305712117613714020441 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.5/docs/conf.py0000664000175000017500000002124312274714562020303 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.5/docs/index.rst0000664000175000017500000000120112160273072020623 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.5/docs/make.bat0000664000175000017500000001177112041524063020401 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.5/docs/installation.rst0000664000175000017500000000155212274714562022240 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.5/test/0000775000175000017500000000000012274730671017030 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/test_requests.py0000664000175000017500000000507612160557403022316 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=True) 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.5/test/test_ratelimit.py0000664000175000017500000000710012041524063022415 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.5/test/test_mbxml_label.py0000664000175000017500000000256212274714652022725 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.5/test/test_mbxml_search.py0000664000175000017500000001042412274723414023103 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 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 testSearchWork(self): musicbrainzngs.search_works("Fountain City") self.assertEqual("http://musicbrainz.org/ws/2/work/?query=Fountain+City", 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 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()) class SearchArtistTest(unittest.TestCase): def testFields(self): fn = os.path.join(os.path.dirname(__file__), "data", "search-artist.xml") res = mbxml.parse_message(open(fn)) 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(os.path.dirname(__file__), "data", "search-release.xml") res = mbxml.parse_message(open(fn)) self.assertEqual(25, len(res["release-list"])) self.assertEqual(16739, res["release-count"]) one = res["release-list"][0] self.assertEqual("100", one["ext:score"]) class SearchReleaseGroupTest(unittest.TestCase): def testFields(self): fn = os.path.join(os.path.dirname(__file__), "data", "search-release-group.xml") res = mbxml.parse_message(open(fn)) 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(os.path.dirname(__file__), "data", "search-work.xml") res = mbxml.parse_message(open(fn)) 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(os.path.dirname(__file__), "data", "search-label.xml") res = mbxml.parse_message(open(fn)) 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(os.path.dirname(__file__), "data", "search-recording.xml") res = mbxml.parse_message(open(fn)) self.assertEqual(25, len(res["recording-list"])) self.assertEqual(1258, res["recording-count"]) one = res["recording-list"][0] self.assertEqual("100", one["ext:score"]) musicbrainzngs-0.5/test/_common.py0000664000175000017500000000355212274714652021037 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.response = response self.exception = exception def open(self, request, body=None): self.myurl = request.get_full_url() 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 """ fn = join(datadir, filename) res = musicbrainzngs.mbxml.parse_message(open(fn)) return res musicbrainzngs-0.5/test/__init__.py0000664000175000017500000000000012041524063021113 0ustar alastairalastair00000000000000musicbrainzngs-0.5/test/test_mbxml_artist.py0000664000175000017500000000234212274714652023150 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.5/test/test_mbxml_release_group.py0000664000175000017500000000222612274714652024477 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.5/test/test_mbxml.py0000664000175000017500000000102412041524063021541 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) musicbrainzngs-0.5/test/test_mbxml_release.py0000664000175000017500000001406512274714652023267 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]) 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) musicbrainzngs-0.5/test/test_mbxml_work.py0000664000175000017500000000265312274714652022631 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") musicbrainzngs-0.5/test/test_getentity.py0000664000175000017500000001213112274720334022446 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 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()) musicbrainzngs-0.5/test/data/0000775000175000017500000000000012274730671017741 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/search-work.xml0000664000175000017500000004041012041524063022673 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.5/test/data/release/0000775000175000017500000000000012274730671021361 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/release/a81f3c15-2f36-47c7-9b0f-f684a8b0530f-recordings.xml0000664000175000017500000000141212041524063031011 0ustar alastairalastair00000000000000 Bored BoredOfficialnormaleng1978GB11ABored Bored2BTime Warpmusicbrainzngs-0.5/test/data/release/b66ebe6d-a577-4af8-9a2e-a029b2147716-recordings.xml0000664000175000017500000000200412041524063031071 0ustar alastairalastair00000000000000 My Albumnormal111Some track18000022279000Another track2790003360000One more8000044Last track././@LongLink0000000000000000000000000000015000000000000011211 Lustar 00000000000000musicbrainzngs-0.5/test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.xmlmusicbrainzngs-0.5/test/data/release/fbe4490e-e366-4da2-a37a-82162d2f41a9-recordings+artist-credits.0000664000175000017500000002273012041524063033303 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.5/test/data/release/212895ca-ee36-439a-a824-d2620cd10461-recordings.xml0000664000175000017500000001200212274714652030642 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.5/test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.xmlmusicbrainzngs-0.5/test/data/release/833d4c3a-2635-4b7a-83c4-4e560588f23a-recordings+artist-credits.0000664000175000017500000002021012041524063033060 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.5/test/data/release/fe29e7f0-eb46-44ba-9348-694166f47885-recordings.xml0000664000175000017500000003112112274714652030724 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.5/test/data/work/0000775000175000017500000000000012274730671020723 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/work/80737426-8ef3-3a9c-a3a6-9507afb93e93-aliases.xml0000664000175000017500000000100412107170162027567 0ustar alastairalastair00000000000000Symphony No. 3 in E-flat major, Op. 55 "Eroica"Symphonie Nr. 3 Es-Dur, Op. 55 "Eroica"Symphony No. 3, Op. 55 "Eroica"musicbrainzngs-0.5/test/data/work/3d7c7cd2-da79-37f4-98b8-ccfb1a4ac6c4-aliases.xml0000664000175000017500000000305212107170162030225 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.5/test/data/label/0000775000175000017500000000000012274730671021020 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/label/022fe361-596c-43a0-8e22-bad712bb9548-aliases.xml0000664000175000017500000000106212107170162027637 0ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/label/e72fabf2-74a3-4444-a9a5-316296cbfc8d-aliases.xml0000664000175000017500000000101512107170162030070 0ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/search-artist.xml0000664000175000017500000001615212041524063023225 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.5/test/data/release-group/0000775000175000017500000000000012274730671022513 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/release-group/f52bc6a1-c848-49e6-85de-f8f53459a624.xml0000664000175000017500000000061312041524063030036 0ustar alastairalastair00000000000000 Super Meat Boy!2010-10-27AlbumSoundtrackmusicbrainzngs-0.5/test/data/artist/0000775000175000017500000000000012274730671021247 5ustar alastairalastair00000000000000musicbrainzngs-0.5/test/data/artist/2736bad5-6280-4c8f-92c8-27a5e63bbab2-aliases.xml0000664000175000017500000000043512107170162030240 0ustar alastairalastair00000000000000ErrorsErrorsGB2004musicbrainzngs-0.5/test/data/artist/0e43fe9d-c472-4b62-be9e-55f971a023e1-aliases.xml0000664000175000017500000000442612107170162030251 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.5/test/data/search-release.xml0000664000175000017500000006506512041524063023346 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.5/test/data/search-release-group.xml0000664000175000017500000004227512041524063024476 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.5/test/data/search-recording.xml0000664000175000017500000007515412041524063023702 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.5/test/data/search-label.xml0000664000175000017500000000075012041524063022773 0ustar alastairalastair00000000000000 musicbrainzngs-0.5/setup.py0000664000175000017500000000410712274714562017566 0ustar alastairalastair00000000000000#!/usr/bin/env python import sys 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 musicbrainz NGS webservice", 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 :: 3 - Alpha", "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.5/README.md0000664000175000017500000000402312274714562017330 0ustar alastairalastair00000000000000## Musicbrainz NGS bindings This library implements webservice bindings for the Musicbrainz NGS site, also known as /ws/2. For more information on the musicbrainz webservice see . ### Usage # 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 development server musicbrainzngs.set_hostname("echoprint.musicbrainz.org") See the query.py file for more examples. More documentation is available at https://python-musicbrainzngs.readthedocs.org ### Contribute 1. Fork the [repository](https://github.com/alastair/python-musicbrainzngs) on Github. 2. Make and test whatever changes you desire. 3. Signoff and commit your changes using `git commit -s`. 4. Send a pull request. ### Authors These bindings were written by [Alastair Porter](http://github.com/alastair). Contributions have been made by: * [Adrian Sampson](https://github.com/sampsyo) * [Galen Hazelwood](https://github.com/galenhz) * [Greg Ward](https://github.com/gward) * [Ian McEwen](https://github.com/ianmcorvidae) * [Johannes Dewender](https://github.com/JonnyJD) * [Michael Marineau](https://github.com/marineam) * [Patrick Speiser](https://github.com/doskir) * [Paul Bailey](https://github.com/paulbailey) * [Ryan Helinski](https://github.com/rlhelinski) * [Sam Doshi](https://github.com/samdoshi) * [Simon Chopin](https://github.com/laarmen) * [Thomas Vander Stichele](https://github.com/thomasvs) * [Wieland Hoffmann](https://github.com/mineo) ### License This library is released under the simplified BSD license except for the file `musicbrainzngs/compat.py` which is licensed under the ISC license. See COPYING for details. musicbrainzngs-0.5/COPYING0000664000175000017500000000410412160273072017072 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.5/query.py0000664000175000017500000000252712041524063017562 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_puids({"cb4d4d70-930c-4d1a-a157-776de18be66a":"e94757ff-2655-4690-b369-4012beba6114"}) #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}) #print m.get_recordings_by_echoprint("aryw4bx1187b98dde8") #m.submit_echoprints({"e97f805a-ab48-4c52-855e-07049142113d": "anechoprint1234567"}) if __name__ == "__main__": main() musicbrainzngs-0.5/CHANGES0000664000175000017500000000644112274730160017042 0ustar alastairalastair000000000000000.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.5/examples/0000775000175000017500000000000012274730671017667 5ustar alastairalastair00000000000000musicbrainzngs-0.5/examples/collection.py0000775000175000017500000001025512274715132022375 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 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']: print('{name} by {editor} ({mbid})'.format( name=collection['name'], editor=collection['editor'], mbid=collection['id'] )) def show_collection(collection_id): """Show the list of releases in a given collection. """ result = musicbrainzngs.get_releases_in_collection(collection_id, limit=25) collection = result['collection'] release_list = collection['release-list'] # release count is only available starting with musicbrainzngs 0.5 if "release-count" in collection: release_count = collection['release-count'] print('{} releases in {}:'.format(release_count, collection['name'])) else: print('Releases in {}:'.format(collection['name'])) 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") options, args = parser.parse_args() if not args: parser.error('no username specified') username = args.pop(0) # Input the password. password = getpass.getpass('Password for {}: '.format(username)) # Call musicbrainzngs.auth() before making any API calls that # require authentication. musicbrainzngs.auth(username, password) if args: # Actions for a specific collction. collection_id = args[0] if options.add: # Add a release to the collection. musicbrainzngs.add_releases_to_collection( collection_id, [options.add] ) elif options.remove: # Remove a release from the collection. musicbrainzngs.remove_releases_from_collection( collection_id, [options.remove] ) else: # Print out the collection's contents. print("") show_collection(collection_id) else: # Show all collections. print("") show_collections() musicbrainzngs-0.5/examples/releasesearch.py0000775000175000017500000000365012274714562023057 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.5/examples/find_disc.py0000775000175000017500000000407112274714562022171 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 ... """ 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'])) 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'])) 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.5/musicbrainzngs/0000775000175000017500000000000012274730671021107 5ustar alastairalastair00000000000000musicbrainzngs-0.5/musicbrainzngs/compat.py0000664000175000017500000000326412041524063022735 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.5/musicbrainzngs/__init__.py0000664000175000017500000000005112041524063023200 0ustar alastairalastair00000000000000from musicbrainzngs.musicbrainz import * musicbrainzngs-0.5/musicbrainzngs/util.py0000664000175000017500000000262412117613714022434 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.5/musicbrainzngs/mbxml.py0000664000175000017500000005472112274723034022604 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 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 ('subelement-key', ) then return a dict {'subelement-key': } instead """ 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): result[inner_result[0]] = 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, "label": parse_label, "place": parse_place, "release": parse_release, "release-group": parse_release_group, "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, "release-list": parse_release_list, "release-group-list": parse_release_group_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"] elements = ["name", "editor"] inner_els = {"release-list": parse_release_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_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 ('target-id', attributes['id']) else: return ('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 (key, [parse_relation(r) for r in rl]) def parse_relation(relation): result = {} attribs = ["type", "type-id"] elements = ["target", "direction", "begin", "end", "ended"] inner_els = {"area": parse_area, "artist": parse_artist, "label": parse_label, "place": parse_place, "recording": parse_recording, "release": parse_release, "release-group": parse_release_group, "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)) 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, "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): return [parse_medium(m) for m in ml] 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, "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_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} result.update(parse_attributes(attribs, work)) result.update(parse_elements(elements, inner_els, work)) 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} 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_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(artist2tags, recording2tags): 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, tags in recording2tags.items(): rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS) rec_xml.set("{%s}id" % NS, rec) taglist = ET.SubElement(rec_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 art_list = ET.SubElement(root, "{%s}artist-list" % NS) for art, tags in artist2tags.items(): art_xml = ET.SubElement(art_list, "{%s}artist" % NS) art_xml.set("{%s}id" % NS, art) taglist = ET.SubElement(art_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 return ET.tostring(root, "utf-8") def make_rating_request(artist2rating, recording2rating): 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, rating in recording2rating.items(): rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS) rec_xml.set("{%s}id" % NS, rec) rating_xml = ET.SubElement(rec_xml, "{%s}user-rating" % NS) rating_xml.text = str(rating) art_list = ET.SubElement(root, "{%s}artist-list" % NS) for art, rating in artist2rating.items(): art_xml = ET.SubElement(art_list, "{%s}artist" % NS) art_xml.set("{%s}id" % NS, art) rating_xml = ET.SubElement(art_xml, "{%s}user-rating" % NS) rating_xml.text = str(rating) 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.5/musicbrainzngs/musicbrainz.py0000664000175000017500000012072712274730221024007 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 xml.etree.ElementTree as etree from xml.parsers import expat from warnings import warn, simplefilter from musicbrainzngs import mbxml from musicbrainzngs import util from musicbrainzngs import compat _version = "0.5" _log = logging.getLogger("musicbrainzngs") # turn on DeprecationWarnings below simplefilter(action="once", category=DeprecationWarning) # Constants for validation. RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'url', 'work'] 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': [ ], 'label': [ "releases", # Subqueries "discids", "media", "aliases", "annotation" ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES, 'place' : ["aliases", "annotation"] + RELATION_INCLUDES + TAG_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" ] + RELATION_INCLUDES, 'release-group': [ "artists", "releases", "discids", "media", "artist-credits", "annotation", "aliases" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'work': [ "artists", # Subqueries "aliases", "annotation" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'url': RELATION_INCLUDES, 'discid': [ "artists", "labels", "recordings", "release-groups", "media", "artist-credits", "discids", "puids", "isrcs", "recording-level-rels", "work-level-rels" ] + RELATION_INCLUDES, 'isrc': ["artists", "releases", "puids", "isrcs"], 'iswc': ["artists"], 'collection': ['releases'], } VALID_BROWSE_INCLUDES = { 'releases': ["artist-credits", "labels", "recordings", "isrcs", "release-groups", "media", "discids"] + RELATION_INCLUDES, 'recordings': ["artist-credits", "isrcs"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'labels': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'artists': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'urls': RELATION_INCLUDES, 'release-groups': ["artist-credits"] + 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' ], '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' ], 'work': [ 'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag', 'type', 'wid', 'work', 'workaccent' ], } # 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(entity, browse=False): def _decorator(func): if browse: includes = list(VALID_BROWSE_INCLUDES.get(entity, [])) else: includes = list(VALID_INCLUDES.get(entity, [])) # puids are allowed so nothing breaks, but not documented if "puids" in includes: includes.remove("puids") includes = ", ".join(includes) if func.__doc__: search_fields = list(VALID_SEARCH_FIELDS.get(entity, [])) # puid is allowed so nothing breaks, but not documented if "puid" in search_fields: search_fields.remove("puid") func.__doc__ = func.__doc__.format(includes=includes, fields=", ".join(search_fields)) 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 base hostname for MusicBrainz webservice requests. Defaults to 'musicbrainz.org'.""" 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`. When you set the format to anything different from the default, you need to provide your own parser 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! """ global ws_format if fmt not in ["xml", "json"]: raise ValueError("invalid format: %s" % fmt) else: ws_format = fmt @_rate_limit def _mb_request(path, method='GET', auth_required=False, 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 client and username/password 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. if auth_required: _log.debug("Auth required for %s" % url) if not user: raise UsageError("authorization required; " "use auth(user, pass) first") 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 _is_auth_required(entity, 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 True elif entity.startswith("collection"): return True else: return False 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 = _is_auth_required(entity, 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(r'([+\-&|!(){}\[\]\^"~*?:\\])', 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", DeprecationWarning, stacklevel=2) # Escape Lucene's special characters. value = util._unicode(value) value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\\/])', 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', True, True) def _do_mb_put(path): """Send a PUT request for the specified object. """ return _mb_request(path, 'PUT', True, 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', True, True, body=body) # The main interface! # Single entity by ID @_docstring('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('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('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('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('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('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('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('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('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('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('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('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('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('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('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('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('release') def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True): """Search for releases with a :musicbrainz:`Disc ID`. 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`. 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`. The result is a dict with either a 'disc' , a 'cdstub' key or a 'release-list' (fuzzy match with TOC). A 'disc' has a 'release-list' and 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" return _do_mb_query("discid", id, includes, params) @_docstring('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)", DeprecationWarning, stacklevel=2) raise ResponseError(cause=compat.HTTPError( None, 404, "Not Found", None, None)) @_docstring('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)", DeprecationWarning, stacklevel=2) raise ResponseError(cause=compat.HTTPError( None, 404, "Not Found", None, None)) @_docstring('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('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, valid_includes, limit, offset, params, release_status=[], release_type=[]): _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('artists', browse=True) def browse_artists(recording=None, release=None, release_group=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}""" # optional parameter work? valid_includes = VALID_BROWSE_INCLUDES['artists'] params = {"recording": recording, "release": release, "release-group": release_group} return _browse_impl("artist", includes, valid_includes, limit, offset, params) @_docstring('labels', browse=True) 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}""" valid_includes = VALID_BROWSE_INCLUDES['labels'] params = {"release": release} return _browse_impl("label", includes, valid_includes, limit, offset, params) @_docstring('recordings', browse=True) 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}""" valid_includes = VALID_BROWSE_INCLUDES['recordings'] params = {"artist": artist, "release": release} return _browse_impl("recording", includes, valid_includes, limit, offset, params) @_docstring('releases', browse=True) 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 valid_includes = VALID_BROWSE_INCLUDES['releases'] params = {"artist": artist, "track_artist": track_artist, "label": label, "recording": recording, "release-group": release_group} return _browse_impl("release", includes, valid_includes, limit, offset, params, release_status, release_type) @_docstring('release-groups', browse=True) 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}""" valid_includes = VALID_BROWSE_INCLUDES['release-groups'] params = {"artist": artist, "release": release} return _browse_impl("release-group", includes, valid_includes, limit, offset, params, [], release_type) @_docstring('urls', browse=True) 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}""" # optional parameter work? valid_includes = VALID_BROWSE_INCLUDES['urls'] params = {"resource": resource} return _browse_impl("url", includes, valid_includes, limit, offset, params) # browse_work is defined in the docs but has no browse criteria # 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 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`. """ params = {} if limit: params["limit"] = limit if offset: params["offset"] = offset return _do_mb_query("collection", "%s/releases" % collection, [], params) # 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", DeprecationWarning, 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", DeprecationWarning, 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(artist_tags={}, recording_tags={}): """Submit user tags. Artist or recording parameters are of the form: {entity_id1: [tag1, ...], ...} """ query = mbxml.make_tag_request(artist_tags, recording_tags) return _do_mb_post("tag", query) def submit_ratings(artist_ratings={}, recording_ratings={}): """ Submit user ratings. Artist or recording parameters are of the form: {entity_id1: rating, ...} """ query = mbxml.make_rating_request(artist_ratings, recording_ratings) 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.5/PKG-INFO0000664000175000017500000000127012274730671017146 0ustar alastairalastair00000000000000Metadata-Version: 1.1 Name: musicbrainzngs Version: 0.5 Summary: python bindings for musicbrainz NGS webservice 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 :: 3 - Alpha 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