Skip to content
This repository
Browse code

Release v1.7

  • Loading branch information...
commit 3d62577148fd11add6ab96c2d8a1582a4cd4f64a 2 parents 7f6a5cc + 5979a9f
Stein Magnus Jodal authored

Showing 56 changed files with 2,207 additions and 2,366 deletions. Show diff stats Hide diff stats

  1. 4  .gitignore
  2. 24  Makefile
  3. 5  README.rst
  4. 15  docs/_templates/layout.html
  5. 19  docs/api/albumbrowse.rst
  6. 48  docs/api/artistbrowse.rst
  7. 33  docs/api/image.rst
  8. 73  docs/api/link.rst
  9. 5  docs/api/playlist.rst
  10. 18  docs/api/search.rst
  11. 35  docs/api/session.rst
  12. 48  docs/audiosink.rst
  13. 5  docs/authors.rst
  14. 120  docs/changes.rst
  15. 33  docs/conf.py
  16. 18  docs/development.rst
  17. 1  docs/index.rst
  18. 4  docs/licenses.rst
  19. 5  docs/managers/session.rst
  20. 162  examples/jukebox.py
  21. 23  pylintrc
  22. 18  setup.py
  23. 48  spotify/__init__.py
  24. 62  spotify/alsahelper.py
  25. 127  spotify/audiosink/__init__.py
  26. 26  spotify/audiosink/alsa.py
  27. 95  spotify/audiosink/gstreamer.py
  28. 28  spotify/audiosink/oss.py
  29. 27  spotify/audiosink/portaudio.py
  30. 177  spotify/manager/session.py
  31. 62  spotify/osshelper.py
  32. 51  src/albumbrowser.c
  33. 3  src/albumbrowser.h
  34. 152  src/artistbrowser.c
  35. 7  src/artistbrowser.h
  36. 2  src/image.c
  37. 4  src/link.c
  38. 2,178  src/mockmodule.c
  39. 13  src/playlist.c
  40. 20  src/search.c
  41. 165  src/session.c
  42. 3  src/session.h
  43. 2  src/toplistbrowser.c
  44. 2  tests/test_album.py
  45. 50  tests/test_albumbrowser.py
  46. 14  tests/test_artist.py
  47. 65  tests/test_artistbrowser.py
  48. 25  tests/test_containermanager.py
  49. 2  tests/test_globals.py
  50. 91  tests/test_link.py
  51. 127  tests/test_playlist.py
  52. 50  tests/test_playlistmanager.py
  53. 117  tests/test_search.py
  54. 50  tests/test_toplist.py
  55. 10  tests/test_track.py
  56. 2  tests/test_user.py
4  .gitignore
@@ -4,6 +4,9 @@
4 4
 *.swp
5 5
 *.wpr
6 6
 *.wpu
  7
+*~
  8
+\#*
  9
+.\#*
7 10
 .coverage
8 11
 .pc
9 12
 MANIFEST
@@ -14,3 +17,4 @@ dist
14 17
 nosetests.xml
15 18
 spotify_appkey.key
16 19
 tmp/
  20
+.DS_Store
24  Makefile
... ...
@@ -0,0 +1,24 @@
  1
+.PHONY: autotest build clean default install test
  2
+
  3
+BUILD_DIR="build/lib/"
  4
+
  5
+default: build
  6
+
  7
+clean:
  8
+	@rm -rf build/
  9
+
  10
+build: clean
  11
+	@python setup.py build --with-mock --build-lib ${BUILD_DIR}
  12
+
  13
+test: build
  14
+	@PYTHONPATH=${BUILD_DIR} nosetests
  15
+
  16
+autotest: build
  17
+	@which inotifywait || (echo "inotifywait not found"; exit 1)
  18
+	@while true; do \
  19
+	  clear; \
  20
+	  PYTHONPATH=${BUILD_DIR} nosetests; \
  21
+	  inotifywait -q -e create -e modify -e delete \
  22
+	    --exclude ".*\.(pyc|sw.)" \
  23
+	    -r spotify/ src/ tests/; \
  24
+	done
5  README.rst
Source Rendered
@@ -14,10 +14,7 @@ apply for, and receive an API key from Spotify.
14 14
 Project resources
15 15
 =================
16 16
 
17  
-- `Documentation for the latest release
18  
-  <http://pyspotify.mopidy.com/docs/master/>`_
19  
-- `Documentation for the development version
20  
-  <http://pyspotify.mopidy.com/docs/develop/>`_
  17
+- `Documentation <http://pyspotify.mopidy.com/>`_
21 18
 - `Source code <http://github.com/mopidy/pyspotify>`_
22 19
 - `Issue tracker <http://github.com/mopidy/pyspotify/issues>`_
23 20
 - IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
15  docs/_templates/layout.html
... ...
@@ -1,15 +0,0 @@
1  
-{% extends "!layout.html" %}
2  
-
3  
-{% block extrahead %}
4  
-{{ super() }}
5  
-<script type="text/javascript">
6  
-  var _gaq = _gaq || [];
7  
-  _gaq.push(['_setAccount', 'UA-15510432-2']);
8  
-  _gaq.push(['_trackPageview']);
9  
-  (function() {
10  
-    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
11  
-    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
12  
-    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
13  
-  })();
14  
-</script>
15  
-{% endblock %}
19  docs/api/albumbrowse.rst
Source Rendered
@@ -2,21 +2,24 @@ Album browsing
2 2
 **************
3 3
 .. currentmodule:: spotify
4 4
 
5  
-Album browsers are created by :meth:`Session.browse_album` object. They are
6  
-iterable objects.
7  
-
8  
-
9 5
 The :class:`AlbumBrowser` class
10 6
 ===============================
11 7
 
12  
-.. class:: AlbumBrowser
13  
-
14  
-    AlbumBrowser objects.
  8
