mirror of
https://github.com/vale981/clay
synced 2025-03-05 17:41:42 -05:00
Merge branch 'master' into ratings
This commit is contained in:
commit
3200c67b72
18 changed files with 399 additions and 45 deletions
143
CHANGELOG.rst
Normal file
143
CHANGELOG.rst
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
Changelog
|
||||||
|
---------
|
||||||
|
|
||||||
|
Clay 1.0.0
|
||||||
|
==========
|
||||||
|
|
||||||
|
2018-04-06
|
||||||
|
|
||||||
|
* 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
|
||||||
|
==========
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -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 locale-gen en_US.UTF-8
|
||||||
|
|
||||||
RUN useradd ${HOST_USER} -m -G audio -u ${HOST_UID}
|
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}
|
WORKDIR /home/${HOST_USER}
|
||||||
|
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -1,9 +1,11 @@
|
||||||
CMD ?= "./clay/app.py"
|
CMD ?= "./clay/app.py"
|
||||||
|
|
||||||
|
# Build Clay Docker image
|
||||||
build:
|
build:
|
||||||
echo $(shell id -u)
|
echo $(shell id -u)
|
||||||
docker build -t clay --build-arg HOST_USER=${USER} --build-arg HOST_UID=$(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
|
run: | build
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--rm \
|
--rm \
|
||||||
|
@ -20,10 +22,12 @@ run: | build
|
||||||
clay \
|
clay \
|
||||||
${CMD}
|
${CMD}
|
||||||
|
|
||||||
|
# Generate Sphinx docs
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs:
|
docs:
|
||||||
make -C docs html
|
make -C docs html
|
||||||
|
|
||||||
|
# Run pylint & radon
|
||||||
check:
|
check:
|
||||||
pylint clay --ignore-imports=y
|
pylint clay --ignore-imports=y
|
||||||
radon cc -a -s -nC -e clay/vlc.py clay
|
radon cc -a -s -nC -e clay/vlc.py clay
|
||||||
|
|
12
README.md
12
README.md
|
@ -19,6 +19,7 @@
|
||||||
* [Misc](#misc)
|
* [Misc](#misc)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Credits](#credits)
|
- [Credits](#credits)
|
||||||
|
- [Changelog](./CHANGELOG.rst)
|
||||||
|
|
||||||
# Clay [beta]
|
# Clay [beta]
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ clay
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
Documentation is .
|
Documentation is [available here](http://clay.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
|
@ -235,14 +236,15 @@ Made by Andrew Dunai.
|
||||||
|
|
||||||
Regards to [gmusicapi] and [VLC] who made this possible.
|
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)
|
- [@ValentijnvdBeek (Valentijn)](https://github.com/ValentijnvdBeek)
|
||||||
- [Sam Kingston (@sjkingo)](https://github.com/sjkingo)
|
- [@Fluctuz](https://github.com/Fluctuz)
|
||||||
|
- [@sjkingo (Sam Kingston)](https://github.com/sjkingo)
|
||||||
|
|
||||||
[gmusicapi]: https://github.com/simon-weber/gmusicapi
|
[gmusicapi]: https://github.com/simon-weber/gmusicapi
|
||||||
[VLC]: https://wiki.videolan.org/python_bindings
|
[VLC]: https://wiki.videolan.org/python_bindings
|
||||||
[urwid]: urwid.org/
|
[urwid]: http://www.urwid.org/
|
||||||
[pyyaml]: https://github.com/yaml/pyyaml
|
[pyyaml]: https://github.com/yaml/pyyaml
|
||||||
[PyGObject]: https://pygobject.readthedocs.io/en/latest/getting_started.html
|
[PyGObject]: https://pygobject.readthedocs.io/en/latest/getting_started.html
|
||||||
[Keybinder]: https://github.com/kupferlauncher/keybinder
|
[Keybinder]: https://github.com/kupferlauncher/keybinder
|
||||||
|
|
|
@ -18,6 +18,7 @@ from clay.playbar import PlayBar
|
||||||
from clay.pages.debug import DebugPage
|
from clay.pages.debug import DebugPage
|
||||||
from clay.pages.mylibrary import MyLibraryPage
|
from clay.pages.mylibrary import MyLibraryPage
|
||||||
from clay.pages.myplaylists import MyPlaylistsPage
|
from clay.pages.myplaylists import MyPlaylistsPage
|
||||||
|
from clay.pages.mystations import MyStationsPage
|
||||||
from clay.pages.playerqueue import QueuePage
|
from clay.pages.playerqueue import QueuePage
|
||||||
from clay.pages.search import SearchPage
|
from clay.pages.search import SearchPage
|
||||||
from clay.pages.settings import SettingsPage
|
from clay.pages.settings import SettingsPage
|
||||||
|
@ -68,6 +69,7 @@ class AppWidget(urwid.Frame):
|
||||||
DebugPage(self),
|
DebugPage(self),
|
||||||
MyLibraryPage(self),
|
MyLibraryPage(self),
|
||||||
MyPlaylistsPage(self),
|
MyPlaylistsPage(self),
|
||||||
|
MyStationsPage(self),
|
||||||
QueuePage(self),
|
QueuePage(self),
|
||||||
SearchPage(self),
|
SearchPage(self),
|
||||||
SettingsPage(self)
|
SettingsPage(self)
|
||||||
|
@ -105,7 +107,11 @@ class AppWidget(urwid.Frame):
|
||||||
|
|
||||||
Request user authorization.
|
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:
|
if self._login_notification:
|
||||||
self._login_notification.close()
|
self._login_notification.close()
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
default: &default
|
default: &default
|
||||||
foreground: "#FFF"
|
foreground: "#FFF"
|
||||||
background: "#222"
|
background: null
|
||||||
|
|
||||||
primary: &primary
|
primary: &primary
|
||||||
foreground: "#F54"
|
foreground: "#F54"
|
||||||
background: "#FFF"
|
background: null
|
||||||
|
|
||||||
primary_inv: &primary_inv
|
primary_inv: &primary_inv
|
||||||
foreground: "#FFF"
|
foreground: "#FFF"
|
||||||
|
|
|
@ -40,6 +40,9 @@ hotkeys:
|
||||||
playlist_page:
|
playlist_page:
|
||||||
start_playlist: enter
|
start_playlist: enter
|
||||||
|
|
||||||
|
station_page:
|
||||||
|
start_station: enter
|
||||||
|
|
||||||
debug_page:
|
debug_page:
|
||||||
copy_message: enter
|
copy_message: enter
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Events implemetation for signal handling.
|
Events implemetation for signal handling.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
|
|
||||||
|
|
||||||
class EventHook(object):
|
class EventHook(object):
|
||||||
|
|
56
clay/gp.py
56
clay/gp.py
|
@ -2,11 +2,7 @@
|
||||||
Google Play Music integration via gmusicapi.
|
Google Play Music integration via gmusicapi.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=broad-except
|
# 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=protected-access
|
||||||
# pylint: disable=no-self-use
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
@ -16,6 +12,8 @@ from gmusicapi.clients import Mobileclient
|
||||||
from clay.eventhook import EventHook
|
from clay.eventhook import EventHook
|
||||||
from clay.log import logger
|
from clay.log import logger
|
||||||
|
|
||||||
|
STATION_FETCH_LEN = 50
|
||||||
|
|
||||||
|
|
||||||
def asynchronous(func):
|
def asynchronous(func):
|
||||||
"""
|
"""
|
||||||
|
@ -202,11 +200,12 @@ class Track(object):
|
||||||
|
|
||||||
Returns :class:`.Station` instance.
|
Returns :class:`.Station` instance.
|
||||||
"""
|
"""
|
||||||
|
station_name = u'Station - {}'.format(self.title)
|
||||||
station_id = gp.mobile_client.create_station(
|
station_id = gp.mobile_client.create_station(
|
||||||
name=u'Station - {}'.format(self.title),
|
name=station_name,
|
||||||
track_id=self.store_id
|
track_id=self.store_id
|
||||||
)
|
)
|
||||||
station = Station(station_id)
|
station = Station(station_id, station_name)
|
||||||
station.load_tracks()
|
station.load_tracks()
|
||||||
return station
|
return station
|
||||||
|
|
||||||
|
@ -285,7 +284,8 @@ class Station(object):
|
||||||
"""
|
"""
|
||||||
Model that represents specific station on Google Play Music.
|
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._id = station_id
|
||||||
self._tracks = []
|
self._tracks = []
|
||||||
self._tracks_loaded = False
|
self._tracks_loaded = False
|
||||||
|
@ -302,9 +302,12 @@ class Station(object):
|
||||||
Fetch tracks related to this station and
|
Fetch tracks related to this station and
|
||||||
populate it with :class:`Track` instances.
|
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 = Track.from_data(data, Track.SOURCE_STATION, many=True)
|
||||||
self._tracks_loaded = True
|
self._tracks_loaded = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
load_tracks_async = asynchronous(load_tracks)
|
||||||
|
|
||||||
def get_tracks(self):
|
def get_tracks(self):
|
||||||
"""
|
"""
|
||||||
|
@ -313,6 +316,20 @@ class Station(object):
|
||||||
assert self._tracks_loaded, 'Must call ".load_tracks()" before ".get_tracks()"'
|
assert self._tracks_loaded, 'Must call ".load_tracks()" before ".get_tracks()"'
|
||||||
return self._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):
|
class SearchResults(object):
|
||||||
"""
|
"""
|
||||||
|
@ -443,6 +460,7 @@ class _GP(object):
|
||||||
self.cached_tracks = None
|
self.cached_tracks = None
|
||||||
self.cached_liked_songs = LikedSongs()
|
self.cached_liked_songs = LikedSongs()
|
||||||
self.cached_playlists = None
|
self.cached_playlists = None
|
||||||
|
self.cached_stations = None
|
||||||
|
|
||||||
self.invalidate_caches()
|
self.invalidate_caches()
|
||||||
|
|
||||||
|
@ -475,10 +493,11 @@ class _GP(object):
|
||||||
|
|
||||||
def invalidate_caches(self):
|
def invalidate_caches(self):
|
||||||
"""
|
"""
|
||||||
Clear cached tracks & playlists.
|
Clear cached tracks & playlists & stations.
|
||||||
"""
|
"""
|
||||||
self.cached_tracks = None
|
self.cached_tracks = None
|
||||||
self.cached_playlists = None
|
self.cached_playlists = None
|
||||||
|
self.cached_stations = None
|
||||||
self.caches_invalidated.fire()
|
self.caches_invalidated.fire()
|
||||||
|
|
||||||
@synchronized
|
@synchronized
|
||||||
|
@ -548,6 +567,25 @@ class _GP(object):
|
||||||
|
|
||||||
get_stream_url_async = asynchronous(get_stream_url)
|
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
|
@synchronized
|
||||||
def get_all_user_playlist_contents(self, **_):
|
def get_all_user_playlist_contents(self, **_):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Predefined values.
|
Predefined values.
|
||||||
"""
|
"""
|
||||||
APP_NAME = 'Clay Player'
|
APP_NAME = 'Clay Player'
|
||||||
VERSION = '0.7.2'
|
VERSION = '1.0.0'
|
||||||
AUTHOR = "Andrew Dunai"
|
AUTHOR = "Andrew Dunai"
|
||||||
DESCRIPTION = "Awesome standalone command line player for Google Play Music"
|
DESCRIPTION = "Awesome standalone command line player for Google Play Music"
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@ class MyLibraryPage(urwid.Columns, AbstractPage):
|
||||||
if error:
|
if error:
|
||||||
notification_area.notify('Failed to load my library: {}'.format(str(error)))
|
notification_area.notify('Failed to load my library: {}'.format(str(error)))
|
||||||
return
|
return
|
||||||
# self.notification.close()
|
|
||||||
self.songlist.populate(tracks)
|
self.songlist.populate(tracks)
|
||||||
self.app.redraw()
|
self.app.redraw()
|
||||||
|
|
||||||
|
|
|
@ -97,8 +97,6 @@ class MyPlaylistListBox(urwid.ListBox):
|
||||||
)
|
)
|
||||||
items.append(myplaylistlistitem)
|
items.append(myplaylistlistitem)
|
||||||
|
|
||||||
# self.notification.close()
|
|
||||||
|
|
||||||
self.walker[:] = items
|
self.walker[:] = items
|
||||||
|
|
||||||
self.app.redraw()
|
self.app.redraw()
|
||||||
|
|
160
clay/pages/mystations.py
Normal file
160
clay/pages/mystations.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
"""
|
||||||
|
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' \u2708 {} '.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
|
|
@ -18,7 +18,7 @@ class QueuePage(urwid.Columns, AbstractPage):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self):
|
def key(self):
|
||||||
return 3
|
return 4
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
|
@ -58,7 +58,7 @@ class SearchPage(urwid.Pile, AbstractPage):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self):
|
def key(self):
|
||||||
return 4
|
return 5
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
|
@ -17,11 +17,11 @@ class Slider(urwid.Widget):
|
||||||
_sizing = frozenset([urwid.FLOW])
|
_sizing = frozenset([urwid.FLOW])
|
||||||
|
|
||||||
CHARS = [
|
CHARS = [
|
||||||
# '_',
|
u'\u2584',
|
||||||
u'\u2581',
|
u'\u25A0',
|
||||||
u'\u2500',
|
u'\u2580',
|
||||||
u'\u2594'
|
|
||||||
]
|
]
|
||||||
|
ZERO_CHAR = u'\u2500'
|
||||||
|
|
||||||
def selectable(self):
|
def selectable(self):
|
||||||
return True
|
return True
|
||||||
|
@ -36,7 +36,7 @@ class Slider(urwid.Widget):
|
||||||
else:
|
else:
|
||||||
self.freq_str = str(freq) + '\nHz'
|
self.freq_str = str(freq) + '\nHz'
|
||||||
self.value = 0
|
self.value = 0
|
||||||
self.slider_height = 5
|
self.slider_height = 13
|
||||||
self.max_value = 20
|
self.max_value = 20
|
||||||
super(Slider, self).__init__()
|
super(Slider, self).__init__()
|
||||||
|
|
||||||
|
@ -50,16 +50,18 @@ class Slider(urwid.Widget):
|
||||||
"""
|
"""
|
||||||
Render 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
|
if self.value == 0:
|
||||||
section_index = int(k * self.slider_height)
|
chars[self.slider_height // 2] = Slider.ZERO_CHAR
|
||||||
char_index = int(k * self.slider_height * len(Slider.CHARS)) % len(Slider.CHARS)
|
else:
|
||||||
chars[section_index] = Slider.CHARS[char_index]
|
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([
|
rows.extend([
|
||||||
(
|
(
|
||||||
u'\u2524{}\u251C'
|
u'\u2524{}\u251C'
|
||||||
|
@ -139,17 +141,17 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.username = urwid.Edit(
|
self.username = urwid.Edit(
|
||||||
edit_text=settings.get('username', 'play_settings')
|
edit_text=settings.get('username', 'play_settings') or ''
|
||||||
)
|
)
|
||||||
self.password = urwid.Edit(
|
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(
|
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(
|
self.download_tracks = urwid.CheckBox(
|
||||||
'Download tracks before playback',
|
'Download tracks before playback',
|
||||||
state=settings.get('download_tracks', 'play_settings')
|
state=settings.get('download_tracks', 'play_settings') or False
|
||||||
)
|
)
|
||||||
self.equalizer = Equalizer()
|
self.equalizer = Equalizer()
|
||||||
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
||||||
|
@ -178,6 +180,8 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
||||||
Called when "Save" button is pressed.
|
Called when "Save" button is pressed.
|
||||||
"""
|
"""
|
||||||
with settings.edit() as config:
|
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']['username'] = self.username.edit_text
|
||||||
config['play_settings']['password'] = self.password.edit_text
|
config['play_settings']['password'] = self.password.edit_text
|
||||||
config['play_settings']['device_id'] = self.device_id.edit_text
|
config['play_settings']['device_id'] = self.device_id.edit_text
|
||||||
|
|
|
@ -121,10 +121,10 @@ class _Settings(object):
|
||||||
section = self.get_section(*sections)
|
section = self.get_section(*sections)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return section[key]
|
return section.get(key)
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
section = self.get_default_config_section(*sections)
|
section = self.get_default_config_section(*sections)
|
||||||
return section[key]
|
return section.get(key)
|
||||||
|
|
||||||
def _get_section(self, config, *sections):
|
def _get_section(self, config, *sections):
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
|
|
|
@ -9,7 +9,7 @@ try:
|
||||||
# Python 3.x
|
# Python 3.x
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Python 2.3
|
# Python 2.x
|
||||||
from string import letters as ascii_letters
|
from string import letters as ascii_letters
|
||||||
import urwid
|
import urwid
|
||||||
from clay.notifications import notification_area
|
from clay.notifications import notification_area
|
||||||
|
|
Loading…
Add table
Reference in a new issue