├── .gitignore ├── README ├── docs ├── Makefile ├── conf.py ├── examples.rst ├── index.rst ├── installation.rst ├── make.bat └── usage.rst ├── rdio ├── __init__.py └── rdio.py ├── readme.markdown ├── setup.py └── test └── rdio_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pyc 3 | *_exclude.* 4 | build/* 5 | dist/* 6 | *.egg-info/* 7 | docs/_build/* -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | An unofficial Python library for accessing the Rdio API, using OAuth. Inspired 2 | by Rdio's own 'rdio-python' library as well as python-twitter. 3 | 4 | To use, do: 5 | 6 | >>> import rdio 7 | >>> rdio_manager = rdio.Api(CONSUMER_KEY, CONSUMER_SECRET) 8 | >>> user = rdio_manager.find_user('benjaminkreeger@gmail.com') 9 | >>> print '%s %s's key is: %s.' % (user.first_name, user.last_name, user.key) -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-rdio.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-rdio.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-rdio" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-rdio" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-rdio documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Apr 27 14:04:17 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'python-rdio' 44 | copyright = u'2011, Benjamin Kreeger' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '1.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '1.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'python-rdiodoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'python-rdio.tex', u'python-rdio Documentation', 182 | u'Benjamin Kreeger', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'python-rdio', u'python-rdio Documentation', 215 | [u'Benjamin Kreeger'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | Below are some examples on how to get data from the Rdio API using the wrapper. For each example, assume that I've already got an instance of the manager class as such (fully authenticated). Also, note that each python prompt line will begin with ``>>>`` Continuations after line breaks begin with ``...``. 6 | 7 | >>> import rdio 8 | >>> api = rdio.Api('aosiudASUDH76ASD&8&SDasd', 'iuahd6542gSSA', 'UAHiduas7d6A%SD24dsaa', 'AJSADU36shj') 9 | 10 | Finding user 11 | ============ 12 | 13 | >>> user = api.find_user(vanity_name='ian') 14 | >>> print "%s, %s" % (user.name, user.key,) 15 | Ian McKellar, s13 16 | >>> user = api.find_user(email='benjaminkreeger@gmail.com') 17 | >>> print "%s, %s" % (user.name, user.key,) 18 | Benjamin Kreeger, s1250 19 | 20 | Authenticating user with OAuth pin 21 | ================================== 22 | 23 | First, you'll need to get the login url and token. 24 | 25 | >>> token_dict = api.get_token_and_login_url() 26 | >>> print 'Authorize this application at: %s?oauth_token=%s' % ( 27 | ... token_dict['login_url'], token_dict['oauth_token']) 28 | 29 | Then you'll need to launch the URL in a web browser, which will prompt the user to login and authorize the application. This'll give the user a PIN code. Have the user copy and paste the code as input to the next line. 30 | 31 | >>> oauth_verifier = raw_input('Enter the PIN / oAuth verifier: ').strip() 32 | 33 | Then feed it to the ``authorize_with_verifier`` method. 34 | 35 | >>> auth_dict = api.authorize_with_verifier(oauth_verifier) 36 | 37 | The ``auth_dict`` will contain the ``access_token_key`` and ``access_token_secret`` keys, which you'll undoubtedly want to keep in your datastore for the user's record. 38 | 39 | Getting current user 40 | ==================== 41 | 42 | >>> user = api.current_user() 43 | >>> print 'Name: %s, Key: %s' % (user.name, user.key,) 44 | Name: Benjamin Kreeger, Key: s1250 45 | 46 | The following examples assume you have the ``current_user``'s results stored in the ``user`` variable. 47 | 48 | Getting current user's heavy rotation 49 | ===================================== 50 | 51 | >>> rotation = api.get_heavy_rotation(user=user.key, friends=True) 52 | >>> for album in rotation[:3]: 53 | ... print "Album: %s, by %s" % (album.name, album.artist_name,) 54 | ... print "Key: %s" % album.key 55 | ... print "Users: %s" % ', '.join([user.name for user in album.users]) 56 | ... print "---" 57 | ... 58 | Album: TRON: Legacy Reconfigured, by Daft Punk 59 | Key: a687025 60 | Users: Ian McKellar, Michael Battey, Wilson Miner, Madelyn Taylor 61 | --- 62 | Album: Disreali Gears (Deluxe Edition), by Cream 63 | Key: a228508 64 | Users: Keith K 65 | --- 66 | Album: Hot Sauce Committee Part Two, by Beastie Boys 67 | Key: a711764 68 | Users: Frank Chimero, Madelyn Taylor, Tyler Abele, Nick Kreeger 69 | --- 70 | 71 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python-rdio documentation master file, created by 2 | sphinx-quickstart on Wed Apr 27 14:04:17 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ======================================= 7 | Welcome to python-rdio's documentation! 8 | ======================================= 9 | 10 | This document refers to version |release|. 11 | 12 | Contents 13 | ======== 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | :numbered: 18 | 19 | installation 20 | usage 21 | examples -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Requirements 6 | ============ 7 | 8 | * `Python 2.6`_ (all other version of Python are untested) 9 | * `python-oauth2`_ (the latest version) 10 | 11 | .. _Python 2.6: http://python.org/download/releases/ 12 | .. _python-oauth2: https://github.com/simplegeo/python-oauth2 13 | 14 | Using pip 15 | ========= 16 | 17 | If you're using ``pip``, the easiest way to get rolling is running this command at your terminal. 18 | 19 | :: 20 | 21 | $ pip install python-rdio 22 | 23 | This retrieves the latest version on from `PyPI`_. 24 | 25 | Using setuptools 26 | ================ 27 | 28 | If you prefer setuptools (``easy_install``), you can do the following at a terminal. 29 | 30 | :: 31 | 32 | $ easy_install python-rdio 33 | 34 | This retrieves the latest version on from `PyPI`_. 35 | 36 | .. _PyPI: http://pypi.python.org/pypi/python-rdio -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-rdio.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-rdio.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Getting the manager 6 | =================== 7 | 8 | This Python-based Rdio API wrapper (``python-rdio``) must be used by creating an instance of the manager class, known as ``Api``. Upon instance creation, you can either feed in just your OAuth ``consumer_key`` and ``consumer_secret``, or (if you've saved them for the user for which you're communicating with the API) your ``consumer_key``, ``consumer_secret``, ``access_key``, and ``access_secret``:: 9 | 10 | import rdio 11 | api = rdio.Api('aosiudASUDH76ASD&8&SDasd', 'iuahd6542gSSA', 'UAHiduas7d6A%SD24dsaa', 'AJSADU36shj') 12 | 13 | Making calls 14 | ============ 15 | 16 | Talking to the API manager merely involves calling its methods, and passing in parameters. Every method name on the `Rdio API`_ has been translated here from ``camelCase`` to ``underscored_lower_case``. Thus, ``getTracksForArtistInCollection`` has become ``get_tracks_for_artist_in_collection``. The same goes for parameters. To look at examples, visit :doc:`examples`. 17 | 18 | .. _Rdio API: http://developer.rdio.com/docs/read/rest/Methods -------------------------------------------------------------------------------- /rdio/__init__.py: -------------------------------------------------------------------------------- 1 | from rdio import Api -------------------------------------------------------------------------------- /rdio/rdio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # The MIT License 4 | # 5 | # Copyright 2011 Benjamin Kreeger 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | """A library that provides a Python interface to Rdio's API.""" 26 | 27 | __author__ = 'benjaminkreeger@gmail.com' 28 | __version__ = '0.4' 29 | 30 | from urlparse import parse_qsl 31 | import inspect 32 | import oauth2 as oauth 33 | import json 34 | import urllib 35 | import re 36 | from datetime import datetime, timedelta 37 | from dateutil import tz 38 | 39 | # Declare some constants and stuff 40 | 41 | ROOT_URL = 'http://api.rdio.com/1/' 42 | OAUTH_TOKEN_URL = 'http://api.rdio.com/oauth/request_token' 43 | OAUTH_ACCESS_URL = 'http://api.rdio.com/oauth/access_token' 44 | ROOT_SITE_URL = 'http://www.rdio.com' 45 | HTTP_METHOD = 'POST' 46 | rdio_types = { 47 | 'a': 'album', 48 | 'al': 'album in collection', 49 | 'r': 'artist', 50 | 'rl': 'artist in collection', 51 | 'rr': 'artist station', 52 | 'tr': 'artist top songs station', 53 | 'h': 'heavy rotation station', 54 | 'p': 'playlist', 55 | 't': 'track', 56 | 's': 'user', 57 | 'c': 'user collection station', 58 | 'e': 'user heavy rotation station', 59 | } 60 | rdio_genders = { 61 | 'm': ('male', 'his',), 62 | 'f': ('female', 'her',), 63 | } 64 | rdio_activity_types = { 65 | 0: ('track added to collection','%s added some music to %s collection.',), 66 | 1: ('track added to playlist','%s added some music to a playlist.',), 67 | 3: ('friend added','%s added a friend.',), 68 | 5: ('user joined','%s joined Rdio.',), 69 | 6: ('comment added to track','%s commented on a track.',), 70 | 7: ('comment added to album','%s commented on an album.',), 71 | 8: ('comment added to artist','%s commented on an artist.',), 72 | 9: ('comment added to playlist','%s commented on a playlist.',), 73 | 10: ('track added via match collection', 74 | '%s matched music to %s collection.',), 75 | 11: ('user subscribed to Rdio','%s subscribed to Rdio.',), 76 | 12: ('track synced to mobile','%s synced some music to %s mobile app.',), 77 | } 78 | methods = { 79 | 'add_friend': 'addFriend', 80 | 'add_to_collection': 'addToCollection', 81 | 'add_to_playlist': 'addToPlaylist', 82 | 'create_playlist': 'createPlaylist', 83 | 'current_user': 'currentUser', 84 | 'delete_playlist': 'deletePlaylist', 85 | 'find_user': 'findUser', 86 | 'get': 'get', 87 | 'get_activity_stream': 'getActivityStream', 88 | 'get_albums_for_artist': 'getAlbumsForArtist', 89 | 'get_albums_for_artist_in_collection': 'getAlbumsForArtistInCollection', 90 | 'get_albums_in_collection': 'getAlbumsInCollection', 91 | 'get_artists_in_collection': 'getArtistsInCollection', 92 | 'get_heavy_rotation': 'getHeavyRotation', 93 | 'get_new_releases': 'getNewReleases', 94 | 'get_object_from_short_code': 'getObjectFromShortCode', 95 | 'get_object_from_url': 'getObjectFromUrl', 96 | 'get_playback_token': 'getPlaybackToken', 97 | 'get_playlists': 'getPlaylists', 98 | 'get_top_charts': 'getTopCharts', 99 | 'get_tracks_for_album_in_collection': 'getTracksForAlbumInCollection', 100 | 'get_tracks_for_artist': 'getTracksForArtist', 101 | 'get_tracks_for_artist_in_collection': 'getTracksForArtistInCollection', 102 | 'get_tracks_in_collection': 'getTracksInCollection', 103 | 'remove_friend': 'removeFriend', 104 | 'remove_from_collection': 'removeFromCollection', 105 | 'remove_from_playlist': 'removeFromPlaylist', 106 | 'search': 'search', 107 | 'search_suggestions': 'searchSuggestions', 108 | 'set_playlist_collaborating': 'setPlaylistCollaborating', 109 | 'set_playlist_collaboration_mode': 'setPlaylistCollaborationMode', 110 | 'set_playlist_fields': 'setPlaylistFields', 111 | 'set_playlist_order': 'setPlaylistOrder', 112 | 'user_followers': 'userFollowers', 113 | 'user_following': 'userFollowing', 114 | } 115 | 116 | TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' 117 | UTC = tz.tzutc() 118 | 119 | # Define API error handling. 120 | class RdioGenericAPIError(Exception): 121 | """Handles all other unknown Rdio API errors.""" 122 | 123 | def __init__(self, method): 124 | super(RdioGenericAPIError, self).__init__() 125 | self.method = method 126 | print "An error occurred: %s." % ( 127 | self.method,) 128 | 129 | class RdioMissingArgumentError(Exception): 130 | """Handles exceptions around missing arguments.""" 131 | 132 | def __init__(self, argument, method): 133 | super(RdioMissingArgumentError, self).__init__() 134 | self.argument = argument 135 | self.method = method 136 | print "Method %s is missing required argument %s." % ( 137 | self.method, self.argument,) 138 | 139 | def __str__(self): 140 | return repr("Method %s is missing required argument %s." % ( 141 | self.method, self.argument,)) 142 | 143 | class RdioNotAuthenticatedException(Exception): 144 | """Handles exceptions around not being logged in.""" 145 | 146 | def __init__(self, method): 147 | super(RdioNotAuthenticatedException, self).__init__() 148 | self.method = method 149 | print "User is not authenticated. %s cannot be called." % ( 150 | self.method,) 151 | 152 | def __str__(self): 153 | return repr("User is not authenticated. %s cannot be called." % 154 | (self.method,)) 155 | 156 | class RdioInvalidParameterException(Exception): 157 | """Handles exceptions around invalid parameters being passed in.""" 158 | 159 | def __init__(self, value, param, method): 160 | super(RdioInvalidParameterException, self).__init__() 161 | self.value = value 162 | self.param = param 163 | self.method = method 164 | print "%s is an invalid parameter for %s in method %s." % ( 165 | self.value, self.param, self.method,) 166 | 167 | def __str__(self): 168 | return repr("%s is an invalid parameter for %s in method %s." % ( 169 | self.value, self.param, self.method,)) 170 | 171 | # Define objects. 172 | class JSONBasedObject(object): 173 | """Describeds a JSON based object (keeps data).""" 174 | 175 | def __init__(self, data): 176 | super(JSONBasedObject, self).__init__() 177 | self._data = data 178 | 179 | class RdioObject(JSONBasedObject): 180 | """Describes common fields a base Rdio object will have.""" 181 | 182 | def __init__(self, data): 183 | super(RdioObject, self).__init__(data) 184 | self.key = data['key'] 185 | self.url = data['url'] 186 | self.icon = data['icon'] 187 | self.base_icon = data['baseIcon'] 188 | self.rdio_type = rdio_types[data['type']] 189 | 190 | class RdioArtist(RdioObject): 191 | """Describes an Rdio artist.""" 192 | 193 | def __init__(self, data): 194 | super(RdioArtist, self).__init__(data) 195 | self.name = data['name'] 196 | self.track_count = data['length'] 197 | self.has_radio = data['hasRadio'] 198 | self.short_url = data['shortUrl'] 199 | self.album_count = -1 200 | self.hits = None 201 | self.user_count = None 202 | self.users = None 203 | self.top_songs_key = None 204 | self.collection_track_count = None 205 | self.radio_key = None 206 | if 'albumCount' in data: self.album_count = data['albumCount'] 207 | if 'hits' in data: self.hits = data['hits'] 208 | if 'user_count' in data: self.user_count = data['user_count'] 209 | if 'users' in data: self.users = parse_result_list(data['users']) 210 | if 'topSongsKey' in data: self.top_songs_key = data['topSongsKey'] 211 | if 'count' in data: self.collection_track_count = data['count'] 212 | if 'radioKey' in data: self.radio_key = data['radioKey'] 213 | 214 | class RdioMusicObject(RdioObject): 215 | """Describes an Rdio music object.""" 216 | 217 | def __init__(self, data): 218 | super(RdioMusicObject, self).__init__(data) 219 | self.name = data['name'] 220 | self.artist_name = data['artist'] 221 | self.artist_url = data['artistUrl'] 222 | self.artist_key = data['artistKey'] 223 | self.is_explicit = data['isExplicit'] 224 | self.is_clean = data['isClean'] 225 | self.price = data['price'] 226 | self.can_stream = data['canStream'] 227 | self.can_sample = data['canSample'] 228 | self.can_tether = data['canTether'] 229 | self.short_url = data['shortUrl'] 230 | self.embed_url = data['embedUrl'] 231 | self.duration = timedelta(seconds=data['duration']) 232 | self.big_icon = None 233 | if 'bigIcon' in data: self.big_icon = data['bigIcon'] 234 | 235 | class RdioAlbum(RdioMusicObject): 236 | """Describes an Rdio album.""" 237 | 238 | def __init__(self, data): 239 | super(RdioAlbum, self).__init__(data) 240 | self.release_date = data['displayDate'] 241 | self.track_keys = [] 242 | self.release_date_iso = None 243 | self.hits = None 244 | self.user_count = None 245 | self.users = None 246 | self.is_compilation = None 247 | if 'trackKeys' in data: self.track_keys = data['trackKeys'] 248 | if 'releaseDateISO' in data: 249 | self.release_date_iso = data['releaseDateISO'] 250 | if 'hits' in data: self.hits = data['hits'] 251 | if 'user_count' in data: self.user_count = data['user_count'] 252 | if 'users' in data: self.users = parse_result_list(data['users']) 253 | if 'isCompilation' in data: self.is_compilation = data['isCompilation'] 254 | 255 | class RdioTrack(RdioMusicObject): 256 | """Describes an Rdio track.""" 257 | 258 | def __init__(self, data): 259 | super(RdioTrack, self).__init__(data) 260 | self.album_name = data['album'] 261 | self.album_key = data['albumKey'] 262 | self.album_url = data['albumUrl'] 263 | self.album_artist_name = None 264 | self.album_artist_key = None 265 | if 'albumArtist' in data: self.album_artist_name = data['albumArtist'] 266 | if 'albumArtistKey' in data: 267 | self.album_artist_key = data['albumArtistKey'] 268 | self.can_download = data['canDownload'] 269 | self.can_download_album_only = data['canDownloadAlbumOnly'] 270 | self.play_count = -1 271 | self.track_number = -1 272 | self.is_on_compilation = None 273 | if 'trackNum' in data: self.track_number = data['trackNum'] 274 | if 'playCount' in data: self.play_count = data['playCount'] 275 | if 'isOnCompilation' in data: 276 | self.is_on_compilation = data['isOnCompilation'] 277 | 278 | class RdioPlaylist(RdioObject): 279 | """Describes an Rdio playlist.""" 280 | 281 | def __init__(self, data): 282 | super(RdioPlaylist, self).__init__(data) 283 | self.name = data['name'] 284 | self.track_count = data['length'] 285 | self.owner_name = data['owner'] 286 | self.owner_url = data['ownerUrl'] 287 | self.owner_key = data['ownerKey'] 288 | self.owner_icon = data['ownerIcon'] 289 | self.last_updated = datetime.fromtimestamp( 290 | int(data['lastUpdated'])) 291 | self.short_url = data['shortUrl'] 292 | self.embed_url = data['embedUrl'] 293 | self.track_keys = [] 294 | self.description = None 295 | if 'trackKeys' in data: self.track_keys = data['trackKeys'] 296 | if 'description' in data: self.description = data['description'] 297 | # process "tracks" if present 298 | if 'tracks' in data: 299 | self.tracks = [RdioTrack(x) for x in data['tracks']] 300 | # Populate track_keys 301 | self.track_keys = [x.key for x in self.tracks] 302 | 303 | class RdioUser(RdioObject): 304 | """Describes an Rdio user.""" 305 | 306 | def __init__(self, data): 307 | super(RdioUser, self).__init__(data) 308 | self.first_name = data['firstName'] 309 | self.last_name = data['lastName'] 310 | self.name = self.get_full_name() 311 | self.library_version = data['libraryVersion'] 312 | self.gender = rdio_genders[data['gender']][0] 313 | self.gender_posessive = rdio_genders[data['gender']][1] 314 | self.user_type = data['type'] 315 | self.username = None 316 | self.last_song_played = None 317 | self.display_name = None 318 | self.track_count = None 319 | self.last_song_play_time = None 320 | self.is_trial = None 321 | self.is_subscriber = None 322 | self.is_unlimited = None 323 | self.heavy_rotation_key = None 324 | self.network_heavy_rotation_key = None 325 | self.collection_key = None 326 | self.following_url = None 327 | self.collection_url = None 328 | self.playlists_url = None 329 | self.followers_url = None 330 | if 'username' in data: self.username = data['username'] 331 | if 'lastSongPlayed' in data: 332 | self.last_song_played = RdioTrack(data['lastSongPlayed']) 333 | if 'displayName' in data: self.display_name = data['displayName'] 334 | if 'trackCount' in data: self.track_count = data['trackCount'] 335 | if 'lastSongPlayTime' in data: 336 | self.last_song_play_time = datetime.strptime( 337 | data['lastSongPlayTime'], TIME_FORMAT).replace(tzinfo=UTC) 338 | if 'isTrial' in data: self.is_trial = data['isTrial'] 339 | if 'isSubscriber' in data: self.is_subscriber = data['isSubscriber'] 340 | if 'isUnlimited' in data: self.is_unlimited = data['isUnlimited'] 341 | if 'heavyRotationKey' in data: 342 | self.heavy_rotation_key = data['heavyRotationKey'] 343 | if 'networkHeavyRotationKey' in data: 344 | self.network_heavy_rotation_key = data['networkHeavyRotationKey'] 345 | if 'collectionKey' in data: self.collection_key = data['collectionKey'] 346 | if 'followingUrl' in data: self.following_url = data['followingUrl'] 347 | if 'collectionUrl' in data: self.collection_url = data['collectionUrl'] 348 | if 'playlistsUrl' in data: self.playlists_url = data['playlistsUrl'] 349 | if 'followersUrl' in data: self.followers_url = data['followersUrl'] 350 | 351 | def get_full_url(self): 352 | return ROOT_SITE_URL + self.url 353 | 354 | def get_full_name(self): 355 | return "%s %s" % (self.first_name, self.last_name,) 356 | 357 | class RdioSearchResult(JSONBasedObject): 358 | """Describes an Rdio search result and the extra fields it brings.""" 359 | 360 | def __init__(self, data): 361 | super(RdioSearchResult, self).__init__(data) 362 | self.album_count = data.get('album_count', 0) 363 | self.artist_count = data.get('artist_count', 0) 364 | self.number_results = data.get('number_results', 0) 365 | self.person_count = data.get('person_count', 0) 366 | self.playlist_count = data.get('playlist_count', 0) 367 | self.track_count = data.get('track_count', 0) 368 | self.results = parse_result_list(data.get('results', '')) 369 | 370 | class RdioActivityItem(JSONBasedObject): 371 | """Describes an item in Rdio's history object list.""" 372 | 373 | def __init__(self, data): 374 | super(RdioActivityItem, self).__init__(data) 375 | self.owner = RdioUser(data['owner']) 376 | self.date = datetime.strptime( 377 | data['date'], TIME_FORMAT).replace(tzinfo=UTC) 378 | self.update_type_id = data['update_type'] 379 | self.update_type = rdio_activity_types[data['update_type']][0] 380 | self._verbose_type = rdio_activity_types[data['update_type']][1] 381 | if self.update_type_id in (0,10,12,): 382 | self.verbose_update_type = self._verbose_type % ( 383 | self.owner.name, self.owner.gender_posessive,) 384 | else: self.verbose_update_type = self._verbose_type % self.owner.name 385 | self.albums = [] 386 | self.reviewed_item = None 387 | self.comment = '' 388 | # gotta be a better way of storing the main subject object 389 | self.subject = None 390 | if 'albums' in data: 391 | for album in data['albums']: 392 | self.albums.append(RdioAlbum(album)) 393 | self.subject = self.albums 394 | if 'reviewed_item' in data: 395 | self.reviewed_item = derive_rdio_type_from_data( 396 | data['reviewed_item']) 397 | self.subject = self.reviewed_item 398 | if 'comment' in data: 399 | self.comment = data['comment'] 400 | self.subject = self.comment 401 | 402 | class RdioActivityStream(JSONBasedObject): 403 | """Describes a stream of history for a user, for public, etc.""" 404 | 405 | def __init__(self, data): 406 | super(RdioActivityStream, self).__init__(data) 407 | self.last_id = data['last_id'] 408 | self.user = RdioUser(data['user']) # public? everyone? 409 | self.updates = [] 410 | if 'updates' in data: 411 | for update in data['updates']: 412 | self.updates.append(RdioActivityItem(update)) 413 | 414 | class RdioPlaylistSet(JSONBasedObject): 415 | """Describes a set of playlists, owned, collaborated, and subscribed.""" 416 | 417 | def __init__(self, data): 418 | super(RdioPlaylistSet, self).__init__(data) 419 | self.owned_playlists = parse_result_list(data['owned']) 420 | self.collaborated_playlists = parse_result_list(data['collab']) 421 | self.subscribed_playlists = parse_result_list(data['subscribed']) 422 | 423 | class RdioStation(RdioObject): 424 | """Describes basic fields for an Rdio Recommendation Station.""" 425 | 426 | def __init__(self, data): 427 | super(RdioStation, self).__init__(data) 428 | self.count = data['count'] 429 | self.length = data['length'] 430 | self.name = data['name'] 431 | self.reload_on_repeat = data['reloadOnRepeat'] 432 | self.tracks = data['tracks'] 433 | self.track_keys = None 434 | if 'trackKeys' in data: self.track_keys = data['trackKeys'] 435 | 436 | class RdioArtistStation(RdioStation): 437 | """Describes an artist recommendation station.""" 438 | 439 | def __init__(self, data): 440 | super(RdioArtistStation, self).__init__(data) 441 | self.artist_name = data['artistName'] 442 | self.artist_url = data['artistUrl'] 443 | self.has_radio = data['hasRadio'] 444 | self.short_url = data['shortUrl'] 445 | self.album_count = None 446 | self.top_songs_key = None 447 | self.radio_key = None 448 | if 'albumCount' in data: self.album_count = data['albumCount'] 449 | if 'topSongsKey' in data: self.top_songs_key = data['topSongsKey'] 450 | if 'radioKey' in data: self.radio_key = data['radioKey'] 451 | 452 | class RdioHeavyRotationStation(RdioStation): 453 | """Describes a user network (or global) heavy rotation station.""" 454 | 455 | def __init__(self, data): 456 | super(RdioHeavyRotationStation, self).__init__(data) 457 | self.user = data['user'] 458 | 459 | class RdioHeavyRotationUserStation(RdioStation): 460 | """Describes a user heavy rotation station.""" 461 | 462 | def __init__(self, data): 463 | super(RdioHeavyRotationUserStation, self).__init__(data) 464 | self.user = data['user'] 465 | 466 | class RdioArtistTopSongsStation(RdioStation): 467 | """Describes an artist station.""" 468 | 469 | def __init__(self, data): 470 | super(RdioArtistTopSongsStation, self).__init__(data) 471 | self.artist_name = data['artistName'] 472 | self.artist_url = data['artistUrl'] 473 | self.has_radio = data['hasRadio'] 474 | self.short_url = data['shortUrl'] 475 | self.album_count = None 476 | self.top_songs_key = None 477 | self.radio_key = None 478 | if 'albumCount' in data: self.album_count = data['albumCount'] 479 | if 'topSongsKey' in data: self.top_songs_key = data['topSongsKey'] 480 | if 'radioKey' in data: self.radio_key = data['radioKey'] 481 | 482 | class RdioUserCollectionStation(RdioStation): 483 | """Describes a user collection station.""" 484 | 485 | def __init__(self, data): 486 | super(RdioUserCollectionStation, self).__init__(data) 487 | self.user = data['user'] 488 | 489 | # Here's the big kahuna. 490 | class Api(object): 491 | """Handles communication with Rdio API.""" 492 | 493 | def __init__(self, 494 | consumer_key=None, 495 | consumer_secret=None, 496 | access_token_key=None, 497 | access_token_secret=None): 498 | """Instantiates a new Rdio API object. 499 | 500 | Keyword arguments: 501 | consumer_key -- The oAuth API key for the application. 502 | consumer_secret -- The oAuth API secret for the application. 503 | access_token_key -- The oAuth user's token key. 504 | access_token_secret -- The oAuth user's token secret. 505 | 506 | """ 507 | # The only thing to do right now is to set credentials. 508 | self.set_credentials(consumer_key=consumer_key, 509 | consumer_secret=consumer_secret, 510 | access_token_key=access_token_key, 511 | access_token_secret=access_token_secret) 512 | 513 | def set_credentials(self, consumer_key=None, consumer_secret=None, 514 | access_token_key=None, access_token_secret=None): 515 | """Sets the consumer_key and _secret for this instance. 516 | 517 | Keyword arguments: 518 | consumer_key -- The oAuth API key for the application. 519 | consumer_secret -- The oAuth API secret for the application. 520 | access_token_key -- The oAuth user's token key. 521 | access_token_secret -- The oAuth user's token secret. 522 | 523 | """ 524 | 525 | # Set our keys and secrets, depending on what was passed in. 526 | if consumer_key and consumer_secret: 527 | # Get our consumer object, which is just made of a key and secret 528 | self._oauth_consumer = oauth.Consumer(key=consumer_key, 529 | secret=consumer_secret) 530 | # Get our client object, which is simply a consumer (un-authed) 531 | self._oauth_client = oauth.Client(self._oauth_consumer) 532 | if access_token_key and access_token_secret: 533 | # Get our token object, which identifies us to the API for the user 534 | # Note: must check for access token when making authenticated calls 535 | self._oauth_access_token = oauth.Token(key=access_token_key, 536 | secret=access_token_secret) 537 | # Upgrade our client object to talk to the API on the user's behalf 538 | self._oauth_client = oauth.Client(self._oauth_consumer, 539 | self._oauth_access_token) 540 | 541 | def get_token_and_login_url(self, oauth_callback='oob'): 542 | """Gets the oAuth token via the oauth2 library. 543 | Keyword arguments: 544 | oauth_callback -- optional. If no oauth_callback is specified, we assume your application 545 | is a desktop application. 546 | """ 547 | #if oauth_callback is not defined, we're assuming you have a desktop application 548 | data = urllib.urlencode({'oauth_callback': oauth_callback}) 549 | try: 550 | # Get token and secret from Rdio's authorization endpoint. 551 | response, content = self._oauth_client.request(OAUTH_TOKEN_URL, 552 | HTTP_METHOD, data) 553 | # Make a dict out of it! Then, return dict. 554 | return dict(parse_qsl(content)) 555 | except: 556 | print "Something happened during %s." % inspect.stack()[0][3] 557 | pass 558 | 559 | def authorize_with_verifier(self, oauth_verifier, request_token): 560 | """Authorizes the oAuth handler with verifier and upgrades the token 561 | and client. Returns dictionary containing access key and secret if 562 | success; None if failure. 563 | 564 | Keyword arguments: 565 | oauth_verifier -- required. The PIN code from oAuth. 566 | request_token -- required. An object containing 'oauth_token' from the authorize/callback url 567 | and 'oauth_token_secret' from get_token_and_login_url() 568 | """ 569 | try: 570 | # If we don't have a request token yet, let the user know. 571 | if not request_token: 572 | raise RdioGenericAPIError("Must set token first.") 573 | 574 | self._oauth_request_token = oauth.Token(key=request_token['oauth_token'], 575 | secret=request_token['oauth_token_secret']) 576 | # Tell the token object to get verified. 577 | self._oauth_request_token.set_verifier(oauth_verifier) 578 | # Update our client object with our private token object. 579 | # Don't do this in our set_credentials function as it's just for 580 | # the request token, not the full access token. 581 | self._oauth_client = oauth.Client(self._oauth_consumer, 582 | self._oauth_request_token) 583 | # Get our full-blown, shiny new access token. 584 | response, content = self._oauth_client.request(OAUTH_ACCESS_URL, 585 | HTTP_METHOD) 586 | parsed_content = dict(parse_qsl(content)) 587 | token = parsed_content['oauth_token'] 588 | token_secret = parsed_content['oauth_token_secret'] 589 | # Send our token to our credential handler function. 590 | self.set_credentials(access_token_key=token, 591 | access_token_secret=token_secret) 592 | # If the private token was made, return True; else return False. 593 | if self._oauth_access_token: 594 | return { 595 | 'oauth_token': token, 596 | 'oauth_token_secret': token_secret} 597 | else: 598 | return None 599 | except RdioGenericAPIError as e: 600 | print "API error: %s." % e.msg 601 | 602 | def call_api_authenticated(self, data): 603 | """Handles checking authentication before talking to the Rdio API. 604 | 605 | Keyword arguments: 606 | data -- the dictionary of data for the call, including 'method' param. 607 | 608 | """ 609 | if not self._oauth_access_token: 610 | raise RdioNotAuthenticatedException(data['method']) 611 | else: return self.call_api(data) 612 | 613 | def call_api(self, data): 614 | """Calls the Rdio API. Responsible for handling errors from the API. 615 | 616 | Keyword arguments: 617 | data -- the dictionary of data for the call, including 'method' param. 618 | 619 | """ 620 | data = urllib.urlencode(data) 621 | response, content = self._oauth_client.request(ROOT_URL, 622 | HTTP_METHOD, data) 623 | parsed_content = json.loads(content) 624 | status = parsed_content['status'] 625 | if status == 'error': 626 | raise RdioGenericAPIError(parsed_content['message']) 627 | return None 628 | elif status == 'ok': 629 | return parsed_content['result'] 630 | 631 | def add_friend(self, user): 632 | """Add a friend to the current user. Returns True if the add succeeds, 633 | and False if it fails. Requires authentication. 634 | 635 | Keyword arguments: 636 | user -- the key of the user to add as a friend. 637 | 638 | """ 639 | data = {'method': methods['add_friend'], 'user': user} 640 | 641 | return self.call_api_authenticated(data) 642 | 643 | def add_to_collection(self, keys): 644 | """Adds tracks or playlists to the current user's collection. 645 | 646 | Keyword arguments: 647 | keys -- a list of tracks or playlists to add to the user's collection. 648 | 649 | """ 650 | data = {'method': methods['add_to_collection'], 'keys': ','.join(keys)} 651 | 652 | return self.call_api_authenticated(data) 653 | 654 | def add_to_playlist(self, playlist, tracks): 655 | """Add a track to a playlist. 656 | 657 | Keyword arguments: 658 | playlist -- key of the playlist to add to. 659 | tracks -- keys of tracks to add to the playlist. 660 | 661 | """ 662 | data = { 663 | 'method': methods['add_to_playlist'], 664 | 'playlist': playlist, 665 | 'tracks': ','.join(tracks)} 666 | print data 667 | return self.call_api_authenticated(data) 668 | 669 | def create_playlist(self, name, description, tracks, extras=[]): 670 | """Create a new playlist in the current user's collection. The new 671 | playlist will be returned if the creation is successful; otherwise null 672 | will be returned. 673 | 674 | Keyword arguments: 675 | name -- playlist name. 676 | description -- playlist description. 677 | tracks -- a list of initial tracks to start the playlist. 678 | extras -- optional. A list of additional fields to return. 679 | """ 680 | data = { 681 | 'method': methods['create_playlist'], 682 | 'name': name, 683 | 'description': description, 684 | 'tracks': ','.join(tracks)} 685 | 686 | if extras: data['extras'] = ','.join(extras) 687 | result = self.call_api_authenticated(data) 688 | 689 | return RdioPlaylist(result) if result else None 690 | 691 | def current_user(self, extras=[]): 692 | """Gets information about the currently logged in user. Requires 693 | authentication. 694 | 695 | Keyword arguments: 696 | extras -- a list of additional fields to return. 697 | 698 | """ 699 | data = {'method': methods['current_user']} 700 | 701 | if extras: data['extras'] = ','.join(extras) 702 | result = self.call_api_authenticated(data) 703 | return RdioUser(result) if result else None 704 | 705 | def delete_playlist(self, playlist): 706 | """Delete a playlist. 707 | 708 | Keyword arguments: 709 | playlist -- the key of the playlist to delete. 710 | 711 | """ 712 | data = {'method': methods['delete_playlist'], 'playlist': playlist} 713 | 714 | return self.call_api_authenticated(data) 715 | 716 | def find_user(self, email=None, vanity_name=None): 717 | """Finds an Rdio user by email or username. Exactly one of email or 718 | vanity_name must be supplied. 719 | 720 | Keyword arguments: 721 | email -- the desired user's email address. 722 | vanity_name -- the desired user's vanity name. 723 | 724 | """ 725 | data = {'method': methods['find_user']} 726 | 727 | if email: 728 | if validate_email(email): data['email'] = email 729 | else: raise RdioInvalidParameterException( 730 | "Invalid email address: %s." % email) 731 | if vanity_name: data['vanityName'] = vanity_name 732 | result = self.call_api(data) 733 | return RdioUser(result) if result else None 734 | 735 | def get(self, keys, extras=[]): 736 | """Fetch one or more objects from Rdio. 737 | 738 | Keyword arguments: 739 | keys -- a list of keys for the objects to fetch. 740 | extras -- optional. A list of additional fields to return. 741 | 742 | """ 743 | data = {'method': methods['get'], 'keys': ','.join(keys)} 744 | 745 | if extras: data['extras'] = ','.join(extras) 746 | results = self.call_api(data) 747 | return parse_result_dictionary(results) if results else None 748 | 749 | def get_activity_stream(self, user, scope, last_id=None): 750 | """Get the activity events for a user, a user's friends, or everyone 751 | on Rdio. 752 | 753 | Keyword arguments: 754 | user -- the key of the user to retrieve an activity stream for. 755 | scope -- the scope of the activity stream, either "user", "friends" 756 | or "everyone". 757 | last_id -- optional. the last_id returned by the last call to 758 | getActivityStream - only activity since that call will be 759 | returned. 760 | 761 | """ 762 | data = {'method': methods['get_activity_stream'], 'user': user} 763 | 764 | if scope: 765 | if scope in ('user','friends','everyone',): 766 | data['scope'] = scope 767 | else: raise RdioInvalidParameterException( 768 | scope, 'scope', 'get_activity_stream') 769 | else: raise RdioMissingArgumentError('scope','get_activity_stream') 770 | if last_id: data['last_id'] = last_id 771 | results = self.call_api(data) 772 | return RdioActivityStream(results) if results else None 773 | 774 | def get_albums_for_artist(self, artist, featuring=False, extras=[], 775 | start=None, count=None): 776 | """Returns the albums by (or featuring) an artist. 777 | 778 | Keyword arguments: 779 | artist -- the key of the artist to retrieve albums for. 780 | featuring -- optional. True returns albums the artist is featured on 781 | instead of albums by ther user. 782 | extras -- optional. A list of optional fields to return. 783 | start -- optional. The offset of the first result to return. 784 | count -- optional. The maximum number of results to return. 785 | 786 | """ 787 | data = {'method': methods['get_albums_for_artist'], 'artist': artist} 788 | 789 | if featuring: data['featuring'] = featuring 790 | if extras: data['extras'] = ','.join(extras) 791 | if start: data['start'] = start 792 | if count: data['count'] = count 793 | results = self.call_api(data) 794 | return parse_result_list(results) if results else None 795 | 796 | def get_albums_for_artist_in_collection(self, artist, user=None): 797 | """Returns the albums by an artist in a user's collection. 798 | 799 | Keyword arguments: 800 | artist -- the key of the artist to retrieve albums for. 801 | user -- optional. The owner of the collection to search. 802 | 803 | """ 804 | data = { 805 | 'method': methods['get_albums_for_artist_in_collection'], 806 | 'artist': artist} 807 | 808 | if user: data['user'] = user 809 | 810 | if user: results = self.call_api(data) 811 | else: results = self.call_api_authenticated(data) 812 | return parse_result_list(results) if results else None 813 | 814 | def get_albums_in_collection(self, user=None, start=None, count=None, 815 | sort=None, query=None): 816 | """Returns the albums in a user's collection. 817 | 818 | Keyword arguments: 819 | user -- optional. The owner of the collection to search. 820 | start -- optional. The offset of the first result to return. 821 | count -- optional. The maximum number of results to return. 822 | sort -- optional. Ways to sort the results. Valid options are 823 | 'dateAdded', 'playCount', 'artist', and 'name'. 824 | query -- optional. The query to filter albums with. 825 | 826 | """ 827 | data = {'method': methods['get_albums_in_collection']} 828 | 829 | if user: data['user'] = user 830 | if start: data['start'] = start 831 | if count: data['count'] = count 832 | if sort: 833 | if sort in ('dateAdded','playCount','artist','name',): 834 | data['sort'] = sort 835 | else: raise RdioInvalidParameterException( 836 | sort, 'sort', 'get_albums_in_collection') 837 | if query: data['query'] = query 838 | if user: results = self.call_api(data) 839 | else: results = self.call_api_authenticated(data) 840 | return parse_result_list(results) if results else None 841 | 842 | def get_artists_in_collection(self, user=None, start=None, count=None, 843 | sort=None, query=None): 844 | """Returns the albums in a user's collection. 845 | 846 | Keyword arguments: 847 | user -- optional. The owner of the collection to search. 848 | start -- optional. The offset of the first result to return. 849 | count -- optional. The maximum number of results to return. 850 | sort -- optional. Ways to sort the results. Valid option is 851 | 'name' only. 852 | query -- optional. The query to filter artists with. 853 | 854 | """ 855 | data = {'method': methods['get_artists_in_collection']} 856 | 857 | if user: data['user'] = user 858 | if start: data['start'] = start 859 | if count: data['count'] = count 860 | if sort: 861 | if sort in ('name',): 862 | data['sort'] = sort 863 | else: raise RdioInvalidParameterException( 864 | sort, 'sort', 'get_artists_in_collection') 865 | if query: data['query'] = query 866 | if user: results = self.call_api(data) 867 | else: results = self.call_api_authenticated(data) 868 | return parse_result_list(results) if results else None 869 | 870 | def get_heavy_rotation(self, user=None, object_type=None, friends=False, 871 | limit=None): 872 | """Finds the most popular artists or albums for a user, their friends, 873 | or the whole site. 874 | 875 | Keyword arguments: 876 | user -- optional. The user to get heavy rotation for, or if this 877 | is missing, everyone. 878 | object_type -- optional. Values are "artists" or "albums". 879 | friends -- optional. If True, gets the user's friend's heavy 880 | rotation instead of the user's. 881 | limit -- optional. The maximum number of results to return. 882 | 883 | """ 884 | data = {'method': methods['get_heavy_rotation']} 885 | 886 | if user: data['user'] = user 887 | if object_type: 888 | if object_type in ('artists','albums',): 889 | data['type'] = object_type 890 | else: raise RdioInvalidParameterException( 891 | object_type, 'type', 'get_heavy_rotation') 892 | if friends: data['friends'] = friends 893 | if limit: data['limit'] = limit 894 | results = self.call_api(data) 895 | return parse_result_list(results) if results else None 896 | 897 | def get_new_releases(self, time=None, start=None, count=False, 898 | extras=[]): 899 | """Returns new albums released across a timeframe. 900 | 901 | Keyword arguments: 902 | time -- optional. Timeframe, either 'thisweek', 'lastweek', or 903 | 'twoweeks'. 904 | start -- optional. The offset of the first result to return. 905 | count -- optional. The maximum number of results to return. 906 | extras -- optional. A list of additional fields to return. 907 | 908 | """ 909 | data = {'method': methods['get_new_releases']} 910 | 911 | if time: 912 | if time in ('thisweek','lastweek','twoweeks',): 913 | data['time'] = time 914 | else: raise RdioInvalidParameterException( 915 | time, 'time', 'get_new_releases') 916 | if start: data['start'] = start 917 | if count: data['count'] = count 918 | if extras: data['extras'] = ','.join(extras) 919 | results = self.call_api(data) 920 | return parse_result_list(results) if results else None 921 | 922 | def get_object_from_short_code(self, short_code): 923 | """Returns the object that the supplied Rdio short-code is a 924 | representation of, or None if the short-code is invalid. 925 | 926 | Keyword arguments: 927 | short_code -- the short-code (everything after http://rd.io/x/). 928 | 929 | """ 930 | data = { 931 | 'method': methods['get_object_from_short_code'], 932 | 'short_code': short_code} 933 | 934 | result = self.call_api_authenticated(data) 935 | return derive_rdio_type_from_data(result) if result else None 936 | 937 | def get_object_from_url(self, url): 938 | """Return the object that the supplied Rdio short-code is a 939 | representation of, or null if the short-code is invalid. 940 | 941 | Keyword arguments: 942 | url -- the path portion of the url, including first slash. 943 | 944 | """ 945 | data = {'method': methods['get_object_from_url'], 'url': url} 946 | result = self.call_api_authenticated(data) 947 | return derive_rdio_type_from_data(result) if result else None 948 | 949 | def get_playback_token(self, domain=None): 950 | """Get a playback token. If you are using this for web playback, you 951 | must supply a domain. 952 | 953 | Keyword arguments: 954 | domain -- optional. The domain in which the playback SWF will be 955 | embedded. 956 | 957 | """ 958 | data = {'method': methods['get_playback_token']} 959 | if domain: data['domain'] = domain 960 | result = self.call_api(data) 961 | return result if result else None 962 | 963 | def get_playlists(self, extras=[]): 964 | """Get the current user's playlists. 965 | 966 | Keyword arguments: 967 | extras -- optional. A list of additional fields to return. 968 | 969 | """ 970 | data = {'method': methods['get_playlists']} 971 | if extras: data['extras'] = ','.join(extras) 972 | 973 | results = self.call_api_authenticated(data) 974 | return RdioPlaylistSet(results) if results else None 975 | 976 | def get_top_charts(self, result_type, start=None, count=None, extras=[]): 977 | """Return the site-wide most popular items for a given type. 978 | 979 | Keyword arguments: 980 | result_type -- type to include in results, valid values are "Artist", 981 | "Album", "Track", and "Playlist". 982 | start -- optional. The offset of the first result to return. 983 | count -- optional. The maximum number of results to return. 984 | extras -- optional. A list of additional fields to return. 985 | 986 | """ 987 | data = {'method': methods['get_top_charts']} 988 | 989 | if result_type in ('Artist','Album','Track','Playlist',): 990 | data['type'] = result_type 991 | else: raise RdioInvalidParameterException( 992 | result_type, 'result_type', 'get_top_charts') 993 | if start: data['start'] = start 994 | if count: data['count'] = count 995 | if extras: data['extras'] = ','.join(extras) 996 | results = self.call_api(data) 997 | return parse_result_list(results) if results else None 998 | 999 | def get_tracks_for_album_in_collection(self, album, user=None, extras=[]): 1000 | """Which tracks on the given album are in the user's collection. 1001 | 1002 | Keyword arguments: 1003 | album -- the key of the album. 1004 | user -- optional. The user whose collection to examine. 1005 | extras -- optional. A list of additional fields to return. 1006 | 1007 | """ 1008 | data = { 1009 | 'method': methods['get_tracks_for_album_in_collection'], 1010 | 'album': album} 1011 | 1012 | if user: data['user'] = user 1013 | if extras: data['extras'] = ','.join(extras) 1014 | results = self.call_api(data) 1015 | return parse_result_list(results) if results else None 1016 | 1017 | def get_tracks_for_artist(self, artist, appears_on=None, extras=[], 1018 | start=None, count=None): 1019 | """Get all of the tracks by this artist. 1020 | 1021 | Keyword arguments: 1022 | artist -- the key of the artist. 1023 | appears_on -- optional. If true, returns tracks that the artist appears 1024 | on, rather than tracks credited to the artist. 1025 | extras -- optional. A list of additional fields to return. 1026 | start -- optional. The offset of the first result to return. 1027 | count -- optional. The maximum number of results to return. 1028 | 1029 | """ 1030 | data = {'method': methods['get_tracks_for_artist'], 'artist': artist} 1031 | 1032 | if appears_on: data['appears_on'] = appears_on 1033 | if extras: data['extras'] = ','.join(extras) 1034 | if start: data['start'] = start 1035 | if count: data['count'] = count 1036 | results = self.call_api(data) 1037 | return parse_result_list(results) if results else None 1038 | 1039 | def get_tracks_for_artist_in_collection(self, artist, user=None, 1040 | extras=[]): 1041 | """Which tracks from the given artist are in the user's collection. 1042 | 1043 | Keyword arguments: 1044 | artist -- the key of the artist. 1045 | user -- optional. The user whose collection to examine. 1046 | extras -- optional. A list of additional fields to return. 1047 | 1048 | """ 1049 | data = { 1050 | 'method': methods['get_tracks_for_artist_in_collection'], 1051 | 'artist': artist} 1052 | 1053 | if user: data['user'] = user 1054 | if extras: data['extras'] = ','.join(extras) 1055 | results = self.call_api(data) 1056 | return parse_result_list(results) if results else None 1057 | 1058 | def get_tracks_in_collection(self, user=None, start=None, count=None, 1059 | sort=None, query=None): 1060 | """Get all of the tracks in the user's collection. 1061 | 1062 | Keyword arguments: 1063 | user -- optional. The key of the collection user. 1064 | start -- optional. The offset of the first result to return. 1065 | count -- optional. The maximum number of resutls to return. 1066 | sort -- optional. Sort by. Valid values are "dateAdded", "playCount", 1067 | "artist", "album", and "name". 1068 | query -- optional. Filter collection tracks by this. 1069 | 1070 | """ 1071 | data = {'method': methods['get_tracks_in_collection']} 1072 | 1073 | if user: data['user'] = user 1074 | if start: data['start'] = start 1075 | if count: data['count'] = count 1076 | if sort: 1077 | if sort in ('dateAdded','playCount','artist','album','name',): 1078 | data['sort'] = sort 1079 | else: raise RdioInvalidParameterException( 1080 | sort, 'sort', 'get_tracks_in_collection') 1081 | if query: data['query'] = query 1082 | results = self.call_api(data) 1083 | return parse_result_list(results) if results else None 1084 | 1085 | def remove_friend(self, user): 1086 | """Remove a friend from the current user. 1087 | 1088 | Keyword arguments 1089 | user -- the key of the user to remove. 1090 | 1091 | """ 1092 | data = {'method': methods['remove_friend'], 'user': user} 1093 | 1094 | return self.call_api_authenticated(data) 1095 | 1096 | def remove_from_collection(self, keys): 1097 | """Remove tracks or playlists from the current user's collection. 1098 | 1099 | Keyword arguments: 1100 | keys -- the list of track or playlist keys to remove from the 1101 | collection. 1102 | 1103 | """ 1104 | data = { 1105 | 'method': methods['remove_from_collection'], 1106 | 'keys': ','.join(keys)} 1107 | 1108 | return self.call_api_authenticated(data) 1109 | 1110 | def remove_from_playlist(self, playlist, tracks, index=None, count=None): 1111 | """Remove an item from a playlist by its position in the playlist. 1112 | 1113 | Keyword arguments: 1114 | playlist -- the key of the playlist to modify. 1115 | index -- the index of the first item to remove. 1116 | count -- the number of tracks to remove from the playlist. 1117 | tracks -- the list of keys of the tracks to remove. 1118 | 1119 | """ 1120 | data = { 1121 | 'method': methods['remove_from_playlist'], 1122 | 'playlist': playlist, 1123 | 'index': index if index else 0, 1124 | 'count': count if count else len(tracks), 1125 | 'tracks': ','.join(tracks)} 1126 | 1127 | return self.call_api_authenticated(data) 1128 | 1129 | def search(self, query, types, never_or=None, extras=[], start=None, 1130 | count=None): 1131 | """Search for artists, albums, tracks, users, or all kinds of 1132 | objects. 1133 | 1134 | Keyword arguments: 1135 | query -- the search query. 1136 | types -- List of types to include in results. Valid values 1137 | are "Artist", "Album", "Track", "Playlist", and "User". 1138 | never_or -- optional. Disables Rdio's and/or query default "and". 1139 | extras -- optional. A list of additional fields to return. 1140 | start -- optional. The offset of the first result to return. 1141 | count -- optional. The maximum number of results to return. 1142 | 1143 | """ 1144 | data = { 1145 | 'method': methods['search'], 1146 | 'query': query, 1147 | 'types': ','.join(types)} 1148 | 1149 | if never_or: data['never_or'] = never_or 1150 | if extras: data['extras'] = ','.join(extras) 1151 | if start: data['start'] = start 1152 | if count: data['count'] = count 1153 | results = self.call_api(data) 1154 | return RdioSearchResult(results) if results else None 1155 | 1156 | def search_suggestions(self, query, extras=[]): 1157 | """Match the supplied prefix against artists, albums, tracks, and 1158 | people in the Rdio system. Returns the first 10 matches. 1159 | 1160 | Keyword arguments: 1161 | query -- the search prefix. 1162 | extras -- optional. A list of additional fields to return. 1163 | 1164 | """ 1165 | data = {'method': methods['search_suggestions'], 'query': query} 1166 | 1167 | if extras: data['extras'] = ','.join(extras) 1168 | results = self.call_api(data) 1169 | return parse_result_list(results) if results else None 1170 | 1171 | def set_playlist_collaborating(self, playlist, collaborating): 1172 | """Start or stop collaborating on a playlist. 1173 | 1174 | Keyword arguments: 1175 | playlist -- the key of the playlist. 1176 | collaborating -- true if you want to turn on collaboration. 1177 | 1178 | """ 1179 | 1180 | data = { 1181 | 'method': methods['set_playlist_collaborating'], 1182 | 'playlist': playlist, 1183 | 'collaborating': collaborating} 1184 | 1185 | return self.call_api_authenticated(data) 1186 | 1187 | def set_playlist_collaboration_mode(self, playlist, mode): 1188 | """Set the playlist collaboration mode to allow no collaboration (0), 1189 | all user collaboration (1), or collaboration with those followed by the 1190 | playlist owner (2). 1191 | 1192 | Keyword arguments: 1193 | playlist -- the key of the playlist to modify. 1194 | mode -- the new mode for the playlist (an integer). 1195 | 1196 | """ 1197 | 1198 | data = { 1199 | 'method': methods['set_playlist_collaboration_mode'], 1200 | 'playlist': playlist, 1201 | 'mode': mode} 1202 | 1203 | return self.call_api_authenticated(data) 1204 | 1205 | def set_playlist_fields(self, playlist, name, description): 1206 | """Sets the name and description for a playlist. 1207 | 1208 | Keyword arguments: 1209 | playlist -- the key of the playlist to modify. 1210 | name -- the new name of the playlist. 1211 | description -- the new description of the playlist. 1212 | 1213 | """ 1214 | 1215 | data = { 1216 | 'method': methods['set_playlist_fields'], 1217 | 'playlist': playlist, 1218 | 'name': name, 1219 | 'description': description} 1220 | 1221 | return self.call_api_authenticated(data) 1222 | 1223 | def set_playlist_order(self, playlist, tracks): 1224 | """Saves the given order of tracks in a given playlist. The new order 1225 | must have the same tracks as the previous order (this method may not 1226 | be used to add/remove tracks). 1227 | 1228 | Keyword arguments: 1229 | playlist -- the key of the playlist to reorder. 1230 | tracks -- a list of the tracks in their new order. 1231 | 1232 | """ 1233 | 1234 | data = { 1235 | 'method': methods['set_playlist_order'], 1236 | 'playlist': playlist, 1237 | 'tracks': ','.join(tracks)} 1238 | 1239 | return self.call_api_authenticated(data) 1240 | 1241 | def user_followers(self, user, start=None, count=None, extras=[]): 1242 | """Get a list of users following a user. 1243 | 1244 | Keyword arguments: 1245 | user -- the key of the user. 1246 | start -- optional. The offset of the first field to return. 1247 | count -- optional. The max number of results to return. 1248 | extras -- optional. A list of additional fields to return. 1249 | 1250 | """ 1251 | 1252 | data = {'method': methods['user_followers'], 'user': user} 1253 | if start: data['start'] = start 1254 | if count: data['count'] = count 1255 | if extras: data['extras'] = ','.join(extras) 1256 | 1257 | results = self.call_api(data) 1258 | return parse_result_list(results) if results else None 1259 | 1260 | def user_following(self, user, start=None, count=None, extras=[]): 1261 | """Get a list of users that a user follows. 1262 | 1263 | Keyword arguments: 1264 | user -- the key of the user. 1265 | start -- optional. The offset of the first field to return. 1266 | count -- optional. The max number of results to return. 1267 | extras -- optional. A list of additional fields to return. 1268 | 1269 | """ 1270 | 1271 | data = {'method': methods['user_following'], 'user': user} 1272 | if start: data['start'] = start 1273 | if count: data['count'] = count 1274 | if extras: data['extras'] = ','.join(extras) 1275 | 1276 | results = self.call_api(data) 1277 | return parse_result_list(results) if results else None 1278 | 1279 | 1280 | def derive_rdio_type_from_data(rdio_object): 1281 | if rdio_types[rdio_object['type']] == 'artist': 1282 | return RdioArtist(rdio_object) 1283 | if rdio_types[rdio_object['type']] == 'album': 1284 | return RdioAlbum(rdio_object) 1285 | if rdio_types[rdio_object['type']] == 'track': 1286 | return RdioTrack(rdio_object) 1287 | if rdio_types[rdio_object['type']] == 'playlist': 1288 | return RdioPlaylist(rdio_object) 1289 | if rdio_types[rdio_object['type']] == 'user': 1290 | return RdioUser(rdio_object) 1291 | 1292 | 1293 | def validate_email(email): 1294 | """Validates email address. Should work for now. 1295 | From http://goo.gl/EuVRg. 1296 | 1297 | """ 1298 | if len(email) > 7: 1299 | if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", email) != None: 1300 | return 1 1301 | return 0 1302 | 1303 | 1304 | def parse_result_dictionary(results): 1305 | """Takes a dictionary and returns a list of RdioObjects.""" 1306 | objects = [] 1307 | for rdio_object in results: 1308 | objects.append(derive_rdio_type_from_data(results[rdio_object])) 1309 | return objects 1310 | 1311 | 1312 | def parse_result_list(results): 1313 | """Takes a list and returns a list of RdioObjects.""" 1314 | objects = [] 1315 | for rdio_object in results: 1316 | objects.append(derive_rdio_type_from_data(rdio_object)) 1317 | return objects 1318 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # python-rdio 2 | 3 | ## About 4 | 5 | Another Python library for accessing the [Rdio API](http://developer.rdio.com/), using OAuth. Inspired by [Rdio's own 'rdio-python' library](http://github.com/rdio/rdio-python/) and [python-twitter](http://code.google.com/p/python-twitter/). This is more or less a test of my own abilities, as I've been writing [Django](http://djangoproject.com/) for a while, and I'd like to get into more Python-oriented development. 6 | 7 | Function names and objects will follow Rdio's API specs for [methods](http://developer.rdio.com/docs/read/rest/methods) and [object types](http://developer.rdio.com/docs/read/rest/types) as closely as possible, accounting for Python-ish function and property names (underscores and all that, instead of camelCase). 8 | 9 | ## Installation 10 | 11 | Don't sweat it. You can geek out and look at the source by cloning this git repo. But I doubt that's why you're here. Just do this! 12 | 13 | pip install python-rdio 14 | 15 | What's that? You're not using `pip`? Shame, shame. I **guess** you can do this, too. 16 | 17 | easy_install python-rdio 18 | 19 | Then, follow the usage example below. Please code responsibly. Everything is fully documented. That means you can use `__doc__` on each method. 20 | 21 | ## Requirements 22 | 23 | * [python 2.6 or higher](http://python.org/download/releases/) but not python 3.0 24 | * [oauth2](https://github.com/simplegeo/python-oauth2) 25 | * [dateutil](http://labix.org/python-dateutil) 26 | 27 | ## Usage 28 | 29 | ``` python 30 | # Setup the API manager. If you have an ACCESS_KEY and ACCESS_SECRET for a 31 | # particular user, you can pass that in as the third and forth arguments 32 | # to Api(). 33 | import rdio 34 | rdio_manager = rdio.Api(CONSUMER_KEY, CONSUMER_SECRET) 35 | user = rdio_manager.find_user('benjaminkreeger@gmail.com') 36 | print '%s %s's key is: %s.' % (user.first_name, user.last_name, user.key) 37 | 38 | # Set authorization: get authorization URL, then pass back the PIN. 39 | token_dict = rdio_manager.get_token_and_login_url() 40 | print 'Authorize this application at: %s?oauth_token=%s' % ( 41 | token_dict['login_url'], token_dict['oauth_token']) 42 | 43 | token_secret = token_dict['oauth_token_secret'] 44 | oauth_verifier = raw_input('Enter the PIN / oAuth verifier: ').strip() 45 | token = raw_input('Enter oauth_token parameter from URL: ').strip() 46 | request_token = {"oauth_token":token, oauth_token_secret":token_secret} 47 | authorization_dict = rdio_manager.authorize_with_verifier(oauth_verifier, request_token) 48 | 49 | # Get back key and secret. rdio_manager is now authorized 50 | # on the user's behalf. 51 | print 'Access token key: %s' % authorization_dict['oauth_token'] 52 | print 'Access token secret: %s' % authorization_dict['oauth_token_secret'] 53 | 54 | # Make an authorized call. 55 | current_user = rdio_manager.current_user() 56 | print 'The full name of the current user is %s.' % ( 57 | current_user.name,) 58 | 59 | # Have some fun. 60 | search_object = rdio_manager.search( 61 | query='Big Echo', 62 | types=['Albums',], 63 | extras=['trackKeys',]) 64 | album = search_object.results[0] 65 | print "Found album %s by %s." % (album.name, album.artist_name,) 66 | new_playlist = rdio_manager.create_playlist( 67 | name='Whoopie!', 68 | description='A test playlist for the Rdio API.', 69 | tracks=album.track_keys, 70 | extras=['trackKeys',]) 71 | print "Just made playlist %s with %i tracks at %s! Has tracks: " % ( 72 | new_playlist.name, 73 | new_playlist.track_count, 74 | new_playlist.short_url) 75 | tracks = rdio_manager.get(new_playlist.track_keys) 76 | for t in tracks: print "%s (Duration: %i seconds)" % (t.name, t.duration.seconds,) 77 | ``` 78 | 79 | ## Version history 80 | 81 | Because you all care. 82 | 83 | * **Version 0.6**: Corrected a regular expression that refused four-character TLDs in emails. 84 | * **Version 0.5**: Fixed some bugs around the 0.4 release, including making `user.last_song_played` an actual `RdioTrack`. Also made duration a `timedelta`. 85 | * **Version 0.4**: Added object types for Rdio stations as documented [here](http://goo.gl/ActAB). Also added user lists for heavy rotation results. 86 | * **Version 0.3**: All calls implemented; most of them are working properly. Some minor tweaking may need to be done here and there, but a majority of the work is done. Also, setuptools! 87 | * **Version 0.2**: Supports PIN authorization through Rdio's oAuth implementation. Added `current_user` call. 88 | * **Version 0.1**: Initial release. Includes data models, unauthenticated API call logic, and one call. 89 | 90 | ## Disclaimer 91 | 92 | I don't work for [Rdio](http://rdio.com/). I'm just a nerd who loves Rdio. If someone from Rdio has any sort of objections to this package, be it naming or whatever, please contact me at `benjaminkreeger [at] gmail [dot] com` and we can totally work it out. 93 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | setup( 9 | name='python-rdio', 10 | version='0.6', 11 | author='Benjamin Kreeger', 12 | author_email='benjaminkreeger@gmail.com', 13 | description='An unofficial wrapper for the Rdio API.', 14 | license='MIT', 15 | keywords='api rpc rdio music', 16 | platforms='any', 17 | url='http://github.com/kreeger/python-rdio', 18 | packages=find_packages(), 19 | long_description=read('README'), 20 | install_requires=['oauth2>=1.5.167', 'python-dateutil==2.0'], 21 | ) 22 | -------------------------------------------------------------------------------- /test/rdio_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path += ["../rdio"] 4 | from rdio import Api, validate_email 5 | 6 | 7 | class RdioTest(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.api = Api() 11 | 12 | def test_validate_email(self): 13 | self.assertEqual(validate_email('ben@kree.gr'), 1) 14 | self.assertEqual(validate_email('ben@kree.info'), 1) 15 | 16 | 17 | if __name__ == 'main': 18 | unittest.main() 19 | --------------------------------------------------------------------------------