+.. class:: AlbumBrowser(album[, callback[, userdata]])
15 9
 
16 10
     .. note:: A sequence of :class:`Track` objects.
17 11
 
  12
+    Browse an album, calling the callback when the browser's metadata is
  13
+    loaded.
  14
+
  15
+    :param album: a Spotify album (does not have to be loaded)
  16
+    :type album: :class:`Album`
  17
+    :param callback: a function with signature :
  18
+        ``(AlbumBrowser browser, Object userdata)``
  19
+    :param userdata: any object
  20
+
18 21
     .. method:: is_loaded
19 22
 
20 23
         :rtype:     :class:`int`
21  
-        :returns:   Wether this album browser has finished loading metadata.
  24
+        :returns:   wether this album browser has finished loading metadata.
22 25
 
48  docs/api/artistbrowse.rst
Source Rendered
@@ -2,21 +2,53 @@ Artist browsing
2 2
 ***************
3 3
 .. currentmodule:: spotify
4 4
 
5  
-Artist browsers are created by :meth:`Session.browse_artist` object. They are
6  
-iterable objects.
7  
-
8  
-
9 5
 The :class:`ArtistBrowser` class
10 6
 ================================
11 7
 
12  
-.. class:: ArtistBrowser
13  
-
14  
-    ArtistBrowser objects.
  8
+.. class:: ArtistBrowser(artist[, type[, callback[, userdata]]])
15 9
 
16 10
     .. note:: A sequence of :class:`Track` objects.
17 11
 
  12
+    Browse an artist, calling the callback when the browser's metadata is
  13
+    loaded.
  14
+
  15
+    :param artist: a Spotify artist (does not have to be loaded)
  16
+    :type artist: :class:`Artist`
  17
+    :param type:    this browser's type. One of:
  18
+
  19
+        * ``'full'`` (default):     all data will be fetched (deprecated in
  20
+          pyspotify 1.7 / libspotify 11)
  21
+        * ``'no_tracks'``:          no information about tracks
  22
+        * ``'no_albums'``:          no information about albums (implies ``'no_tracks'``)
  23
+
  24
+        The ``'no_tracks'`` and ``'no_albums'`` browser types also include a
  25
+        list of top tracks for this artist.
  26
+
  27
+    :param callback: a function with signature :
  28
+        ``(ArtistBrowser browser, Object userdata)``
  29
+    :param userdata: any object
  30
+
18 31
     .. method:: is_loaded
19 32
 
20 33
         :rtype:     :class:`int`
21  
-        :returns:   Wether this artist browser has finished loading metadata.
  34
+        :returns:   wether this artist browser has finished loading metadata.
  35
+
  36
+    .. method:: albums
  37
+
  38
+        :rtype:     list of :class:`Album`
  39
+        :returns:   the list of albums found while browsing
  40
+
  41
+    .. method:: similar_artists
  42
+
  43
+        :rtype:     list of :class:`Artist`
  44
+        :returns:   the list of similar artists found while browsing
  45
+
  46
+    .. method:: tracks
  47
+
  48
+        :rtype:     list of :class:`Track`
  49
+        :returns:   the list of tracks found while browsing
  50
+
  51
+    .. method:: tophit_tracks
22 52
 
  53
+        :rtype:     list of :class:`Track`
  54
+        :returns:   the list of top tracks for this artist found while browsing
33  docs/api/image.rst
Source Rendered
@@ -8,7 +8,34 @@ The :class:`Image` class
8 8
 ========================
9 9
 .. currentmodule:: spotify
10 10
 
11  
-.. autoclass:: Image
12  
-    :members:
13  
-    :undoc-members:
  11
+.. class:: Image
14 12
 
  13
+  Image objects
  14
+
  15
+  .. method:: add_load_callback()
  16
+
  17
+     Add a load callback
  18
+
  19
+  .. method:: data()
  20
+
  21
+     Get image data
  22
+
  23
+  .. method:: error()
  24
+
  25
+     Check if image retrieval returned an error code
  26
+
  27
+  .. method:: format()
  28
+
  29
+     Get image format (currently only JPEG)
  30
+
  31
+  .. method:: image_id()
  32
+
  33
+     Get image ID
  34
+
  35
+  .. method:: is_loaded()
  36
+
  37
+     True if this Image has been loaded by the client
  38
+
  39
+  .. method:: remove_load_callback()
  40
+
  41
+     Remove a load callback
73  docs/api/link.rst
Source Rendered
@@ -17,71 +17,100 @@ The :class:`Link` class
5  docs/api/playlist.rst
Source Rendered
@@ -150,6 +150,11 @@ objects.
150 150
         :rtype:     :class:`int`
151 151
         :returns:   The number of subscribers of this playlist
152 152
 
  153
+    .. method:: owner
  154
+
  155
+        :rtype:     :class:`spotify.User`
  156
+        :returns:   the owner of the playlist
  157
+
153 158
     .. method:: rename(name)
154 159
 
155 160
         :param name:    the new name
18  docs/api/search.rst
Source Rendered
@@ -39,6 +39,24 @@ The :class:`Results` class
39 39
         :rtype:     string
40 40
         :returns:   the query expression that generated these results.
41 41
 
  42
+    .. method:: total_albums
  43
+
  44
+        :rtype:     :class:`int`
  45
+        :returns:   the total number of albums available for this search query.
  46
+
  47
+        .. note:: If this value is larger than the interval specified at
  48
+            creation of the search object, more search results are available.
  49
+            To fetch these, create a new search object with a new interval.
  50
+
  51
+    .. method:: total_artists
  52
+
  53
+        :rtype:     :class:`int`
  54
+        :returns:   the total number of artists available for this search query.
  55
+
  56
+        .. note:: If this value is larger than the interval specified at
  57
