diff --git a/clay/gp.py b/clay/gp.py index 1d2bd3a..9b2232c 100644 --- a/clay/gp.py +++ b/clay/gp.py @@ -12,6 +12,8 @@ from gmusicapi.clients import Mobileclient from clay.eventhook import EventHook from clay.log import logger +STATION_FETCH_LEN = 50 + def asynchronous(func): """ Decorates a function to become asynchronous. @@ -355,7 +357,8 @@ class Station(object): """ Model that represents specific station on Google Play Music. """ - def __init__(self, station_id): + def __init__(self, station_id, name): + self.name = name self._id = station_id self._tracks = [] self._tracks_loaded = False @@ -372,9 +375,12 @@ class Station(object): Fetch tracks related to this station and populate it with :class:`Track` instances. """ - data = gp.mobile_client.get_station_tracks(self.id, 100) + data = gp.mobile_client.get_station_tracks(self.id, STATION_FETCH_LEN) self._tracks = Track.from_data(data, Track.SOURCE_STATION, many=True) self._tracks_loaded = True + return self + + load_tracks_async = asynchronous(load_tracks) def get_tracks(self): """ @@ -383,6 +389,20 @@ class Station(object): assert self._tracks_loaded, 'Must call ".load_tracks()" before ".get_tracks()"' return self._tracks + @classmethod + def from_data(cls, data, many=False): + """ + Construct and return one or many :class:`.Station` instances + from Google Play Music API response. + """ + if many: + return [cls.from_data(one) for one in data if one['inLibrary']] + + return Station( + station_id=data['id'], + name=data['name'] + ) + class SearchResults(object): """ @@ -472,6 +492,7 @@ class _GP(object): # self._last_call_index = 0 self.cached_tracks = None self.cached_playlists = None + self.cached_stations = None self.invalidate_caches() @@ -504,10 +525,11 @@ class _GP(object): def invalidate_caches(self): """ - Clear cached tracks & playlists. + Clear cached tracks & playlists & stations. """ self.cached_tracks = None self.cached_playlists = None + self.cached_stations = None self.caches_invalidated.fire() @synchronized @@ -576,6 +598,25 @@ class _GP(object): get_stream_url_async = asynchronous(get_stream_url) + @synchronized + def get_all_user_station_contents(self, **_): + """ + Return list of :class:`.Station` instances. + """ + if self.cached_stations: + return self.cached_stations + self.get_all_tracks() + + self.cached_stations = Station.from_data( + self.mobile_client.get_all_stations(), + True + ) + return self.cached_stations + + get_all_user_station_contents_async = ( # pylint: disable=invalid-name + asynchronous(get_all_user_station_contents) + ) + @synchronized def get_all_user_playlist_contents(self, **_): """ diff --git a/clay/pages/mystations.py b/clay/pages/mystations.py new file mode 100644 index 0000000..0ee448b --- /dev/null +++ b/clay/pages/mystations.py @@ -0,0 +1,159 @@ +""" +Components for "My stations" page. +""" +import urwid + +from clay.gp import gp +from clay.songlist import SongListBox +from clay.notifications import notification_area +from clay.pages.page import AbstractPage +from clay.hotkeys import hotkey_manager + +class MyStationListItem(urwid.Columns): + """ + One station in the list of stations. + """ + signals = ['activate'] + + def __init__(self, station): + self.station = station + self.text = urwid.SelectableIcon(u' \u2630 {} '.format( + self.station.name + ), cursor_position=3) + self.text.set_layout('left', 'clip', None) + self.content = urwid.AttrWrap( + self.text, + 'default', + 'selected' + ) + super(MyStationListItem, self).__init__([self.content]) + + def keypress(self, size, key): + """ + Handle keypress. + """ + return hotkey_manager.keypress("station_page", self, super(MyStationListItem, self), + size, key) + + def start_station(self): + """ + Start playing the selected station + """ + urwid.emit_signal(self, 'activate', self) + return None + + +class MyStationListBox(urwid.ListBox): + """ + List of stations. + """ + signals = ['activate'] + + def __init__(self, app): + self.app = app + + self.walker = urwid.SimpleListWalker([ + urwid.Text('Not ready') + ]) + self.notification = None + + gp.auth_state_changed += self.auth_state_changed + + super(MyStationListBox, self).__init__(self.walker) + + def auth_state_changed(self, is_auth): + """ + Called when auth state changes (e. g. user is logged in). + Requests fetching of station. + """ + if is_auth: + self.walker[:] = [ + urwid.Text(u'\n \uf01e Loading stations...', align='center') + ] + + gp.get_all_user_station_contents_async(callback=self.on_get_stations) + + def on_get_stations(self, stations, error): + """ + Called when a list of stations fetch completes. + Populates list of stations. + """ + if error: + notification_area.notify('Failed to get stations: {}'.format(str(error))) + + items = [] + for station in stations: + mystationlistitem = MyStationListItem(station) + urwid.connect_signal( + mystationlistitem, 'activate', self.item_activated + ) + items.append(mystationlistitem) + + self.walker[:] = items + + self.app.redraw() + + def item_activated(self, mystationlistitem): + """ + Called when a specific station is selected. + Re-emits this event. + """ + urwid.emit_signal(self, 'activate', mystationlistitem) + + +class MyStationsPage(urwid.Columns, AbstractPage): + """ + Stations page. + + Contains two parts: + + - List of stations (:class:`.MyStationBox`) + - List of songs in selected station (:class:`clay:songlist:SongListBox`) + """ + @property + def name(self): + return 'Stations' + + @property + def key(self): + return 3 + + def __init__(self, app): + self.app = app + + self.stationlist = MyStationListBox(app) + self.songlist = SongListBox(app) + self.songlist.set_placeholder('\n Select a station.') + + urwid.connect_signal( + self.stationlist, 'activate', self.mystationlistitem_activated + ) + + super(MyStationsPage, self).__init__([ + self.stationlist, + self.songlist + ]) + + def mystationlistitem_activated(self, mystationlistitem): + """ + Called when specific station is selected. + Requests fetching of station tracks + """ + self.songlist.set_placeholder(u'\n \uf01e Loading station tracks...') + mystationlistitem.station.load_tracks_async(callback=self.on_station_loaded) + + def on_station_loaded(self, station, error): + """ + Called when station tracks fetch completes. + Populates songlist with tracks from the selected station. + """ + if error: + notification_area.notify('Failed to get station tracks: {}'.format(str(error))) + + self.songlist.populate( + station.get_tracks() + ) + self.app.redraw() + + def activate(self): + pass