from __future__ import unicode_literals
import logging
import threading
import spotify
from spotify import ffi, lib, serialized, utils
__all__ = ["Album", "AlbumBrowser", "AlbumType"]
logger = logging.getLogger(__name__)
[docs]class Album(object):
"""A Spotify album.
You can get an album from a track or an artist, or you can create an
:class:`Album` yourself from a Spotify URI::
>>> session = spotify.Session()
# ...
>>> album = session.get_album('spotify:album:6wXDbHLesy6zWqQawAa91d')
>>> album.load().name
u'Forward / Return'
"""
def __init__(self, session, uri=None, sp_album=None, add_ref=True):
assert uri or sp_album, "uri or sp_album is required"
self._session = session
if uri is not None:
album = spotify.Link(self._session, uri=uri).as_album()
if album is None:
raise ValueError("Failed to get album from Spotify URI: %r" % uri)
sp_album = album._sp_album
add_ref = True
if add_ref:
lib.sp_album_add_ref(sp_album)
self._sp_album = ffi.gc(sp_album, lib.sp_album_release)
def __repr__(self):
return "Album(%r)" % self.link.uri
def __eq__(self, other):
if isinstance(other, self.__class__):
return self._sp_album == other._sp_album
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self._sp_album)
@property
def is_loaded(self):
"""Whether the album's data is loaded."""
return bool(lib.sp_album_is_loaded(self._sp_album))
def load(self, timeout=None):
"""Block until the album's data is loaded.
After ``timeout`` seconds with no results :exc:`~spotify.Timeout` is
raised. If ``timeout`` is :class:`None` the default timeout is used.
The method returns ``self`` to allow for chaining of calls.
"""
return utils.load(self._session, self, timeout=timeout)
@property
def is_available(self):
"""Whether the album is available in the current region.
Will always return :class:`None` if the album isn't loaded.
"""
if not self.is_loaded:
return None
return bool(lib.sp_album_is_available(self._sp_album))
@property
@serialized
def artist(self):
"""The artist of the album.
Will always return :class:`None` if the album isn't loaded.
"""
sp_artist = lib.sp_album_artist(self._sp_album)
if sp_artist == ffi.NULL:
return None
return spotify.Artist(self._session, sp_artist=sp_artist, add_ref=True)
@serialized
def cover(self, image_size=None, callback=None):
"""The album's cover :class:`Image`.
``image_size`` is an :class:`ImageSize` value, by default
:attr:`ImageSize.NORMAL`.
If ``callback`` isn't :class:`None`, it is expected to be a callable
that accepts a single argument, an :class:`Image` instance, when
the image is done loading.
Will always return :class:`None` if the album isn't loaded or the
album has no cover.
"""
if image_size is None:
image_size = spotify.ImageSize.NORMAL
cover_id = lib.sp_album_cover(self._sp_album, int(image_size))
if cover_id == ffi.NULL:
return None
sp_image = lib.sp_image_create(self._session._sp_session, cover_id)
return spotify.Image(
self._session, sp_image=sp_image, add_ref=False, callback=callback
)
def cover_link(self, image_size=None):
"""A :class:`Link` to the album's cover.
``image_size`` is an :class:`ImageSize` value, by default
:attr:`ImageSize.NORMAL`.
This is equivalent with ``album.cover(image_size).link``, except that
this method does not need to create the album cover image object to
create a link to it.
"""
if image_size is None:
image_size = spotify.ImageSize.NORMAL
sp_link = lib.sp_link_create_from_album_cover(self._sp_album, int(image_size))
return spotify.Link(self._session, sp_link=sp_link, add_ref=False)
@property
@serialized
def name(self):
"""The album's name.
Will always return :class:`None` if the album isn't loaded.
"""
name = utils.to_unicode(lib.sp_album_name(self._sp_album))
return name if name else None
@property
def year(self):
"""The album's release year.
Will always return :class:`None` if the album isn't loaded.
"""
if not self.is_loaded:
return None
return lib.sp_album_year(self._sp_album)
@property
def type(self):
"""The album's :class:`AlbumType`.
Will always return :class:`None` if the album isn't loaded.
"""
if not self.is_loaded:
return None
return AlbumType(lib.sp_album_type(self._sp_album))
@property
def link(self):
"""A :class:`Link` to the album."""
sp_link = lib.sp_link_create_from_album(self._sp_album)
return spotify.Link(self._session, sp_link=sp_link, add_ref=False)
def browse(self, callback=None):
"""Get an :class:`AlbumBrowser` for the album.
If ``callback`` isn't :class:`None`, it is expected to be a callable
that accepts a single argument, an :class:`AlbumBrowser` instance, when
the browser is done loading.
Can be created without the album being loaded.
"""
return spotify.AlbumBrowser(self._session, album=self, callback=callback)
[docs]class AlbumBrowser(object):
"""An album browser for a Spotify album.
You can get an album browser from any :class:`Album` instance by calling
:meth:`Album.browse`::
>>> session = spotify.Session()
# ...
>>> album = session.get_album('spotify:album:6wXDbHLesy6zWqQawAa91d')
>>> browser = album.browse()
>>> browser.load()
>>> len(browser.tracks)
7
"""
def __init__(
self,
session,
album=None,
callback=None,
sp_albumbrowse=None,
add_ref=True,
):
assert album or sp_albumbrowse, "album or sp_albumbrowse is required"
self._session = session
self.loaded_event = threading.Event()
if sp_albumbrowse is None:
handle = ffi.new_handle((self._session, self, callback))
self._session._callback_handles.add(handle)
sp_albumbrowse = lib.sp_albumbrowse_create(
self._session._sp_session,
album._sp_album,
_albumbrowse_complete_callback,
handle,
)
add_ref = False
if add_ref:
lib.sp_albumbrowse_add_ref(sp_albumbrowse)
self._sp_albumbrowse = ffi.gc(sp_albumbrowse, lib.sp_albumbrowse_release)
def __repr__(self):
if self.is_loaded:
return "AlbumBrowser(%r)" % self.album.link.uri
else:
return "AlbumBrowser(<not loaded>)"
def __eq__(self, other):
if isinstance(other, self.__class__):
return self._sp_albumbrowse == other._sp_albumbrowse
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self._sp_albumbrowse)
loaded_event = None
""":class:`threading.Event` that is set when the album browser is loaded.
"""
@property
def is_loaded(self):
"""Whether the album browser's data is loaded."""
return bool(lib.sp_albumbrowse_is_loaded(self._sp_albumbrowse))
def load(self, timeout=None):
"""Block until the album browser's data is loaded.
After ``timeout`` seconds with no results :exc:`~spotify.Timeout` is
raised. If ``timeout`` is :class:`None` the default timeout is used.
The method returns ``self`` to allow for chaining of calls.
"""
return utils.load(self._session, self, timeout=timeout)
@property
def error(self):
"""An :class:`ErrorType` associated with the album browser.
Check to see if there was problems creating the album browser.
"""
return spotify.ErrorType(lib.sp_albumbrowse_error(self._sp_albumbrowse))
@property
def backend_request_duration(self):
"""The time in ms that was spent waiting for the Spotify backend to
create the album browser.
Returns ``-1`` if the request was served from local cache. Returns
:class:`None` if the album browser isn't loaded yet.
"""
if not self.is_loaded:
return None
return lib.sp_albumbrowse_backend_request_duration(self._sp_albumbrowse)
@property
@serialized
def album(self):
"""Get the :class:`Album` the browser is for.
Will always return :class:`None` if the album isn't loaded.
"""
sp_album = lib.sp_albumbrowse_album(self._sp_albumbrowse)
if sp_album == ffi.NULL:
return None
return Album(self._session, sp_album=sp_album, add_ref=True)
@property
@serialized
def artist(self):
"""The :class:`Artist` of the album.
Will always return :class:`None` if the album isn't loaded.
"""
sp_artist = lib.sp_albumbrowse_artist(self._sp_albumbrowse)
if sp_artist == ffi.NULL:
return None
return spotify.Artist(self._session, sp_artist=sp_artist, add_ref=True)
@property
@serialized
def copyrights(self):
"""The album's copyright strings.
Will always return an empty list if the album browser isn't loaded.
"""
if not self.is_loaded:
return []
@serialized
def get_copyright(sp_albumbrowse, key):
return utils.to_unicode(lib.sp_albumbrowse_copyright(sp_albumbrowse, key))
return utils.Sequence(
sp_obj=self._sp_albumbrowse,
add_ref_func=lib.sp_albumbrowse_add_ref,
release_func=lib.sp_albumbrowse_release,
len_func=lib.sp_albumbrowse_num_copyrights,
getitem_func=get_copyright,
)
@property
@serialized
def tracks(self):
"""The album's tracks.
Will always return an empty list if the album browser isn't loaded.
"""
if not self.is_loaded:
return []
@serialized
def get_track(sp_albumbrowse, key):
return spotify.Track(
self._session,
sp_track=lib.sp_albumbrowse_track(sp_albumbrowse, key),
add_ref=True,
)
return utils.Sequence(
sp_obj=self._sp_albumbrowse,
add_ref_func=lib.sp_albumbrowse_add_ref,
release_func=lib.sp_albumbrowse_release,
len_func=lib.sp_albumbrowse_num_tracks,
getitem_func=get_track,
)
@property
@serialized
def review(self):
"""A review of the album.
Will always return an empty string if the album browser isn't loaded.
"""
return utils.to_unicode(lib.sp_albumbrowse_review(self._sp_albumbrowse))
@ffi.callback("void(sp_albumbrowse *, void *)")
@serialized
def _albumbrowse_complete_callback(sp_albumbrowse, handle):
logger.debug("albumbrowse_complete_callback called")
if handle == ffi.NULL:
logger.warning(
"pyspotify albumbrowse_complete_callback called without userdata"
)
return
(session, album_browser, callback) = ffi.from_handle(handle)
session._callback_handles.remove(handle)
album_browser.loaded_event.set()
if callback is not None:
callback(album_browser)
[docs]@utils.make_enum("SP_ALBUMTYPE_")
class AlbumType(utils.IntEnum):
pass