+            creation of the search object, more search results are available.
  58
+            To fetch these, create a new search object with a new interval.
  59
+
42 60
     .. method:: total_tracks
43 61
 
44 62
         :rtype:     :class:`int`
35  docs/api/session.rst
Source Rendered
@@ -4,9 +4,9 @@ Session handling
4 4
 .. currentmodule:: spotify
5 5
 
6 6
 The session handling is usually done by inheriting the
7  
-:class:`spotify.managers.SpotifySessionManager` class from the :mod:`manager`
8  
-module.  Then the manager's :meth:`connect` method calls the
9  
-:func:`spotify.connect` function.
  7
+:class:`spotify.manager.SpotifySessionManager` class from the
  8
+:mod:`spotify.manager` module.  Then the manager's :meth:`connect` method calls
  9
+the :func:`spotify.connect` function.
10 10
 
11 11
 .. function:: connect(session_manager)
12 12
 
@@ -31,24 +31,32 @@ The :class:`Session` class
31 31
         Browse an album, calling the callback when the browser's metadata is
32 32
         loaded.
33 33
 
34  
-        :param album: A spotify album (does not have to be loaded)
  34
+        :param album: a Spotify album (does not have to be loaded)
35 35
         :type album: :class:`Album`
36  
-        :param callback: signature : ``(AlbumBrowser browser, Object userdata)``
  36
+        :param callback: a function with signature :
  37
+            ``(AlbumBrowser browser, Object userdata)``
37 38
         :param userdata: any object
38 39
         :returns: An :class:`AlbumBrowser` object containing the results
39 40
 
  41
+        .. deprecated:: 1.7
  42
+            Use :class:`AlbumBrowser` instead.
  43
+
40 44
 
41 45
     .. method:: browse_artist(artist, callback[, userdata])
42 46
 
43 47
         Browse an artist, calling the callback when the browser's metadata is
44 48
         loaded.
45 49
 
46  
-        :param artist: A spotify artist (does not have to be loaded)
  50
+        :param artist: a Spotify artist (does not have to be loaded)
47 51
         :type artist: :class:`Artist`
48  
-        :param callback: signature : ``(ArtistBrowser browser, Object userdata)``
  52
+        :param callback: a function with signature :
  53
+            ``(ArtistBrowser browser, Object userdata)``
49 54
         :param userdata: any object
50 55
         :returns: An :class:`ArtistBrowser` object containing the results.
51 56
 
  57
+        .. deprecated:: 1.7
  58
+            Use :class:`ArtistBrowser` instead.
  59
+
52 60
 
53 61
     .. method:: display_name()
54 62
 
@@ -57,6 +65,13 @@ The :class:`Session` class
57 65
 
58 66
         Raises :exc:`SpotifyError` if not logged in.
59 67
 
  68
+    .. method:: flush_caches
  69
+
  70
+        This will make libspotify write all data that is meant to be stored
  71
+        on disk to the disk immediately. libspotify does this periodically
  72
+        by itself and also on logout. So under normal conditions this
  73
+        should never need to be used.
  74
+
60 75
     .. method:: image_create(id)
61 76
 
62 77
         :param string id:   the id of the image to be fetched.
@@ -101,7 +116,7 @@ The :class:`Session` class
101 116
         <spotify.manager.SpotifySessionManager.notify_main_thread>` session
102 117
         callback.
103 118
 
104  
-    .. method:: search(query, callback[ ,track_offset=0, track_count=32, album_offset=0, album_count=32, artist_offset=0, artist_count=32, userdata=None])
  119
+    .. method:: search(query, callback[ ,track_offset=0, track_count=32, album_offset=0, album_count=32, artist_offset=0, artist_count=32, playlist_offset=0, playlist_count=32, search_type='standard', userdata=None])
105 120
 
106 121
         :param query:           Query search string
107 122
         :param callback:        signature ``(Results results, Object userdata)``
@@ -111,6 +126,9 @@ The :class:`Session` class
111 126
         :param album_count:     The number of albums to ask for
112 127
         :param artist_offset:   The offset among the artists of the result
113 128
         :param artist_count:    The number of artists to ask for
  129
+        :param playlist_offset: The offset among the playlists of the result
  130
+        :param playlist_count:  The number of playlists to ask for
  131
+        :param search_type:     'standard' or 'suggest'
114 132
 
115 133
         :returns:               The search results
116 134
         :rtype:                 :class:`Results`
@@ -149,4 +167,3 @@ The :class:`Session` class
149 167
         user.
150 168
 
151 169
         If the user is not logged in, this method raises a :exc:`SpotifyError`.
152  
-
48  docs/audiosink.rst
Source Rendered
... ...
@@ -0,0 +1,48 @@
  1
+Audio sinks
  2
+***********
  3
+
  4
+.. automodule:: spotify.audiosink
  5
+    :members:
  6
+    :member-order: bysource
  7
+
  8
+Implementations
  9
+===============
  10
+
  11
+Implementations of the :class:`BaseAudioSink` interface include:
  12
+
  13
+
  14
+.. module:: spotify.audiosink.alsa
  15
+
  16
+.. class:: AlsaSink
  17
+
  18
+    Requires a system using ALSA, which includes most Linux systems, and the
  19
+    `pyalsaaudio <http://pyalsaaudio.sourceforge.net/>`_ library.
  20
+
  21
+
  22
+.. module:: spotify.audiosink.oss
  23
+
  24
+.. class:: OssSink
  25
+
  26
+    Requires a system using OSS or with an OSS emulation, typically a Linux or
  27
+    BSD system. Uses the ``ossaudiodev`` module from the Python standard
  28
+    library.
  29
+
  30
+
  31
+.. module:: spotify.audiosink.portaudio
  32
+
  33
+.. class:: PortAudioSink
  34
