From f00bb26f8c11206fb85bb8bd3bc2e6c435548e48 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Fri, 16 Feb 2018 18:03:07 +0200 Subject: [PATCH 01/16] Added CHANGELOG --- CHANGELOG.rst | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 126 insertions(+) create mode 100644 CHANGELOG.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..c774468 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,125 @@ +Changelog +--------- + +Clay 0.7.2 +========== + +2018-02-12 + + * Added --transparent flag + +Clay 0.7.1 +========== + +2018-02-08 + + * Late fix for broken setup.py entrypoint support + +Clay 0.7.0 +========== + +2018-02-08 + + * Added Dockerfile + * Fixed installation instructions in README (by Valentijn) + * Load global hotkeys when X is running only (by Valentijn) + * Clarified in README that keybinder and pygobject are optional (by Valentijn) + * Improved error handling and reporting (by Valentijn) + * Version, help and keybinder command line arguments (by Valentijn) + * Added copyright (by Valentijn) + +Clay 0.6.2 +========== + +2018-02-02 + + * Fixed playback for non-subscribed accounts + +Clay 0.6.1 +========== + +2018-02-02 + + * Attempt to fix purchased song while not on paid subscription + +Clay 0.6.0 +========== + +2018-02-01 + + * Added track caching option + * More debugging + +Clay 0.5.6 +========== + +2018-01-31 + + * Added debug page + +Clay 0.5.5 +========== + +2018-01-31 + + * Added CLAY_DEBUG to log Google Play Music traffic + * Fixed typo in install_requires + * Updated README + +Clay 0.5.3 +========== + +2018-01-30 + + * Added codename + * Linter fixes + +Clay 0.5.2 +========== + +2018-01-30 + + * Fixed versioning + +Clay 0.5.1 +========== + +2018-01-30 + + * Debugging + * Cleanup & typos + * Fixed issue with uploaded tracks + +Clay 0.5 +======== + +2018-01-29 + + * Added slider for eqializer + * Updated README + * Misc fixes + +Clay 0.4 +======== + * Added equalizer + +2018-01-29 + +Clay 0.3 +======== + +2018-01-26 + + * Initial functionality + * Cleanups + * Notifications + * Hotkeys + * Linting + * Documentation + * Song search + * Song context menu + * Clearer song IDs + * Auth token caching + * Colors + * Copy URL to clipboard + diff --git a/README.md b/README.md index ed679eb..5c1ea01 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ * [Misc](#misc) - [Troubleshooting](#troubleshooting) - [Credits](#credits) +- [Changelog](./CHANGELOG.rst) # Clay [beta] From ba02cbca26a0dc0b4d80fca76c720943dae70abc Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Sun, 18 Feb 2018 01:38:20 +0200 Subject: [PATCH 02/16] Makefile docs --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index ece1a37..29f9bf7 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,11 @@ CMD ?= "./clay/app.py" +# Build Clay Docker image build: echo $(shell id -u) docker build -t clay --build-arg HOST_USER=${USER} --build-arg HOST_UID=$(shell id -u) . +# Run Clay Docker image run: | build docker run -it \ --rm \ @@ -20,10 +22,12 @@ run: | build clay \ ${CMD} +# Generate Sphinx docs .PHONY: docs docs: make -C docs html +# Run pylint & radon check: pylint clay --ignore-imports=y radon cc -a -s -nC -e clay/vlc.py clay From 43345f2dd1ed5b60f21e03fddbecb00f18b3ab7b Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Sun, 18 Feb 2018 14:05:22 +0200 Subject: [PATCH 03/16] Tweaks to equalizer. --- clay/pages/settings.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/clay/pages/settings.py b/clay/pages/settings.py index cb158fe..e562bf4 100644 --- a/clay/pages/settings.py +++ b/clay/pages/settings.py @@ -17,10 +17,14 @@ class Slider(urwid.Widget): CHARS = [ # '_', - u'\u2581', - u'\u2500', - u'\u2594' + # u'\u2581', + # u'\u2500', + # u'\u2594' + u'\u2584', + u'\u25A0', + u'\u2580', ] + ZERO_CHAR = u'\u2500' def selectable(self): return True @@ -35,7 +39,7 @@ class Slider(urwid.Widget): else: self.freq_str = str(freq) + '\nHz' self.value = 0 - self.slider_height = 5 + self.slider_height = 13 self.max_value = 20 super(Slider, self).__init__() @@ -49,16 +53,18 @@ class Slider(urwid.Widget): """ Render widget. """ - rows = [('+' if self.value >= 0 else '') + str(self.value) + ' dB'] + rows = [('+' if self.value > 0 else '') + str(self.value) + ' dB'] - chars = [' '] * 5 + chars = [' '] * self.slider_height - k = ((float(self.value) / (self.max_value + 1)) + 1) / 2 # Convert value to [0;1] range - section_index = int(k * self.slider_height) - char_index = int(k * self.slider_height * len(Slider.CHARS)) % len(Slider.CHARS) - chars[section_index] = Slider.CHARS[char_index] + if self.value == 0: + chars[self.slider_height // 2] = Slider.ZERO_CHAR + else: + k = ((float(self.value) / (self.max_value + 1)) + 1) / 2 # Convert value to [0;1] range + section_index = int(k * self.slider_height) + char_index = int(k * self.slider_height * len(Slider.CHARS)) % len(Slider.CHARS) + chars[section_index] = Slider.CHARS[char_index] - # rows.extend(['X'] * self.slider_height) rows.extend([ ( u'\u2524{}\u251C' From 36e3681d48cdece75b3c053ac0bd72f8f20401e5 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Sun, 18 Feb 2018 14:11:58 +0200 Subject: [PATCH 04/16] Cleanup. --- Dockerfile | 2 -- clay/eventhook.py | 1 - clay/gp.py | 6 +----- clay/pages/mylibrary.py | 1 - clay/pages/myplaylists.py | 2 -- clay/pages/settings.py | 4 ---- clay/songlist.py | 2 +- 7 files changed, 2 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8f81443..2aca90a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,6 @@ RUN apt-get install -y python3.6-dev python3-pip libvlc-dev vlc locales language RUN locale-gen en_US.UTF-8 RUN useradd ${HOST_USER} -m -G audio -u ${HOST_UID} -#RUN mkdir -p /home/${HOST_USER}/.config/clay -#RUN chown ${HOST_USER} /home/${HOST_USER}/.config/clay WORKDIR /home/${HOST_USER} diff --git a/clay/eventhook.py b/clay/eventhook.py index a528b94..b75cbb5 100644 --- a/clay/eventhook.py +++ b/clay/eventhook.py @@ -1,7 +1,6 @@ """ Events implemetation for signal handling. """ -# pylint: disable=too-few-public-methods class EventHook(object): diff --git a/clay/gp.py b/clay/gp.py index 7df902d..106c33c 100644 --- a/clay/gp.py +++ b/clay/gp.py @@ -2,11 +2,7 @@ Google Play Music integration via gmusicapi. """ # pylint: disable=broad-except -# pylint: disable=too-many-arguments -# pylint: disable=too-many-instance-attributes -# pylint: disable=too-many-return-statements # pylint: disable=protected-access -# pylint: disable=no-self-use from __future__ import print_function from threading import Thread, Lock from uuid import UUID @@ -86,7 +82,7 @@ class Track(object): SOURCE_PLAYLIST = 'playlist' SOURCE_SEARCH = 'search' - def __init__( + def __init__( # pylint: disable=too-many-arguments self, title, artist, duration, source, library_id=None, store_id=None, playlist_item_id=None, diff --git a/clay/pages/mylibrary.py b/clay/pages/mylibrary.py index 15d853b..3159c7c 100644 --- a/clay/pages/mylibrary.py +++ b/clay/pages/mylibrary.py @@ -43,7 +43,6 @@ class MyLibraryPage(urwid.Columns, AbstractPage): if error: notification_area.notify('Failed to load my library: {}'.format(str(error))) return - # self.notification.close() self.songlist.populate(tracks) self.app.redraw() diff --git a/clay/pages/myplaylists.py b/clay/pages/myplaylists.py index 22ba440..3c09b18 100644 --- a/clay/pages/myplaylists.py +++ b/clay/pages/myplaylists.py @@ -91,8 +91,6 @@ class MyPlaylistListBox(urwid.ListBox): ) items.append(myplaylistlistitem) - # self.notification.close() - self.walker[:] = items self.app.redraw() diff --git a/clay/pages/settings.py b/clay/pages/settings.py index e562bf4..352aef3 100644 --- a/clay/pages/settings.py +++ b/clay/pages/settings.py @@ -16,10 +16,6 @@ class Slider(urwid.Widget): _sizing = frozenset([urwid.FLOW]) CHARS = [ - # '_', - # u'\u2581', - # u'\u2500', - # u'\u2594' u'\u2584', u'\u25A0', u'\u2580', diff --git a/clay/songlist.py b/clay/songlist.py index f70287a..a659349 100644 --- a/clay/songlist.py +++ b/clay/songlist.py @@ -9,7 +9,7 @@ try: # Python 3.x from string import ascii_letters except ImportError: - # Python 2.3 + # Python 2.x from string import letters as ascii_letters import urwid from clay.notifications import notification_area From 12fb9cdcc075f5ee2dc7bdce5f19d68f6123cc02 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Fri, 23 Mar 2018 12:12:20 +0200 Subject: [PATCH 05/16] Minor tweaks to colors & settings. --- clay/app.py | 6 +++++- clay/colours.yaml | 4 ++-- clay/pages/settings.py | 10 ++++++---- clay/settings.py | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/clay/app.py b/clay/app.py index a98b0ce..52ce78e 100755 --- a/clay/app.py +++ b/clay/app.py @@ -105,7 +105,11 @@ class AppWidget(urwid.Frame): Request user authorization. """ - authtoken, device_id, _, password, username = settings.get_section("play_settings").values() + authtoken, device_id, username, password = [ + settings.get(key, "play_settings") + for key + in ('authtoken', 'device_id', 'username', 'password') + ] if self._login_notification: self._login_notification.close() diff --git a/clay/colours.yaml b/clay/colours.yaml index 785027f..67657cf 100644 --- a/clay/colours.yaml +++ b/clay/colours.yaml @@ -1,10 +1,10 @@ default: &default foreground: "#FFF" - background: "#222" + background: null primary: &primary foreground: "#F54" - background: "#FFF" + background: null primary_inv: &primary_inv foreground: "#FFF" diff --git a/clay/pages/settings.py b/clay/pages/settings.py index 3b2b483..12cf496 100644 --- a/clay/pages/settings.py +++ b/clay/pages/settings.py @@ -141,17 +141,17 @@ class SettingsPage(urwid.Columns, AbstractPage): def __init__(self, app): self.app = app self.username = urwid.Edit( - edit_text=settings.get('username', 'play_settings') + edit_text=settings.get('username', 'play_settings') or '' ) self.password = urwid.Edit( - mask='*', edit_text=settings.get('password', 'play_settings') + mask='*', edit_text=settings.get('password', 'play_settings') or '' ) self.device_id = urwid.Edit( - edit_text=settings.get('device_id', 'play_settings') + edit_text=settings.get('device_id', 'play_settings') or '' ) self.download_tracks = urwid.CheckBox( 'Download tracks before playback', - state=settings.get('download_tracks', 'play_settings') + state=settings.get('download_tracks', 'play_settings') or False ) self.equalizer = Equalizer() super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([ @@ -180,6 +180,8 @@ class SettingsPage(urwid.Columns, AbstractPage): Called when "Save" button is pressed. """ with settings.edit() as config: + if 'play_settings' not in config: + config['play_settings'] = {} config['play_settings']['username'] = self.username.edit_text config['play_settings']['password'] = self.password.edit_text config['play_settings']['device_id'] = self.device_id.edit_text diff --git a/clay/settings.py b/clay/settings.py index 80533a2..72dca95 100644 --- a/clay/settings.py +++ b/clay/settings.py @@ -121,10 +121,10 @@ class _Settings(object): section = self.get_section(*sections) try: - return section[key] + return section.get(key) except (KeyError, TypeError): section = self.get_default_config_section(*sections) - return section[key] + return section.get(key) def _get_section(self, config, *sections): config = config.copy() From eb33b3a2803723d6305a85fc53ffea2eb1299bdc Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sat, 31 Mar 2018 21:35:32 +0200 Subject: [PATCH 06/16] Changed key of page --- clay/pages/playerqueue.py | 2 +- clay/pages/search.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clay/pages/playerqueue.py b/clay/pages/playerqueue.py index b61600c..b5c7852 100644 --- a/clay/pages/playerqueue.py +++ b/clay/pages/playerqueue.py @@ -18,7 +18,7 @@ class QueuePage(urwid.Columns, AbstractPage): @property def key(self): - return 3 + return 4 def __init__(self, app): self.app = app diff --git a/clay/pages/search.py b/clay/pages/search.py index f76604c..f9aa32f 100644 --- a/clay/pages/search.py +++ b/clay/pages/search.py @@ -58,7 +58,7 @@ class SearchPage(urwid.Pile, AbstractPage): @property def key(self): - return 4 + return 5 def __init__(self, app): self.app = app From 08a60e8d951d85a308f2c02cf3a0e08f1bf73250 Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sat, 31 Mar 2018 21:36:58 +0200 Subject: [PATCH 07/16] Added a hotkey to start a station --- clay/config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clay/config.yaml b/clay/config.yaml index e1591cf..4984605 100644 --- a/clay/config.yaml +++ b/clay/config.yaml @@ -37,6 +37,9 @@ hotkeys: playlist_page: start_playlist: enter + station_page: + start_station: enter + debug_page: copy_message: enter From 7ae0c93decd5c3e3738eaf5068dcebce2d81b871 Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sat, 31 Mar 2018 22:37:49 +0200 Subject: [PATCH 08/16] Added MyStationsPage to pages --- clay/gp.py | 47 +++++++++++- clay/pages/mystations.py | 159 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 clay/pages/mystations.py 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 From fc17a4d8cfca6a0acbb3b825036ac9793ccd67ec Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sat, 31 Mar 2018 22:41:20 +0200 Subject: [PATCH 09/16] Added MyStations to pages --- clay/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clay/app.py b/clay/app.py index 52ce78e..30d8122 100755 --- a/clay/app.py +++ b/clay/app.py @@ -18,6 +18,7 @@ from clay.playbar import PlayBar from clay.pages.debug import DebugPage from clay.pages.mylibrary import MyLibraryPage from clay.pages.myplaylists import MyPlaylistsPage +from clay.pages.mystations import MyStationsPage from clay.pages.playerqueue import QueuePage from clay.pages.search import SearchPage from clay.pages.settings import SettingsPage @@ -68,6 +69,7 @@ class AppWidget(urwid.Frame): DebugPage(self), MyLibraryPage(self), MyPlaylistsPage(self), + MyStationsPage(self), QueuePage(self), SearchPage(self), SettingsPage(self) From 275b826e0c6bfb1acb3c7672a39d90710db7ccb7 Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sat, 31 Mar 2018 22:47:34 +0200 Subject: [PATCH 10/16] Added async methods to get stations data --- clay/gp.py | 47 +++-------------------------------------------- 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/clay/gp.py b/clay/gp.py index 9b2232c..1d2bd3a 100644 --- a/clay/gp.py +++ b/clay/gp.py @@ -12,8 +12,6 @@ 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. @@ -357,8 +355,7 @@ class Station(object): """ Model that represents specific station on Google Play Music. """ - def __init__(self, station_id, name): - self.name = name + def __init__(self, station_id): self._id = station_id self._tracks = [] self._tracks_loaded = False @@ -375,12 +372,9 @@ 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, STATION_FETCH_LEN) + data = gp.mobile_client.get_station_tracks(self.id, 100) 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): """ @@ -389,20 +383,6 @@ 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): """ @@ -492,7 +472,6 @@ class _GP(object): # self._last_call_index = 0 self.cached_tracks = None self.cached_playlists = None - self.cached_stations = None self.invalidate_caches() @@ -525,11 +504,10 @@ class _GP(object): def invalidate_caches(self): """ - Clear cached tracks & playlists & stations. + Clear cached tracks & playlists. """ self.cached_tracks = None self.cached_playlists = None - self.cached_stations = None self.caches_invalidated.fire() @synchronized @@ -598,25 +576,6 @@ 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, **_): """ From b38c97cc641af508f52aea16ed336dd085bc959d Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sat, 31 Mar 2018 23:04:31 +0200 Subject: [PATCH 11/16] v2:Added async methods to get stations data --- clay/gp.py | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/clay/gp.py b/clay/gp.py index 1d2bd3a..4e31625 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, **_): """ @@ -653,4 +694,4 @@ class _GP(object): return self.mobile_client.is_subscribed -gp = _GP() # pylint: disable=invalid-name +gp = _GP() # pylint: disable=invalid-name \ No newline at end of file From 04de7fc155c18c0c260ba9d965cc59a72f7bf03e Mon Sep 17 00:00:00 2001 From: fluctuz Date: Sun, 1 Apr 2018 00:01:46 +0200 Subject: [PATCH 12/16] fixed formatting --- clay/gp.py | 4 ++-- clay/pages/mystations.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clay/gp.py b/clay/gp.py index 4e31625..99ffc58 100644 --- a/clay/gp.py +++ b/clay/gp.py @@ -375,7 +375,7 @@ 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, STATION_FETCH_LEN) + 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 @@ -694,4 +694,4 @@ class _GP(object): return self.mobile_client.is_subscribed -gp = _GP() # pylint: disable=invalid-name \ No newline at end of file +gp = _GP() # pylint: disable=invalid-name diff --git a/clay/pages/mystations.py b/clay/pages/mystations.py index 0ee448b..3091900 100644 --- a/clay/pages/mystations.py +++ b/clay/pages/mystations.py @@ -142,7 +142,7 @@ class MyStationsPage(urwid.Columns, AbstractPage): 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): + def on_station_loaded(self, station, error): """ Called when station tracks fetch completes. Populates songlist with tracks from the selected station. From 797ad901221ff908593e91d4b0206a01d2b695bd Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Fri, 6 Apr 2018 11:28:46 +0300 Subject: [PATCH 13/16] Minor cleanup. --- README.md | 7 ++++--- clay/gp.py | 6 ++++-- clay/pages/mystations.py | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5c1ea01..da93fb0 100644 --- a/README.md +++ b/README.md @@ -232,10 +232,11 @@ Made by Andrew Dunai. Regards to [gmusicapi] and [VLC] who made this possible. -Special thanks to the people who contribute to this project: +People who contribute to this project: -- [Valentijn (@ValentijnvdBeek)](https://github.com/ValentijnvdBeek) -- [Sam Kingston (@sjkingo)](https://github.com/sjkingo) +- [@ValentijnvdBeek (Valentijn)](https://github.com/ValentijnvdBeek) +- [@Fluctuz](https://github.com/Fluctuz) +- [@sjkingo (Sam Kingston)](https://github.com/sjkingo) [gmusicapi]: https://github.com/simon-weber/gmusicapi [VLC]: https://wiki.videolan.org/python_bindings diff --git a/clay/gp.py b/clay/gp.py index 99ffc58..142c7d3 100644 --- a/clay/gp.py +++ b/clay/gp.py @@ -14,6 +14,7 @@ from clay.log import logger STATION_FETCH_LEN = 50 + def asynchronous(func): """ Decorates a function to become asynchronous. @@ -287,11 +288,12 @@ class Track(object): Returns :class:`.Station` instance. """ + station_name = u'Station - {}'.format(self.title) station_id = gp.mobile_client.create_station( - name=u'Station - {}'.format(self.title), + name=station_name, track_id=self.store_id ) - station = Station(station_id) + station = Station(station_id, station_name) station.load_tracks() return station diff --git a/clay/pages/mystations.py b/clay/pages/mystations.py index 3091900..cf1d74d 100644 --- a/clay/pages/mystations.py +++ b/clay/pages/mystations.py @@ -9,6 +9,7 @@ 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. @@ -17,7 +18,7 @@ class MyStationListItem(urwid.Columns): def __init__(self, station): self.station = station - self.text = urwid.SelectableIcon(u' \u2630 {} '.format( + self.text = urwid.SelectableIcon(u' \u2708 {} '.format( self.station.name ), cursor_position=3) self.text.set_layout('left', 'clip', None) From 90caca92a75a428eed629b91cd7ff9f174aa7328 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Fri, 6 Apr 2018 11:51:00 +0300 Subject: [PATCH 14/16] Version 1.0.0 --- CHANGELOG.rst | 16 ++++++++++++++++ clay/meta.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c774468..e8d7f29 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,22 @@ Changelog --------- +Clay 1.0.0 +========== + + * Configurable keybinds (by Valentijn) + * Configurable colors (by Valentijn) + * Pluggable X keybinds (by Valentijn) + * "My stations" page (by @Fluctuz) + * Better settings management (by Valentijn) + * Equalizer + * Track caching indicator + * Optimized settings & cache + * Code complexity & code climate integration + * Countless fixes + * Badges! + * IRC channel! + Clay 0.7.2 ========== diff --git a/clay/meta.py b/clay/meta.py index 899dd93..7864e89 100644 --- a/clay/meta.py +++ b/clay/meta.py @@ -2,7 +2,7 @@ Predefined values. """ APP_NAME = 'Clay Player' -VERSION = '0.7.2' +VERSION = '1.0.0' AUTHOR = "Andrew Dunai" DESCRIPTION = "Awesome standalone command line player for Google Play Music" From 2ffe6da9f4a742afb8e6f03458e144e19ab5ae8e Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Fri, 6 Apr 2018 11:55:45 +0300 Subject: [PATCH 15/16] Added missing date --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e8d7f29..ebcb9e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ Changelog Clay 1.0.0 ========== +2018-04-06 + * Configurable keybinds (by Valentijn) * Configurable colors (by Valentijn) * Pluggable X keybinds (by Valentijn) From cd7a9dc81fc4f9a67b8073535c86f857e48cf19e Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 8 Apr 2018 15:46:55 +0530 Subject: [PATCH 16/16] Fix broken links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da93fb0..55559e1 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ clay # Documentation -Documentation is ![available here](http://clay.readthedocs.io/en/latest/). +Documentation is [available here](http://clay.readthedocs.io/en/latest/). # Requirements @@ -240,7 +240,7 @@ People who contribute to this project: [gmusicapi]: https://github.com/simon-weber/gmusicapi [VLC]: https://wiki.videolan.org/python_bindings -[urwid]: urwid.org/ +[urwid]: http://www.urwid.org/ [pyyaml]: https://github.com/yaml/pyyaml [PyGObject]: https://pygobject.readthedocs.io/en/latest/getting_started.html [Keybinder]: https://github.com/kupferlauncher/keybinder