def create_image(uri): uri = Uri.from_uri(uri) if uri is None: return None return Image.from_id(uri.code)
class Artist(Descriptor): __protobuf__ = metadata_pb2.Artist gid = PropertyProxy uri = PropertyProxy('gid', func=lambda gid: Uri.from_gid('artist', gid)) name = PropertyProxy popularity = PropertyProxy top_tracks = PropertyProxy('top_track', 'TopTracks') albums = PropertyProxy('album_group', 'AlbumGroup') singles = PropertyProxy('single_group', 'AlbumGroup') compilations = PropertyProxy('compilation_group', 'AlbumGroup') appears_on = PropertyProxy('appears_on_group', 'AlbumGroup') genres = PropertyProxy('genre') external_ids = PropertyProxy('external_id', 'ExternalId') portraits = PropertyProxy('portrait', 'Image') biographies = PropertyProxy('biography') activity_periods = PropertyProxy('activity_period') restrictions = PropertyProxy('restriction') related = PropertyProxy('related') is_portrait_album_cover = PropertyProxy('is_portrait_album_cover') portrait_group = PropertyProxy('portrait_group') @staticmethod def __parsers__(): return [MercuryJSON, XML, Tunigo]
def track_uri(self, callback=None): """Requests the track stream URI. :param callback: Callback to trigger on a successful response :type callback: function :return: decorate wrapper if no callback is provided, otherwise returns the `Request` object. :rtype: function or `spotify.core.request.Request` """ def on_track_uri(response): def on_track_key(key): if callback: callback(response, EncryptedStream(self.sp, key)) self.sp.trackKeyQueue.put(on_track_key) files = {} for file in self.files: files[file.format] = file log.debug("[%s] File format found: %s", self.uri, file.format) request = self.build("sp/track_uri2", self.uri.to_id(), Uri.from_gid(None, files.get(7).file_id)) return self.request_wrapper(request, on_track_uri)
class Image(Descriptor): __protobuf__ = metadata_pb2.Image file_uri = PropertyProxy( 'file_id', func=lambda file_id: Uri.from_gid('image', file_id)) size = PropertyProxy() width = PropertyProxy height = PropertyProxy @staticmethod def __parsers__(): return [MercuryJSON, XML, Tunigo] @property def file_url(self): if not self.file_uri or type(self.file_uri) is not Uri: return None return 'https://%s/%s/%s' % (RESOURCE_HOST, SIZES.get(self.size, self.width), self.file_uri.to_id(size=40)) @classmethod def from_id(cls, id): return cls(None, { 'file_id': Uri.from_id('image', id).to_gid(size=40), 'size': 3 })
def get_object_key(self, content_type, internal): group, type = self.get_schema_key(content_type) if group is None or type is None: return None, None uri = Uri.from_gid(type, internal.gid) return 'hm://%s/%s' % (group, uri.type), uri.to_id()
def get_object_key(self, content_type, internal): group, type = self.get_schema_key(content_type) if group is None or type is None: return None, None uri = Uri.from_gid(type, internal.gid) return "hm://%s/%s" % (group, uri.type), uri.to_id()
def parse(cls, sp, data, parser): return Artist( sp, { 'name': data.get('name'), 'gid': Uri.from_uri(data.get('uri')).to_gid(), 'portrait': [{ 'imageUri': data.get('imageUri') }] }, parser.MercuryJSON, parser)
def parse(cls, sp, data, parser): return Artist(sp, { 'name': data.get('name'), 'gid': Uri.from_uri(data.get('uri')).to_gid(), 'portrait': [ { 'imageUri': data.get('imageUri') } ] }, parser.MercuryJSON, parser)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) return Image(sp, { 'file_id': Uri.from_id('image', data.get('file_id')).to_gid(size=40), 'size': convert(data.get('size'), long), 'width': convert(data.get('width'), long), 'height': convert(data.get('height'), long) }, parser.XML, parser)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) return Playlist(sp, { 'uri': Uri.from_uri(data.get('uri')), 'attributes': { 'name': data.get('name') }, 'image': data.get('image') }, parser.XML, parser)
def reply_mercury_json(self, data): self.response_type = Parser.MercuryJSON if self.multi is None: self.multi = True data = json.loads(data) for item in data: uri = Uri.from_uri(item.get('uri')) yield uri.type, item
def parse(cls, sp, data, parser): image = data.get('image') if not image: return None image_id = image[image.rfind('/') + 1:] return Image(sp, { 'file_id': Uri.from_id('image', image_id).to_gid(size=40), 'size': 0 }, parser.Tunigo, parser)
def parse(cls, sp, data, parser): image_uri = data.get('imageUri') if not image_uri: return None image_id = image_uri[image_uri.rfind('/') + 1:] return Image(sp, { 'file_id': Uri.from_id('image', image_id).to_gid(size=40), 'size': 0 }, parser.MercuryJSON, parser)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) return Playlist( sp, { 'uri': Uri.from_uri(data.get('uri')), 'attributes': { 'name': data.get('name') }, 'image': data.get('image') }, parser.XML, parser)
def playlists(self, username, start=0, count=100, callback=None): if count > 100: raise ValueError("You may only request up to 100 playlists at once") request = HermesRequest(self.sp, { 'method': 'GET', 'uri': 'hm://playlist/user/%s/rootlist?from=%s&length=%s' % (self.uri_quote(username), start, count) }, Playlist, defaults={ 'uri': Uri.from_uri('spotify:user:%s:rootlist' % username) }) return self.request_wrapper(request, callback)
def parse(cls, sp, data, parser): image_uri = None if data.get('image'): image_uri = 'spotify:image:' + data.get('image') return Playlist(sp, { 'uri': Uri.from_uri(data.get('uri')), 'attributes': { 'name': data.get('title') }, 'image': image_uri }, parser.Tunigo, parser)
def playlist(self, uri, start=0, count=100, callback=None): if type(uri) is Uri: uri = str(uri) parts = [self.uri_quote(p) for p in uri.split(':')] request = HermesRequest(self.sp, { 'method': 'GET', 'uri': 'hm://playlist/%s?from=%s&length=%s' % ('/'.join(parts[1:]), start, count) }, Playlist, defaults={ 'uri': Uri.from_uri(uri) }) return self.request_wrapper(request, callback)
def parse(cls, sp, data, parser): image_uri = None if data.get('image'): image_uri = 'spotify:image:' + data.get('image') return Playlist( sp, { 'uri': Uri.from_uri(data.get('uri')), 'attributes': { 'name': data.get('title') }, 'image': image_uri }, parser.Tunigo, parser)
def playlist(self, uri, start=0, count=100, callback=None): if type(uri) is Uri: uri = str(uri) parts = [self.uri_quote(p) for p in uri.split(":")] request = HermesRequest( self.sp, {"method": "GET", "uri": "hm://playlist/%s?from=%s&length=%s" % ("/".join(parts[1:]), start, count)}, Playlist, defaults={"uri": Uri.from_uri(uri)}, ) return self.request_wrapper(request, callback)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) uri = Uri.from_id('artist', data.get('id')) return Artist(sp, { 'gid': uri.to_gid(), 'uri': uri, 'name': data.get('name'), 'portrait': cls.get_portraits(data), 'popularity': float(data.get('popularity')) if data.get('popularity') else None, 'restriction': data.get('restrictions') }, parser.XML, parser)
class Album(Descriptor): __protobuf__ = metadata_pb2.Album gid = PropertyProxy uri = PropertyProxy('gid', func=lambda gid: Uri.from_gid('album', gid)) name = PropertyProxy artists = PropertyProxy('artist', 'Artist') type = PropertyProxy label = PropertyProxy date = PropertyProxy(func=PropertyProxy.parse_date) popularity = PropertyProxy genres = PropertyProxy('genre') covers = PropertyProxy('cover', 'Image') external_ids = PropertyProxy('external_id', 'ExternalId') discs = PropertyProxy('disc', 'Disc') # review - [] copyrights = PropertyProxy('copyright', 'Copyright') restrictions = PropertyProxy('restriction', 'Restriction') # related - [] # sale_period - [] cover_group = PropertyProxy('cover_group', 'ImageGroup') @staticmethod def __parsers__(): return [MercuryJSON, XML, Tunigo] def is_available(self): message = '' for restriction in self.restrictions: success, message = restriction.check() if success: return True log.debug('Album "%s" not available (%s)', self.uri, message) return False @property def tracks(self): # Iterate through each disc and return a flat track list for disc in self.discs: for track in disc.tracks: yield track
def playlists(self, username, start=0, count=100, callback=None): if count > 100: raise ValueError("You may only request up to 100 playlists at once") request = HermesRequest( self.sp, { "method": "GET", "uri": "hm://playlist/user/%s/rootlist?from=%s&length=%s" % (self.uri_quote(username), start, count), }, Playlist, defaults={"uri": Uri.from_uri("spotify:user:%s:rootlist" % username)}, ) return self.request_wrapper(request, callback)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) return Image( sp, { 'file_id': Uri.from_id('image', data.get('file_id')).to_gid(size=40), 'size': convert(data.get('size'), long), 'width': convert(data.get('width'), long), 'height': convert(data.get('height'), long) }, parser.XML, parser)
def parse(cls, sp, data, parser): return Album(sp, { 'gid': Uri.from_uri(data.get('uri')).to_gid(), 'name': data.get('albumName'), 'artist': [ { 'artistName': data.get('artistName') } ], 'cover': [ { 'image': data.get('image') } ], }, parser.Tunigo, parser)
def parse(cls, sp, data, parser): return Album(sp, { 'name': data.get('name'), 'gid': Uri.from_uri(data.get('uri')).to_gid(), 'artist': [ { 'uri': data.get('artistUri'), 'name': data.get('artistName') } ], 'cover': [ { 'imageUri': data.get('imageUri'), } ] }, parser.MercuryJSON, parser)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data, {'artist-id': ('artist-id', 'artist')}) uri = Uri.from_id('track', data.get('id')) return Track( sp, { 'gid': uri.to_gid(), 'uri': uri, 'name': data.get('title'), 'artist': [{ '$source': 'node', 'id': artist.get('artist-id'), 'name': artist.get('artist') } for artist in data.get('artist', [])], 'album': { '$source': 'node', 'id': data.get('album-id'), 'name': data.get('album'), 'artist-id': data.get('album-artist-id'), 'artist-name': data.get('album-artist'), 'cover': data.get('cover'), 'cover-small': data.get('cover-small'), 'cover-large': data.get('cover-large'), }, # TODO year 'number': int(data.get('track-number')), 'duration': int(data.get('length')), 'popularity': float(data.get('popularity')), 'external_id': data.get('external-ids'), 'restriction': data.get('restrictions'), 'file': data.get('files') }, parser.XML, parser)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data, { 'artist-id': ('artist-id', 'artist') }) uri = Uri.from_id('track', data.get('id')) return Track(sp, { 'gid': uri.to_gid(), 'uri': uri, 'name': data.get('title'), 'artist': [ { '$source': 'node', 'id': artist.get('artist-id'), 'name': artist.get('artist') } for artist in data.get('artist', []) ], 'album': { '$source': 'node', 'id': data.get('album-id'), 'name': data.get('album'), 'artist-id': data.get('album-artist-id'), 'artist-name': data.get('album-artist'), 'cover': data.get('cover'), 'cover-small': data.get('cover-small'), 'cover-large': data.get('cover-large'), }, # TODO year 'number': int(data.get('track-number')), 'duration': int(data.get('length')), 'popularity': float(data.get('popularity')), 'external_id': data.get('external-ids'), 'restriction': data.get('restrictions'), 'file': data.get('files') }, parser.XML, parser)
def list(self, group=None, flat=False): if group: # Pull the code from a group URI parts = group.split(':') group = parts[2] if len(parts) == 4 else group path = [] for item in self.items: if item.uri.type == 'start-group': # Ignore groups if we are returning a flat list if flat: continue # Only return placeholders on the root level if (not group and not path) or (path and path[-1] == group): # Return group placeholder yield PlaylistItem(self.sp).dict_update({ 'uri': Uri.from_uri('spotify:group:%s:%s' % (item.uri.code, item.uri.title)), 'name': item.uri.title }) path.append(item.uri.code) continue elif item.uri.type == 'end-group': # Group close tag if path and path.pop() == group: return continue if group is None: # Ignore if we are inside a group if path and not flat: continue else: # Ignore if we aren't inside the specified group if not path or path[-1] != group: continue # Return item yield item
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) return Album(sp, { 'gid': Uri.from_id('album', data.get('id')).to_gid(), 'name': data.get('name'), 'artist': [ { '$source': 'node', 'id': data.get('artist-id'), 'name': data.get('artist-name') } ], 'type': cls.get_type(data.get('album-type')), 'cover': cls.get_covers(data), 'popularity': convert(data.get('popularity'), float), 'restriction': data.get('restrictions'), 'external_id': data.get('external-ids') }, parser.XML, parser)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data) uri = Uri.from_id('artist', data.get('id')) return Artist( sp, { 'gid': uri.to_gid(), 'uri': uri, 'name': data.get('name'), 'portrait': cls.get_portraits(data), 'popularity': float(data.get('popularity')) if data.get('popularity') else None, 'restriction': data.get('restrictions') }, parser.XML, parser)
def get(self, uris, callback=None): log.debug("metadata(%s)", uris) multi = type(uris) is list if type(uris) is not list: uris = [uris] # array of "request" Objects that will be protobuf'd requests = [] h_type = "" for uri in uris: if type(uri) is not Uri: uri = Uri.from_uri(uri) if uri.type == "local": log.debug('ignoring "local" track URI: %s', uri) continue h_type = uri.type requests.append({"method": "GET", "uri": "hm://metadata/%s/%s" % (uri.type, uri.to_id())}) # Build ProtoRequest request = HermesRequest( self.sp, requests, { "vnd.spotify/metadata-artist": Artist, "vnd.spotify/metadata-album": Album, "vnd.spotify/metadata-track": Track, }, {"method": "GET", "uri": "hm://metadata/%ss" % h_type}, container="objects", multi=multi, ) return self.request_wrapper(request, callback)
def get(self, uris, callback=None): log.debug('metadata(%s)', uris) multi = type(uris) is list if type(uris) is not list: uris = [uris] # array of "request" Objects that will be protobuf'd requests = [] h_type = '' for uri in uris: if type(uri) is not Uri: uri = Uri.from_uri(uri) if uri.type == 'local': log.debug('ignoring "local" track URI: %s', uri) continue h_type = uri.type requests.append({ 'method': 'GET', 'uri': 'hm://metadata/%s/%s' % (uri.type, uri.to_id()) }) # Build ProtoRequest request = HermesRequest(self.sp, requests, { 'vnd.spotify/metadata-artist': Artist, 'vnd.spotify/metadata-album': Album, 'vnd.spotify/metadata-track': Track }, { 'method': 'GET', 'uri': 'hm://metadata/%ss' % h_type }, multi=multi) return self.request_wrapper(request, callback)
def get(self, uris, callback=None): log.debug('metadata(%s)', uris) multi = type(uris) is list if type(uris) is not list: uris = [uris] # array of "request" Objects that will be protobuf'd requests = [] h_type = '' for uri in uris: if type(uri) is not Uri: uri = Uri.from_uri(uri) if uri.type == 'local': log.debug('ignoring "local" track URI: %s', uri) continue h_type = uri.type requests.append({ 'method': 'GET', 'uri': 'hm://metadata/%s/%s' % (uri.type, uri.to_id()) }) # Build ProtoRequest request = HermesRequest(self.sp, requests, { 'vnd.spotify/metadata-artist': Artist, 'vnd.spotify/metadata-album': Album, 'vnd.spotify/metadata-track': Track }, { 'method': 'GET', 'uri': 'hm://metadata/%ss' % h_type }, container='objects', multi=multi) return self.request_wrapper(request, callback)
def parse(cls, sp, data, parser): if type(data) is not dict: data = etree_convert(data, {"artist-id": ("artist-id", "artist")}) uri = Uri.from_id("track", data.get("id")) return Track( sp, { "gid": uri.to_gid(), "uri": uri, "name": data.get("title"), "artist": [ {"$source": "node", "id": artist.get("artist-id"), "name": artist.get("artist")} for artist in data.get("artist", []) ], "album": { "$source": "node", "id": data.get("album-id"), "name": data.get("album"), "artist-id": data.get("album-artist-id"), "artist-name": data.get("album-artist"), "cover": data.get("cover"), "cover-small": data.get("cover-small"), "cover-large": data.get("cover-large"), }, # TODO year "number": int(data.get("track-number")), "duration": int(data.get("length")), "popularity": float(data.get("popularity")), "external_id": data.get("external-ids"), "restriction": data.get("restrictions"), "file": data.get("files"), }, parser.XML, parser, )
class Track(Descriptor): __protobuf__ = metadata_pb2.Track gid = PropertyProxy uri = PropertyProxy('gid', func=lambda gid: Uri.from_gid('track', gid)) name = PropertyProxy album = PropertyProxy('album', 'Album') artists = PropertyProxy('artist', 'Artist') number = PropertyProxy disc_number = PropertyProxy duration = PropertyProxy popularity = PropertyProxy explicit = PropertyProxy external_ids = PropertyProxy('external_id', 'ExternalId') restrictions = PropertyProxy('restriction', 'Restriction') files = PropertyProxy('file', 'AudioFile') alternatives = PropertyProxy('alternative', 'Track') # sale_period - [] preview = PropertyProxy @staticmethod def __parsers__(): return [XML] def is_available(self): catalogues = {} available = False for restriction in self.restrictions: re_available, re_allowed = restriction.check() if re_allowed and restriction.catalogues: for catalogue in restriction.catalogues: catalogues[catalogue] = True if restriction.type is None or restriction.type == 'streaming': available |= re_available if catalogues.get(self.sp.catalogue): return True return False def find_alternative(self): if not self.alternatives: log.debug('No alternatives available for "%s"', self.uri) return False alternative = None # Try find an available alternative for alternative in self.alternatives: if alternative.is_available(): break else: alternative = None if alternative is None: log.debug('Unable to find alternative for "%s"', self.uri) return False # Update our object with new attributes self.protobuf_update(alternative, 'uri', 'gid', 'restrictions', 'files') return True def track_uri(self, callback=None): """Requests the track stream URI. :param callback: Callback to trigger on a successful response :type callback: function :return: decorate wrapper if no callback is provided, otherwise returns the `Request` object. :rtype: function or `spotify.core.request.Request` """ request = self.build('sp/track_uri', 'mp3160', self.uri.to_id()) return self.request_wrapper(request, callback) def track_event(self, lid, event, time): """Send the "sp/track_event" event. :param lid: Stream lid (from "sp/track_uri") :param event: Event :param time: Current track playing position (in milliseconds) """ return self.send('sp/track_event', lid, event, time) def track_progress(self, lid, position, source='unknown', reason='unknown', latency=150, context='unknown', referrer=None): """ :type lid: str :type position: int :type source: str :type reason: str :type latency: int :type context: str :type referrer: dict {'referrer', 'version', 'vendor'} :return: """ referrer = set_defaults(referrer, { 'referrer': 'unknown', 'version': '0.1.0', 'vendor': 'com.spotify' }) return self.send( 'sp/track_progress', lid, # Start source, reason, # Timings position, latency, # Context context, str(self.uri), # Referrer referrer['referrer'], referrer['version'], referrer['vendor'], ) def track_end(self, lid, position, seeks=None, latency=150, context='unknown', source=None, reason=None, referrer=None): """ :type lid: str :type position: int :type seeks: dict {'num_forward', 'num_backward', 'ms_forward', 'ms_backward'} :type latency: int :type context: str :type source: dict {'start', 'end'} :type reason: dict {'start', 'end'} :type referrer: dict {'referrer', 'version', 'vendor'} :return: """ seeks = set_defaults(seeks, { 'num_forward': 0, 'num_backward': 0, 'ms_forward': 0, 'ms_backward': 0 }) source = set_defaults(source, {'start': 'unknown', 'end': 'unknown'}) reason = set_defaults(reason, {'start': 'unknown', 'end': 'unknown'}) referrer = set_defaults(referrer, { 'referrer': 'unknown', 'version': '0.1.0', 'vendor': 'com.spotify' }) return self.send( 'sp/track_end', lid, # Timings position, # ms_played position, # ms_played_union # Seek count seeks['num_forward'], seeks['num_backward'], seeks['ms_forward'], seeks['ms_backward'], latency, # Context str(self.uri), context, # Source source['start'], source['end'], # Reason reason['start'], reason['end'], # Referrer referrer['referrer'], referrer['version'], referrer['vendor'], position # max_continuous )
def from_id(cls, id): return cls(None, { 'file_id': Uri.from_id('image', id).to_gid(size=40), 'size': 3 })