+
  35
+    Requires a system with the `PortAudio <http://www.portaudio.com/>`_ library
  36
+    installed and the Python binding `pyaudio
  37
+    <http://people.csail.mit.edu/hubert/pyaudio/>`_. The PortAudio library is
  38
+    available for both Linux, Mac OS X, and Windows.
  39
+
  40
+
  41
+.. module:: spotify.audiosink.gstreamer
  42
+
  43
+.. class:: GstreamerSink
  44
+
  45
+    Requires a system with `Gstreamer <http://gstreamer.freedesktop.org/>`_
  46
+    installed and the Python bindings gst-python. The Gstreamer library is
  47
+    available for both Linux, Mac OS X, and Windows. Though, it isn't always
  48
+    trivial to install Gstreamer.
5  docs/authors.rst
Source Rendered
@@ -12,5 +12,8 @@ Contributors to pyspotify in the order of appearance:
12 12
 - Antoine Pierlot-Garcin <antoine@bokbox.com>
13 13
 - Jamie Kirkpatrick <jkp@kirkconsulting.co.uk>
14 14
 - Francisco Jordano <arcturus@ardeenelinfierno.com>
15  
-- triptec
  15
+- Andreas Franzén <andreas.franzen@osynlig.se>
16 16
 - Benjamin Chapus <xben@free.fr>
  17
+- Tommaso Barbugli <tbarbugli@gmail.com>
  18
+- Bjørn Schjerve <bischjer@gmail.com>
  19
+- David Buchmann <david.buchmann@gmail.com>
120  docs/changes.rst
Source Rendered
@@ -2,6 +2,117 @@
2 2
 Changes
3 3
 =======
4 4
 
  5
+.. currentmodule:: spotify
  6
+
  7
+v1.7 (2012-04-22)
  8
+=================
  9
+
  10
+**API changes**
  11
+
  12
+- This version works with *libspotify* version 11.
  13
+
  14
+- Artist and album browsers are now created directly from the
  15
+  :class:`ArtistBrowser` and :class:`AlbumBrowser` class constructors. The
  16
+  :meth:`Session.browse_artist` and :meth:`Session.browse_album` methods still
  17
+  work but have been deprecated. Also, callbacks are optional for the two
  18
+  browsers.
  19
+
  20
+- The audio sink wrappers have been cleaned up and moved to a new
  21
+  :mod:`spotify.audiosink` module. The interface is the same, but you'll need
  22
+  to update your imports if you previously used either
  23
+  :class:`spotify.alsahelper.AlsaController` (renamed to
  24
+  :class:`spotify.audiosink.alsa.AlsaSink`) or
  25
+  :class:`spotify.osshelper.OssController` (renamed to
  26
+  :class:`spotify.audiosink.oss.OssSink`).
  27
+
  28
+- An :class:`ArtistBrowser` object is now a list of :class:`Track`, as it was
  29
+  written in the API documentation.
  30
+
  31
+- ``offset`` is now optional in :meth:`Link.from_track`.
  32
+
  33
+- Remove undocumented/internal method
  34
+  :meth:`spotify.manager.SpotifySessionManager.wake`.
  35
+  :meth:`spotify.manager.SpotifySessionManager.notify_main_thread` does the
  36
+  same job. Make sure you haven't accidentally overrided :meth:`notify_main_thread`
  37
+  in your :class:`SpotifySessionManager` subclass.
  38
+
  39
+- Remove undocumented/internal method
  40
+  :meth:`spotify.manager.SpotifySessionManager.terminate`. Use
  41
+  :meth:`spotify.manager.SpotifySessionManager.disconnect` instead.
  42
+
  43
+**New features**
  44
+
  45
+- Added method :meth:`spotify.Playlist.owner`.
  46
+
  47
+- Added methods :meth:`spotify.Results.total_albums` and
  48
+  :meth:`spotify.Results.total_artists`.
  49
+
  50
+- Added methods :meth:`spotify.ArtistBrowser.albums`,
  51
+  :meth:`spotify.ArtistBrowser.similar_artists`,
  52
+  :meth:`spotify.ArtistBrowser.tracks` and
  53
+  :meth:`spotify.ArtistBrowser.tophit_tracks`.
  54
+
  55
+- Added optional argument ``type`` for :class:`spotify.ArtistBrowser`.
  56
+
  57
+- pyspotify now registers a "null handler" for logging to the ``spotify``
  58
+  logger. This means that any pyspotify code is free to log debug log to any
  59
+  logger matching ``spotify.*``.
  60
+
  61
+  By default the log statements will be swallowed by the null handler. An
  62
+  application developer using pyspotify may add an additional log handler which
  63
+  listens for log messages to the ``spotify`` logger, and thus get debug
  64
+  information from pyspotify.
  65
+
  66
+- Multi-user credential retainment using ``login_blob`` from the
  67
+  :class:`spotify.manager.SpotifySessionManager` and the
  68
+  :meth:`spotify.manager.SpotifySessionManager.credentials_blob_updated` method.
  69
+
  70
+- Added a ``search_type`` argument for searches.
  71
+
  72
+- Added new method :meth:`spotify.Session.flush_caches`.
  73
+
  74
+- Add new :meth:`spotify.manager.SpotifySessionManager.music_delivery_safe`
  75
+  callback that can safely use the Spotify API without segfaulting. A little
  76
+  overhead is caused by serializing and passing data to the main thread, so if
  77
+  you are not going to use the Spotify API from your callbacks, or you're doing
  78
+  your own synchronization, you can continue to use the non-safe methods with a
  79
+  bit less overhead.
  80
+
  81
+- Bundled audio sink support:
  82
+
  83
+  - A audio sink wrapper for `PortAudio
  84
+    <http://www.portaudio.com/>`_,
  85
