from __future__ import unicode_literals
import base64
import logging
import threading
import spotify
from spotify import ffi, lib, serialized, utils
__all__ = ['Image', 'ImageFormat', 'ImageSize']
logger = logging.getLogger(__name__)
[docs]class Image(object):
"""A Spotify image.
You can get images from :meth:`Album.cover`, :meth:`Artist.portrait`,
:meth:`Playlist.image`, or you can create an :class:`Image` yourself from a
Spotify URI::
>>> session = spotify.Session()
# ...
>>> image = session.get_image(
... 'spotify:image:a0bdcbe11b5cd126968e519b5ed1050b0e8183d0')
>>> image.load().data_uri[:50]
u'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEBLAEsAAD'
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.
"""
def __init__(
self, session, uri=None, sp_image=None, add_ref=True, callback=None
):
assert uri or sp_image, 'uri or sp_image is required'
self._session = session
if uri is not None:
image = spotify.Link(self._session, uri=uri).as_image()
if image is None:
raise ValueError(
'Failed to get image from Spotify URI: %r' % uri
)
sp_image = image._sp_image
add_ref = True
if add_ref:
lib.sp_image_add_ref(sp_image)
self._sp_image = ffi.gc(sp_image, lib.sp_image_release)
self.loaded_event = threading.Event()
handle = ffi.new_handle((self._session, self, callback))
self._session._callback_handles.add(handle)
spotify.Error.maybe_raise(
lib.sp_image_add_load_callback(
self._sp_image, _image_load_callback, handle
)
)
def __repr__(self):
return 'Image(%r)' % self.link.uri
def __eq__(self, other):
if isinstance(other, self.__class__):
return self._sp_image == other._sp_image
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self._sp_image)
loaded_event = None
""":class:`threading.Event` that is set when the image is loaded."""
@property
def is_loaded(self):
"""Whether the image's data is loaded."""
return bool(lib.sp_image_is_loaded(self._sp_image))
@property
def error(self):
"""An :class:`ErrorType` associated with the image.
Check to see if there was problems loading the image.
"""
return spotify.ErrorType(lib.sp_image_error(self._sp_image))
[docs] def load(self, timeout=None):
"""Block until the image'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 format(self):
"""The :class:`ImageFormat` of the image.
Will always return :class:`None` if the image isn't loaded.
"""
if not self.is_loaded:
return None
return ImageFormat(lib.sp_image_format(self._sp_image))
@property
@serialized
def data(self):
"""The raw image data as a bytestring.
Will always return :class:`None` if the image isn't loaded.
"""
if not self.is_loaded:
return None
data_size_ptr = ffi.new('size_t *')
data = lib.sp_image_data(self._sp_image, data_size_ptr)
buffer_ = ffi.buffer(data, data_size_ptr[0])
data_bytes = buffer_[:]
assert len(data_bytes) == data_size_ptr[0], '%r == %r' % (
len(data_bytes),
data_size_ptr[0],
)
return data_bytes
@property
def data_uri(self):
"""The raw image data as a data: URI.
Will always return :class:`None` if the image isn't loaded.
"""
if not self.is_loaded:
return None
if self.format is not ImageFormat.JPEG:
raise ValueError('Unknown image format: %r' % self.format)
return 'data:image/jpeg;base64,%s' % (
base64.b64encode(self.data).decode('ascii')
)
@property
def link(self):
"""A :class:`Link` to the image."""
return spotify.Link(
self._session,
sp_link=lib.sp_link_create_from_image(self._sp_image),
add_ref=False,
)
@ffi.callback('void(sp_image *, void *)')
@serialized
def _image_load_callback(sp_image, handle):
logger.debug('image_load_callback called')
if handle == ffi.NULL:
logger.warning('pyspotify image_load_callback called without userdata')
return
(session, image, callback) = ffi.from_handle(handle)
session._callback_handles.remove(handle)
image.loaded_event.set()
if callback is not None:
callback(image)
# Load callbacks are by nature only called once per image, so we clean up
# and remove the load callback the first time it is called.
lib.sp_image_remove_load_callback(sp_image, _image_load_callback, handle)
[docs]@utils.make_enum('SP_IMAGE_SIZE_')
class ImageSize(utils.IntEnum):
pass