diff --git a/clay/config.yaml b/clay/config.yaml index e1591cf..8d08b81 100644 --- a/clay/config.yaml +++ b/clay/config.yaml @@ -26,6 +26,8 @@ hotkeys: unappend: mod + u request_station: mod + p show_context_menu: meta + p + thumbs_up: meta + u + thumbs_down: meta + d library_view: move_to_beginning: home diff --git a/clay/gp.py b/clay/gp.py index 825e816..543204c 100644 --- a/clay/gp.py +++ b/clay/gp.py @@ -16,6 +16,8 @@ from gmusicapi.clients import Mobileclient from clay.eventhook import EventHook from clay.log import logger +import sys + def asynchronous(func): """ Decorates a function to become asynchronous. @@ -86,27 +88,29 @@ class Track(object): SOURCE_PLAYLIST = 'playlist' SOURCE_SEARCH = 'search' - def __init__( - self, - title, artist, duration, source, - library_id=None, store_id=None, playlist_item_id=None, - album_name=None, album_url=None, original_data=None - ): - self.title = title - self.artist = artist - self.duration = duration - self.source = source + def __init__(self, source, data): + # In playlist items and user uploaded songs the storeIds are missing so + self.store_id = (data['storeId'] if 'storeId' in data else data['id']) + self.playlist_item_id = (UUID(data['id']) if source == self.SOURCE_PLAYLIST else None) + self.library_id = (UUID(data['id']) if source == self.SOURCE_LIBRARY else None) + # To filter out the playlist items we need to reassign the store_id when fetching the track + if 'track' in data: + data = data['track'] + self.store_id = data['storeId'] + + self.title = data['title'] + self.artist = data['artist'] + self.duration = int(data['durationMillis']) + self.rating = (data['rating'] if 'rating' in data else 0) + self.source = source self.cached_url = None - self.library_id = library_id - self.store_id = store_id - self.playlist_item_id = playlist_item_id + # User uploaded songs miss a store_id + self.album_name = data['album'] + self.album_url = (data['albumArtRef'][0]['url'] if 'albumArtRef' in data else "") - self.album_name = album_name - self.album_url = album_url - - self.original_data = original_data + self.original_data = data @property def id(self): # pylint: disable=invalid-name @@ -131,100 +135,6 @@ class Track(object): (self.playlist_item_id and self.playlist_item_id == other.playlist_item_id) ) - @classmethod - def _from_search(cls, data): - """ - Create track from search result data. - """ - # Data contains a nested track representation. - return Track( - title=data['track']['title'], - artist=data['track']['artist'], - duration=int(data['track']['durationMillis']), - source=cls.SOURCE_SEARCH, - store_id=data['track']['storeId'], # or data['trackId'] - album_name=data['track']['album'], - album_url=data['track']['albumArtRef'][0]['url'], - original_data=data - ) - - @classmethod - def _from_station(cls, data): - """ - Create track from station track data. - """ - # Station tracks have all the info in place. - return Track( - title=data['title'], - artist=data['artist'], - duration=int(data['durationMillis']), - source=cls.SOURCE_STATION, - store_id=data['storeId'], - album_name=data['album'], - album_url=data['albumArtRef'][0]['url'], - original_data=data - ) - - @classmethod - def _from_library(cls, data): - """ - Create track from library track data. - """ - # Data contains all info about track - # including ID in library and ID in store. - UUID(data['id']) - return Track( - title=data['title'], - artist=data['artist'], - duration=int(data['durationMillis']), - source=cls.SOURCE_LIBRARY, - store_id=data['storeId'], - library_id=data['id'], - album_name=data['album'], - album_url=data['albumArtRef'][0]['url'], - original_data=data - ) - - @classmethod - def _from_playlist(cls, data): - """ - Create track from playlist track data. - """ - if 'track' in data: - # Data contains a nested track representation that can be used - # to construct new track. - return Track( - title=data['track']['title'], - artist=data['track']['artist'], - duration=int(data['track']['durationMillis']), - source=cls.SOURCE_PLAYLIST, - store_id=data['track']['storeId'], # or data['trackId'] - playlist_item_id=data['id'], - album_name=data['track']['album'], - album_url=data['track']['albumArtRef'][0]['url'], - original_data=data - ) - # We need to find a track in Library by trackId. - UUID(data['trackId']) - track = gp.get_track_by_id(data['trackId']) - return Track( - title=track.title, - artist=track.artist, - duration=track.duration, - source=cls.SOURCE_PLAYLIST, - store_id=track.store_id, - album_name=track.album_name, - album_url=track.album_url, - original_data=data - ) - - _CREATE_TRACK = { - SOURCE_SEARCH: '_from_search', - SOURCE_STATION: '_from_station', - SOURCE_LIBRARY: '_from_library', - SOURCE_PLAYLIST: '_from_playlist', - } - @classmethod def from_data(cls, data, source, many=False): """ @@ -232,15 +142,16 @@ class Track(object): from Google Play Music API response. """ if many: - return [ - track - for track - in [cls.from_data(one, source) for one in data] - if track is not None - ] - + return [track for track in + [cls.from_data(one, source) for one in data] + if track is not None] try: - return getattr(cls, cls._CREATE_TRACK[source])(data) + if source == cls.SOURCE_PLAYLIST and 'track' not in data: + track = gp.get_track_by_id(UUID(data['trackId'])) + else: + track = Track(source, data) + + return track except Exception as error: # pylint: disable=bare-except logger.error( 'Failed to parse track data: %s, failing data: %s', @@ -642,6 +553,16 @@ class _GP(object): self.invalidate_caches() return result + def set_track_rating(self, id_, rating): + """ + Set the rating for song with the specified ID. + + 0 for no thumb, 1 for down thumb and 5 for up thumb + """ + song = self.mobile_client.get_track_info(id_) + song['rating'] = rating + self.mobileclient.change_song_metadata(song) + @property def is_authenticated(self): """ diff --git a/clay/hotkeys.py b/clay/hotkeys.py index 694aea9..153dcb7 100644 --- a/clay/hotkeys.py +++ b/clay/hotkeys.py @@ -124,6 +124,7 @@ class _HotkeyManager(object): hotkey[0] = mod_key hotkeys[hotkey_name][' '.join(hotkey)] = action + return hotkeys def keypress(self, name, caller, super_, size, key): diff --git a/clay/player.py b/clay/player.py index 93b308d..20a0165 100644 --- a/clay/player.py +++ b/clay/player.py @@ -143,6 +143,7 @@ def _dummy_log(data, level, ctx, fmt, args): pass #+pylint: disable=unused-argument + class _Player(object): """ Interface to libVLC. Uses Queue as a playback plan. @@ -161,11 +162,11 @@ class _Player(object): def __init__(self): self.instance = vlc.Instance() print_func = CFUNCTYPE(c_void_p, - c_void_p, # data - c_int, # level - c_void_p, # context - c_char_p, # fmt - c_void_p) #args + c_void_p, # data + c_int, # level + c_void_p, # context + c_char_p, # fmt + c_void_p) # args self.instance.log_set(print_func(_dummy_log), None) diff --git a/clay/songlist.py b/clay/songlist.py index a9c41b9..3514ca2 100644 --- a/clay/songlist.py +++ b/clay/songlist.py @@ -107,6 +107,7 @@ class SongListItem(urwid.Pile): """ return SongListItem.STATE_ICONS[state] + def update_text(self): """ Update text of this item from the attached track. @@ -155,6 +156,21 @@ class SongListItem(urwid.Pile): return None return super(SongListItem, self).mouse_event(size, event, button, col, row, focus) + def thumbs_up(self): + """ + Toggle the thumbs up of this song. + """ + if self.track.rating == 5: + gp.set_track_rating(self.track.id, 0) + else: + gp.set_track_rating(self.track.id, 5) + + def thumbs_down(self): + if self.track.rating == 1: + gp.set_track_rating(self.track.id, 0) + else: + gp.set_track_rating(self.track.id, 1) + def _send_signal(self, signal): urwid.emit_signal(self, signal, self)