+    :class:`spotify.audiosink.portaudio.PortAudioSink`, have been contributed
  86
+    by Tommaso Barbugli.  PortAudio is available on both Linux, Mac OS X, and
  87
+    Windows.
  88
+
  89
+  - A audio sink wrapper for `Gstreamer <http://gstreamer.freedesktop.org/>`_,
  90
+    :class:`spotify.audiosink.gstreamer.GstreamerSink`, have been contributed
  91
+    by David Buchmann. Gstreamer is available on both Linux, Mac OS X, and
  92
+    Windows.
  93
+
  94
+  - The audio sink selector code originally written by Tommaso Barbugli for the
  95
+    ``jukebox.py`` example app have been generalized and made available for
  96
+    other applications as :func:`spotify.audiosink.import_audio_sink`.
  97
+
  98
+- Jukebox example:
  99
+
  100
+  - The jukebox got support for playing entire playlists. Thanks to Bjørn
  101
+    Schjerve.
  102
+
  103
+  - The jukebox now formats duration in minutes and seconds. Thanks to David
  104
+    Buchmann.
  105
+
  106
+**Other changes**
  107
+
  108
+- For developers: *pyspotify* now uses `libmockspotify
  109
+  <https://github.com/mopidy/libmockspotify>`_ for its mocking needs. The
  110
+  mock module only contains Python bindings to the *libmockspotify* API. To be
  111
+  able to run the tests, you need to pass ``--with-mock`` to your ``python
  112
+  setup.py ...`` command to build pyspotify with mock support. Alternatively,
  113
+  you can use ``make test`` to run the tests.
  114
+
  115
+
5 116
 v1.6.1 (2011-12-29)
6 117
 ===================
7 118
 
@@ -79,9 +190,10 @@ with libspotify v0.0.8.
79 190
 - Add new method: :meth:`spotify.Playlist.rename`
80 191
 - Add new method: :meth:`spotify.Session.get_friends`. Contributed by Francisco
81 192
   Jordano.
82  
-- Add new method: :meth:`spotify.Playlist.add_tracks`. Contributed by triptec.
  193
+- Add new method: :meth:`spotify.Playlist.add_tracks`. Contributed by Andreas
  194
+  Franzén.
83 195
 - Add new method: :meth:`spotify.PlaylistContainer.add_new_playlist`.
84  
-  Contributed by triptec.
  196
+  Contributed by Andreas Franzén.
85 197
 
86 198
 **Bug fixes**
87 199
 
@@ -92,8 +204,8 @@ with libspotify v0.0.8.
92 204
 - Argument errors were unchecked in :meth:`spotify.Session.search`
93 205
 - Fix crash on valid error at image creation. Fixed by Jamie Kirkpatrick.
94 206
 - Keep compatibility with Python 2.5. Contributed by Jamie Kirkpatrick.
95  
-- Callbacks given at artist/album browser creation are now called by pyspotify
96  
-  (jkp)
  207
+- Callbacks given at artist/album browser creation are now called by pyspotify.
  208
+  Fixed by Jamie Kirkpatrick.
97 209
 - Fix exception when a ``long`` was returned from
98 210
   :meth:`spotify.manager.SpotifySessionManager.music_delivery`
99 211
 
33  docs/conf.py
@@ -13,18 +13,10 @@
13 13
 
14 14
 import sys, os, re
15 15
 
16  
-import distutils.command.build
17  
-from distutils.dist import Distribution
18  
-
19  
-b = distutils.command.build.build(Distribution())
20  
-b.initialize_options()
21  
-b.finalize_options()
22  
-
23 16
 # If extensions (or modules to document with autodoc) are in another directory,
24 17
 # add these directories to sys.path here. If the directory is relative to the
25 18
 # documentation root, use os.path.abspath to make it absolute, like shown here.
26  
-pyspotify_path = '../' + b.build_platlib
27  
-sys.path.insert(0, os.path.abspath(pyspotify_path))
  19
+sys.path.insert(0, os.path.abspath('..'))
28 20
 
29 21
 def get_version():
30 22
     init_py = open('../spotify/__init__.py').read()
@@ -54,7 +46,7 @@ def get_version():
54 46
 
55 47
 # General information about the project.
56 48
 project = u'pyspotify'
57  
-copyright = u'2009-2011, Doug Winter and contributors'
  49
+copyright = u'2009-2012, Doug Winter and contributors'
58 50
 
59 51
 # The version info for the project you're documenting, acts as replacement for
60 52
 # |version| and |release|, also used in various other places throughout the
@@ -227,3 +219,24 @@ def get_version():
227 219
     ('index', 'pyspotify', u'pyspotify Documentation',
228 220
      [u'Doug Winter and contributors'], 1)
229 221
 ]
  222
+
  223
+
  224
+class Mock(object):
  225
+    def __init__(self, *args, **kwargs):
  226
+        pass
  227
+
  228
+    def __call__(self, *args, **kwargs):
  229
+        return Mock()
  230
+
  231
+    @classmethod
  232
+    def __getattr__(self, name):
  233
+        if name in ('__file__', '__path__'):
  234
+            return '/dev/null'
  235
+        elif name[0] == name[0].upper():
  236
+            return type(name, (), {})
  237
+        else:
  238
+            return Mock()
  239
+
  240
+MOCK_MODULES = ['spotify._spotify']
  241
+for mod_name in MOCK_MODULES:
  242
+    sys.modules[mod_name] = Mock()
18  docs/development.rst
Source Rendered
@@ -43,7 +43,7 @@ Using Pip::
43 43
 Then you can build pyspotify and run ``nosetests``::
44 44
 
45 45
     rm -rf build/
46  
-    python setup.py build
  46
+    python setup.py build --with-mock
47 47
     PYTHONPATH=$(echo build/lib.linux-*/) nosetests
48 48
 
49 49
 
@@ -77,18 +77,8 @@ Then, to generate docs::
77 77
     make        # For help on available targets
78 78
     make html   # To generate HTML docs
79 79
 
80  
-.. note::
81  
-
82  
-    The documentation at http://pyspotify.mopidy.com/ is automatically updated
83  
-    when a documentation update is pushed to ``mopidy/pyspotify`` at GitHub.
84  
-
85  
-    Documentation generated from the ``master`` branch is published at
86  
-    http://pyspotify.mopidy.com/docs/master/, and will always be valid for the
87  
-    latest release.
88  
-
89  
-    Documentation generated from the ``develop`` branch is published at
90  
-    http://pyspotify.mopidy.com/docs/develop/, and will always be valid for the
91  
-    latest development snapshot.
  80
+The documentation at http://pyspotify.mopidy.com/ is automatically updated when
  81
+a documentation update is pushed to ``mopidy/pyspotify`` at GitHub.
92 82
 
93 83
 
94 84
 Creating releases
@@ -112,7 +102,7 @@ Creating releases
112 102
 
113 103
 #. Build package and upload to PyPI::
114 104
 
115  
-    rm MANIFEST                         # Will be regenerated by setup.py
  105
+    git clean -fdx
116 106
     python setup.py sdist upload
117 107
 
118 108
 #. Spread the word.
1  docs/index.rst
Source Rendered
@@ -8,6 +8,7 @@ Table of contents
8 8
 
9 9
     introduction
10 10
     managers/index
  11
+    audiosink
11 12
     api/index
12 13
     changes
13 14
     development
4  docs/licenses.rst
Source Rendered
@@ -9,7 +9,7 @@ contributed what, please refer to our git repository.
9 9
 Source code license
10 10
 ===================
11 11
 
12  
-Copyright 2009-2011 Doug Winter and contributors
  12
+Copyright 2009-2012 Doug Winter and contributors
13 13
 
14 14
 Licensed under the Apache License, Version 2.0 (the "License");
15 15
 you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ limitations under the License.
27 27
 Documentation license
28 28
 =====================
29 29
 
30  
-Copyright 2009-2011 Doug Winter and contributors
  30
+Copyright 2009-2012 Doug Winter and contributors
31 31
 
32 32
 This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
33 33
 Unported License. To view a copy of this license, visit
5  docs/managers/session.rst
Source Rendered
... ...
@@ -1,8 +1,9 @@
1 1
 Session manager
2  
-****************
  2
+***************
  3
+
3 4
 .. currentmodule:: spotify.manager
4 5
 
5 6
 .. autoclass:: SpotifySessionManager
6 7
     :members:
7  
-    :exclude-members: terminate,wake,loop
  8
+    :exclude-members: loop
8 9
     :member-order: bysource
162  examples/jukebox.py
... ...
@@ -1,21 +1,19 @@
1 1
 #!/usr/bin/env python
2 2
 
3 3
 import cmd
4  
-import readline
  4
+import logging
  5
+import os
5 6
 import sys
6  
-import traceback
7  
-import time
8 7
 import threading
9  
-import os
  8
+import time
  9
+
  10
+from spotify import ArtistBrowser, Link, ToplistBrowser
  11
+from spotify.audiosink import import_audio_sink
  12
+from spotify.manager import (SpotifySessionManager, SpotifyPlaylistManager,
  13
+    SpotifyContainerManager)
10 14
 
11  
-import spotify
12  
-from spotify.manager import SpotifySessionManager, SpotifyPlaylistManager, \
13  
-    SpotifyContainerManager
14  
-try:
15  
-    from spotify.alsahelper import AlsaController
16  
-except ImportError:
17  
-    from spotify.osshelper import OssController as AlsaController
18  
-from spotify import Link, SpotifyError, ToplistBrowser
  15
+AudioSink = import_audio_sink()
  16
+container_loaded = threading.Event()
19 17
 
20 18
 class JukeboxUI(cmd.Cmd, threading.Thread):
21 19
 
@@ -30,14 +28,20 @@ def __init__(self, jukebox):
30 28
         self.results = False
31 29
 
32 30
     def run(self):
33  
-        self.cmdloop()
  31
+        container_loaded.wait()
  32
+        container_loaded.clear()
  33
+        try:
  34
+            self.cmdloop()
  35
+        finally:
  36
+            self.do_quit(None)
34 37
 
35 38
     def do_logout(self, line):
36 39
         self.jukebox.session.logout()
37 40
 
38 41
     def do_quit(self, line):
  42
+        self.jukebox.stop()
  43
+        self.jukebox.disconnect()
39 44
         print "Goodbye!"
40  
-        self.jukebox.terminate()
41 45
         return True
42 46
 
43 47
     def do_list(self, line):
@@ -66,10 +70,19 @@ def do_list(self, line):
66 70
                 playlist = self.jukebox.starred
67 71
             for i, t in enumerate(playlist):
68 72
                 if t.is_loaded():
69  
-                    print "%3d %s - %s" % (i, t.artists()[0].name(), t.name())
  73
+                    print "%3d %s - %s [%s]" % (
  74
+                        i, t.artists()[0].name(), t.name(),
  75
+                        self.pretty_duration(t.duration()))
70 76
                 else:
71 77
                     print "%3d %s" % (i, "loading...")
72 78
 
  79
+    def pretty_duration(self, milliseconds):
  80
+        seconds = milliseconds // 1000
  81
+        minutes = seconds // 60
  82
+        seconds = seconds % 60
  83
+        duration = '%02d:%02d' % (minutes, seconds)
  84
+        return duration
  85
+
73 86
     def do_play(self, line):
74 87
         if not line:
75 88
             self.jukebox.play()
@@ -84,10 +97,15 @@ def do_play(self, line):
84 97
         else:
85 98
             try:
86 99
                 playlist, track = map(int, line.split(' ', 1))
  100
+                self.jukebox.load(playlist, track)
87 101
             except ValueError:
88  
-                print "Usage: play [track_link] | [playlist] [track]"
89  
-                return
90  
-            self.jukebox.load(playlist, track)
  102
+                try:
  103
+                    playlist = int(line)
  104
+                    self.jukebox.load_playlist(playlist)
  105
+                except ValueError:
  106
+                    print("Usage: play [track_link] | "
  107
+                          "[playlist] [track] | [playlist]")
  108
+                    return
91 109
         self.jukebox.play()
92 110
 
93 111
     def do_browse(self, line):
@@ -98,10 +116,23 @@ def do_browse(self, line):
98 116
         if not l.type() in [Link.LINK_ALBUM, Link.LINK_ARTIST]:
99 117
             print "You can only browse albums and artists"
100 118
             return
101  
-        def browse_finished(browser):
102  
-            print "Browse finished"
  119
+        def browse_finished(browser, userdata):
  120
+            print "Browse finished, %s" % (userdata)
103 121
         self.jukebox.browse(l, browse_finished)
104 122
 
  123
+    def print_search_results(self):
  124
+        print "Artists:"
  125
+        for a in self.results.artists():
  126
+            print "    ", Link.from_artist(a), a.name()
  127
+        print "Albums:"
  128
+        for a in self.results.albums():
  129
+            print "    ", Link.from_album(a), a.name()
  130
+        print "Tracks:"
  131
+        for a in self.results.tracks():
  132
+            print "    ", Link.from_track(a, 0), a.name()
  133
+        print self.results.total_tracks() - len(self.results.tracks()), \
  134
+            "Tracks not shown"
  135
+
105 136
     def do_search(self, line):
106 137
         if not line:
107 138
             if self.results is False:
@@ -118,12 +149,15 @@ def do_search(self, line):
118 149
                 print "Tracks:"
119 150
                 for a in self.results.tracks():
120 151
                     print "    ", Link.from_track(a, 0), a.name()
121  
-                print self.results.total_tracks() - len(self.results.tracks()), "Tracks not shown"
  152
+                print self.results.total_tracks() - \
  153
+                        len(self.results.tracks()), "Tracks not shown"
122 154
         else:
  155
+            line = line.decode('utf-8')
123 156
             self.results = None
124 157
             def search_finished(results, userdata):
125 158
                 print "\nSearch results received"
126 159
                 self.results = results
  160
+                self.print_search_results()
127 161
             self.jukebox.search(line, search_finished)
128 162
 
129 163
     def do_queue(self, line):
@@ -150,7 +184,8 @@ def emptyline(self):
150 184
     def do_watch(self, line):
151 185
         if not line:
152 186
             print """Usage: watch [playlist]
153  
-You will be notified when tracks are added, moved or removed from the playlist."""
  187
+You will be notified when tracks are added, moved or removed from the
  188
+playlist."""
154 189
         else:
155 190
             try:
156 191
                 p = int(line)
@@ -177,7 +212,7 @@ def do_unwatch(self, line):
177 212
             self.jukebox.watch(self.jukebox.ctr[p], True)
178 213
 
179 214
     def do_toplist(self, line):
180  
-        usage = "Usage: toplist (albums|artists|tracks) (GB|FR|..|NO|all|current)"
  215
+        usage = "Usage: toplist (albums|artists|tracks) (GB|FR|..|all|current)"
181 216
         if not line:
182 217
             print usage
183 218
         else:
@@ -194,7 +229,8 @@ def do_add_new_playlist(self, line):
194 229
         if not line:
195 230
             print "Usage: add_new_playlist <name>"
196 231
         else:
197  
-          new_playlist = self.jukebox.ctr.add_new_playlist(line.decode('utf-8'))
  232
+            new_playlist = self.jukebox.ctr.add_new_playlist(
  233
+                line.decode('utf-8'))
198 234
 
199 235
     def do_add_to_playlist(self, line):
200 236
         usage = "Usage: add_to_playlist <playlist_index> <insert_point>" + \
@@ -215,9 +251,12 @@ def do_add_to_playlist(self, line):
215 251
                 tracks = self.results.tracks()
216 252
                 for i in args:
217 253
                     for a in tracks[int(i)].artists():
218  
-                        print u'{}. {} - {} '.format(i,a.name(),tracks[int(i)].name())
219  
-                print u'adding them to {} '.format(self.jukebox.ctr[index].name())
220  
-                self.jukebox.ctr[index].add_tracks(insert,[tracks[int(i)] for i in args])
  254
+                        print u'{0}. {1} - {2} '.format(
  255
+                            i, a.name(), tracks[int(i)].name())
  256
+                print u'adding them to {0} '.format(
  257
+                    self.jukebox.ctr[index].name())
  258
+                self.jukebox.ctr[index].add_tracks(
  259
+                    insert, [tracks[int(i)] for i in args])
221 260
 
222 261
     do_ls = do_list
223 262
     do_EOF = do_quit
@@ -237,7 +276,7 @@ def tracks_removed(self, p, t, u):
237 276
 ## container calllbacks ##
238 277
 class JukeboxContainerManager(SpotifyContainerManager):
239 278
     def container_loaded(self, c, u):
240  
-        print 'Container loaded !'
  279
+        container_loaded.set()
241 280
 
242 281
     def playlist_added(self, c, p, i, u):
243 282
         print 'Container: playlist "%s" added.' % p.name()
@@ -257,28 +296,28 @@ class Jukebox(SpotifySessionManager):
257 296
 
258 297
     def __init__(self, *a, **kw):
259 298
         SpotifySessionManager.__init__(self, *a, **kw)
260  
-        self.audio = AlsaController()
  299
+        self.audio = AudioSink(backend=self)
261 300
         self.ui = JukeboxUI(self)
262 301
         self.ctr = None
263 302
         self.playing = False
264 303
         self._queue = []
265 304
         self.playlist_manager = JukeboxPlaylistManager()
266 305
         self.container_manager = JukeboxContainerManager()
  306
+        self.track_playing = None
267 307
         print "Logging in, please wait..."
268 308
 
  309
+    def new_track_playing(self, track):
  310
+        self.track_playing = track
269 311
 
270 312
     def logged_in(self, session, error):
271 313
         if error:
272 314
             print error
273 315
             return
274 316
         self.session = session
275  
-        try:
276  
-            self.ctr = session.playlist_container()
277  
-            self.container_manager.watch(self.ctr)
278  
-            self.starred = session.starred()
279  
-            self.ui.start()
280  
-        except:
281  
-            traceback.print_exc()
  317
+        self.ctr = session.playlist_container()
  318
+        self.container_manager.watch(self.ctr)
  319
+        self.starred = session.starred()
  320
+        self.ui.start()
282 321
 
283 322
     def logged_out(self, session):
284 323
         self.ui.cmdqueue.append("quit")
@@ -286,6 +325,7 @@ def logged_out(self, session):
286 325
     def load_track(self, track):
287 326
         if self.playing:
288 327
             self.stop()
  328
+        self.new_track_playing(track)
289 329
         self.session.load(track)
290 330
         print "Loading %s" % track.name()
291 331
 
@@ -296,17 +336,38 @@ def load(self, playlist, track):
296 336
             pl = self.ctr[playlist]
297 337
         elif playlist == len(self.ctr):
298 338
             pl = self.starred
299  
-        self.session.load(pl[track])
300  
-        print "Loading %s from %s" % (pl[track].name(), pl.name())
  339
+        spot_track = pl[track]
  340
+        self.new_track_playing(spot_track)
  341
+        self.session.load(spot_track)
  342
+        print "Loading %s from %s" % (spot_track.name(), pl.name())
  343
+
  344
+    def load_playlist(self, playlist):
  345
+        if self.playing:
  346
+            self.stop()
  347
+        if 0 <= playlist < len(self.ctr):
  348
+            pl = self.ctr[playlist]
  349
+        elif playlist == len(self.ctr):
  350
+            pl = self.starred
  351
+        print "Loading playlist %s" % pl.name()
  352
+        if len(pl):
  353
+            print "Loading %s from %s" % (pl[0].name(), pl.name())
  354
+            self.new_track_playing(pl[0])
  355
+            self.session.load(pl[0])
  356
+        for i, track in enumerate(pl):
  357
+            if i == 0:
  358
+                continue
  359
+            self._queue.append((playlist, i))
301 360
 
302 361
     def queue(self, playlist, track):
303 362
         if self.playing:
304 363
             self._queue.append((playlist, track))
305 364
         else:
  365
+            print 'Loading %s', track.name()
306 366
             self.load(playlist, track)
307 367
             self.play()
308 368
 
309 369
     def play(self):
  370
+        self.audio.start()
310 371
         self.session.play(1)
311 372
         print "Playing"
312 373
         self.playing = True
@@ -315,22 +376,22 @@ def stop(self):
315 376
         self.session.play(0)
316 377
         print "Stopping"
317 378
         self.playing = False
  379
+        self.audio.stop()
318 380
 
319  
-    def music_delivery(self, *a, **kw):
320  
-        return self.audio.music_delivery(*a, **kw)
  381
+    def music_delivery_safe(self, *args, **kwargs):
  382
+        return self.audio.music_delivery(*args, **kwargs)
321 383
 
322 384
     def next(self):
323 385
         self.stop()
324 386
         if self._queue:
325  
-            t = self._queue.pop()
  387
+            t = self._queue.pop(0)
326 388
             self.load(*t)
327 389
             self.play()
328 390
         else:
329 391
             self.stop()
330 392
 
331 393
     def end_of_track(self, sess):
332  
-        print "track ends."
333  
-        self.next()
  394
+        self.audio.end_of_track()
334 395
 
335 396
     def search(self, *args, **kwargs):
336 397
         self.session.search(*args, **kwargs)
@@ -341,19 +402,18 @@ def browse(self, link, callback):
341 402
             while not browser.is_loaded():
342 403
                 time.sleep(0.1)
343 404
             for track in browser:
344  
-                print track
  405
+                print track.name()
345 406
         if link.type() == link.LINK_ARTIST:
346  
-            browser = self.session.browse_artist(link.as_artist(), callback)
  407
+            browser = ArtistBrowser(link.as_artist())
347 408
             while not browser.is_loaded():
348 409
                 time.sleep(0.1)
349 410
             for album in browser:
350 411
                 print album.name()
351  
-        callback(browser)
352 412
 
353 413
     def watch(self, p, unwatch=False):
354 414
         if not unwatch:
355 415
             print "Watching playlist: %s" % p.name()
356  
-            self.playlist_manager.watch(p);
  416
+            self.playlist_manager.watch(p)
357 417
         else:
358 418
             print "Unatching playlist: %s" % p.name()
359 419
             self.playlist_manager.unwatch(p)
@@ -375,8 +435,12 @@ def shell(self):
375 435
 if __name__ == '__main__':
376 436
     import optparse
377 437
     op = optparse.OptionParser(version="%prog 0.1")
378  
-    op.add_option("-u", "--username", help="spotify username")
379  
-    op.add_option("-p", "--password", help="spotify password")
  438
+    op.add_option("-u", "--username", help="Spotify username")
  439
+    op.add_option("-p", "--password", help="Spotify password")
  440
+    op.add_option("-v", "--verbose", help="Show debug information",