diff --git a/app.py b/app.py index d89123d..74ecc4c 100755 --- a/app.py +++ b/app.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- import sys @@ -43,16 +44,16 @@ PALETTE = [ class PlayProgress(urwid.ProgressBar): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(PlayProgress, self).__init__(*args, **kwargs) self.track = None def get_text(self): if self.track is None: - return 'Idle' + return u'Idle' progress = player.get_play_progress_seconds() total = player.get_length_seconds() - return ' {} {} - {} [{:02d}:{:02d} / {:02d}:{:02d}] {} {}'.format( - '>' if player.is_playing else 'X', + return u' {} {} - {} [{:02d}:{:02d} / {:02d}:{:02d}] {} {}'.format( + u'|>' if player.is_playing else u'||', # '\u25B6' if player.is_playing else '\u25A0', self.track.artist, self.track.title, @@ -60,11 +61,11 @@ class PlayProgress(urwid.ProgressBar): progress % 60, total // 60, total % 60, - 'S' if player.get_is_random() else ' ', - 'R' if player.get_is_repeat_one() else ' ' + u'S' if player.get_is_random() else ' ', + u'R' if player.get_is_repeat_one() else ' ' # '⋍' if player.get_is_random() else ' ', # '⟲' if player.get_is_repeat_one() else ' ' - ).encode('utf-8') + ) def set_track(self, track): self.track = track @@ -78,7 +79,7 @@ class AppWidget(urwid.Frame): def __init__(self, page_class): self.page_class = page_class # self.attrwrap = urwid.AttrWrap(urwid.Text(), 'panel') - super().__init__( + super(AppWidget.Tab, self).__init__( self.get_title() ) @@ -133,7 +134,7 @@ class AppWidget(urwid.Frame): self.seekbar ]) self.current_page = StartUp(self) - super().__init__( + super(AppWidget, self).__init__( header=self.header, footer=self.panel, body=self.current_page @@ -207,7 +208,7 @@ class AppWidget(urwid.Frame): elif key == 'ctrl q': sys.exit(0) else: - super().keypress(size, key) + super(AppWidget, self).keypress(size, key) def main(): diff --git a/gp.py b/gp.py index a61c8cd..575a956 100644 --- a/gp.py +++ b/gp.py @@ -1,6 +1,8 @@ from gmusicapi.clients import Mobileclient from threading import Thread, Lock +gp = None + def async(fn): def wrapper(*args, **kwargs): @@ -12,6 +14,7 @@ def async(fn): result = fn(*args, **kwargs) except Exception as e: callback(None, e, **extra) + raise else: callback(result, None, **extra) @@ -32,13 +35,68 @@ def synchronized(fn): return wrapper +class Track(object): + def __init__(self, id, title, artist, duration): + self.id = id + self.title = title + self.artist = artist + self.duration = duration + + @classmethod + def from_data(cls, data, many=False): + if many: + return [cls.from_data(one) for one in data] + + return Track( + id=data['id'], + title=data['title'], + artist=data['artist'], + duration=int(data['durationMillis']) + ) + + def get_url(self, callback): + gp.get_stream_url(self.id, callback=callback, extra=dict(track=self)) + + +class Playlist(object): + def __init__(self, id, name, tracks): + self.id = id + self.name = name + self.tracks = tracks + + @classmethod + def from_data(cls, data, many=False): + if many: + return [cls.from_data(one) for one in data] + + return Playlist( + id=data['id'], + name=data['name'], + tracks=cls.playlist_items_to_tracks(data['tracks']) + ) + + @classmethod + def playlist_items_to_tracks(self, playlist_tracks): + results = [] + cached_tracks_map = gp.get_cached_tracks_map() + for playlist_track in playlist_tracks: + if 'track' in playlist_track: + track = dict(playlist_track['track']) + track['id'] = playlist_track['trackId'] + track = Track.from_data(track) + else: + track = cached_tracks_map[playlist_track['trackId']] + results.append(track) + return results + + class GP(object): def __init__(self): self.mc = Mobileclient() self.invalidate_caches() def invalidate_caches(self): - self.cached_songs = None + self.cached_tracks = None self.cached_playlists = None @async @@ -51,11 +109,11 @@ class GP(object): @async @synchronized - def get_all_songs(self): - if self.cached_songs: - return self.cached_songs - self.cached_songs = self.mc.get_all_songs() - return self.cached_songs + def get_all_tracks(self): + if self.cached_tracks: + return self.cached_tracks + self.cached_tracks = Track.from_data(self.mc.get_all_songs(), True) + return self.cached_tracks @async def get_stream_url(self, id): @@ -66,18 +124,18 @@ class GP(object): def get_all_user_playlist_contents(self): if self.cached_playlists: return self.cached_playlists - if not self.cached_songs: - self.cached_songs = self.mc.get_all_songs() - cached_songs_map = {track['id']: track for track in self.cached_songs} - self.cached_playlists = self.mc.get_all_user_playlist_contents() - for playlist in self.cached_playlists: - for song in playlist['tracks']: - if 'track' not in song: - song['track'] = cached_songs_map[song['trackId']] - else: - song['track']['id'] = song['trackId'] + if not self.cached_tracks: + self.cached_tracks = self.mc.get_all_tracks() + + self.cached_playlists = Playlist.from_data( + self.mc.get_all_user_playlist_contents(), + True + ) return self.cached_playlists + def get_cached_tracks_map(self): + return {track.id: track for track in self.cached_tracks} + gp = GP() diff --git a/mylibrary.py b/mylibrary.py index f6ba093..7d7b3a4 100644 --- a/mylibrary.py +++ b/mylibrary.py @@ -1,7 +1,6 @@ import urwid from gp import gp from songlist import SongListBox -from player import Track class MyLibrary(urwid.Columns): @@ -12,16 +11,16 @@ class MyLibrary(urwid.Columns): self.app = app self.songlist = SongListBox(app) - gp.get_all_songs(callback=self.on_get_all_songs) + gp.get_all_tracks(callback=self.on_get_all_songs) - return super().__init__([ + return super(MyLibrary, self).__init__([ self.songlist ]) - def on_get_all_songs(self, results, error): + def on_get_all_songs(self, tracks, error): if error: self.app.set_page('Error', error) return - self.songlist.populate(Track.from_data(results, many=True)) + self.songlist.populate(tracks) self.app.redraw() diff --git a/myplaylists.py b/myplaylists.py index 1b82eb3..6313895 100644 --- a/myplaylists.py +++ b/myplaylists.py @@ -1,17 +1,17 @@ +# -*- coding: utf-8 -*- import urwid from gp import gp -from player import Track from songlist import SongListBox -class PlaylistListItem(urwid.Columns): +class MyPlaylistListItem(urwid.Columns): signals = ['activate'] - def __init__(self, data): - self.data = data + def __init__(self, playlist): + self.playlist = playlist self.text = urwid.SelectableIcon(' ☰ {} ({})'.format( - self.data['name'], - len(self.data['tracks']) + self.playlist.name, + len(self.playlist.tracks) ), cursor_position=3) self.text.set_layout('left', 'clip', None) self.content = urwid.AttrWrap( @@ -28,14 +28,10 @@ class PlaylistListItem(urwid.Columns): return super().keypress(size, key) def get_tracks(self): - return Track.from_data([ - item['track'] - for item - in self.data['tracks'] - ], many=True) + return self.playlist.tracks -class PlaylistListBox(urwid.ListBox): +class MyPlaylistListBox(urwid.ListBox): signals = ['activate'] def __init__(self, app): @@ -59,18 +55,18 @@ class PlaylistListBox(urwid.ListBox): items = [] for playlist in playlists: - playlistlistitem = PlaylistListItem(playlist) + myplaylistlistitem = MyPlaylistListItem(playlist) urwid.connect_signal( - playlistlistitem, 'activate', self.item_activated + myplaylistlistitem, 'activate', self.item_activated ) - items.append(playlistlistitem) + items.append(myplaylistlistitem) self.walker[:] = items self.app.redraw() - def item_activated(self, playlistlistitem): - urwid.emit_signal(self, 'activate', playlistlistitem) + def item_activated(self, myplaylistlistitem): + urwid.emit_signal(self, 'activate', myplaylistlistitem) class MyPlaylists(urwid.Columns): @@ -80,21 +76,21 @@ class MyPlaylists(urwid.Columns): def __init__(self, app): self.app = app - self.playlistlist = PlaylistListBox(app) + self.myplaylistlist = MyPlaylistListBox(app) self.songlist = SongListBox(app) self.songlist.populate([]) urwid.connect_signal( - self.playlistlist, 'activate', self.playlistlistitem_activated + self.myplaylistlist, 'activate', self.myplaylistlistitem_activated ) return super().__init__([ - self.playlistlist, + self.myplaylistlist, self.songlist ]) - def playlistlistitem_activated(self, playlistlistitem): + def myplaylistlistitem_activated(self, myplaylistlistitem): self.songlist.populate( - playlistlistitem.get_tracks() + myplaylistlistitem.get_tracks() ) diff --git a/pages.py b/pages.py index 1943014..cf39310 100644 --- a/pages.py +++ b/pages.py @@ -22,7 +22,7 @@ class StartUp(urwid.Filler): 'Please set your credentials on the settings page.' ) - super().__init__( + super(StartUp, self).__init__( urwid.Pile([ urwid.Padding( urwid.AttrWrap(urwid.BigText( @@ -59,7 +59,7 @@ class StartUp(urwid.Filler): class Error(urwid.Filler): def __init__(self, app, error): - super().__init__( + super(Error, self).__init__( urwid.Text('Error:\n\n{}'.format(str(error))), valign='top' ) diff --git a/player.py b/player.py index e7b69f3..e372b5a 100644 --- a/player.py +++ b/player.py @@ -5,34 +5,9 @@ import json import vlc from eventhook import EventHook -import gp -from gp import gp -class Track(object): - def __init__(self, id, title, artist, duration): - self.id = id - self.title = title - self.artist = artist - self.duration = duration - - @classmethod - def from_data(cls, data, many=False): - if many: - return [cls.from_data(one) for one in data] - - return Track( - id=data['id'], - title=data['title'], - artist=data['artist'], - duration=int(data['durationMillis']) - ) - - def get_url(self, callback): - gp.get_stream_url(self.id, callback=callback, extra=dict(track=self)) - - -class Playlist(object): +class Queue(object): def __init__(self): self.random = False self.repeat_one = False @@ -109,10 +84,10 @@ class Player(object): self._media_position_changed ) - self.playlist = Playlist() + self.queue = Queue() def broadcast_state(self): - track = self.playlist.get_current_track() + track = self.queue.get_current_track() if track is None: data = dict( playing=False, @@ -145,29 +120,29 @@ class Player(object): self.get_play_progress() ) - def load_playlist(self, data, current_index): - self.playlist.load(data, current_index) + def load_queue(self, data, current_index): + self.queue.load(data, current_index) self._play() def get_is_random(self): - return self.playlist.random + return self.queue.random def get_is_repeat_one(self): - return self.playlist.repeat_one + return self.queue.repeat_one def set_random(self, value): - self.playlist.random = value + self.queue.random = value self.playback_flags_changed.fire() def set_repeat_one(self, value): - self.playlist.repeat_one = value + self.queue.repeat_one = value self.playback_flags_changed.fire() def get_queue(self): - return self.playlist.get_tracks() + return self.queue.get_tracks() def _play(self): - track = self.playlist.get_current_track() + track = self.queue.get_current_track() if track is None: return track.get_url(callback=self._play_ready) @@ -200,14 +175,14 @@ class Player(object): return int(self.mp.get_length() // 1000) def next(self, force=False): - self.playlist.next(force) + self.queue.next(force) self._play() def get_current_track(self): - return self.playlist.get_current_track() + return self.queue.get_current_track() # def prev(self): - # self.playlist.prev() + # self.queue.prev() # self._play() def seek(self, delta): diff --git a/settings.py b/settings.py index 0ed565b..252bb32 100644 --- a/settings.py +++ b/settings.py @@ -1,4 +1,5 @@ import os +import errno import yaml import appdirs @@ -51,7 +52,13 @@ class Settings(urwid.Columns): @classmethod def get_config_filename(cls): filedir = appdirs.user_config_dir('clay', 'Clay') - os.makedirs(filedir, exist_ok=True) + + try: + os.makedirs(filedir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + path = os.path.join(filedir, 'config.json') if not os.path.exists(path): with open(path, 'w') as f: diff --git a/songlist.py b/songlist.py index 34e9ea7..d9923ea 100644 --- a/songlist.py +++ b/songlist.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import urwid from player import player @@ -12,9 +13,9 @@ class SongListItem(urwid.Pile): STATE_ICONS = { 0: ' ', - 1: '\u2505', - 2: '\u25B6', - 3: '\u25A0' + 1: u'\u2505', + 2: u'\u25B6', + 3: u'\u25A0' } def __init__(self, track, index): @@ -36,7 +37,7 @@ class SongListItem(urwid.Pile): 'line1_focus' ) - super().__init__([ + super(SongListItem, self).__init__([ self.content ]) self.update_text() @@ -50,8 +51,8 @@ class SongListItem(urwid.Pile): def update_text(self): self.line1.set_text( - '{index:3d} {icon} {title} [{minutes:02d}:{seconds:02d}]'.format( - index=self.index, + u'{index:3d} {icon} {title} [{minutes:02d}:{seconds:02d}]'.format( + index=self.index + 1, icon=self.get_state_icon(self.state), title=self.track.title, minutes=self.track.duration // (1000 * 60), @@ -59,7 +60,7 @@ class SongListItem(urwid.Pile): ) ) self.line2.set_text( - ' {}'.format(self.track.artist) + u' {}'.format(self.track.artist) ) if self.state == SongListItem.STATE_IDLE: self.content.set_attr('line1') @@ -72,7 +73,7 @@ class SongListItem(urwid.Pile): if key == 'enter': urwid.emit_signal(self, 'activate', self) return - return super().keypress(size, key) + return super(SongListItem, self).keypress(size, key) # def render(self, size, focus=False): # # if focus: @@ -99,7 +100,7 @@ class SongListBox(urwid.ListBox): player.track_changed += self.track_changed player.media_state_changed += self.media_state_changed - return super().__init__(self.walker) + return super(SongListBox, self).__init__(self.walker) def tracks_to_songlist(self, tracks): current_track = player.get_current_track() @@ -113,7 +114,7 @@ class SongListBox(urwid.ListBox): return items def item_activated(self, songitem): - player.load_playlist(self.tracks, songitem.index) + player.load_queue(self.tracks, songitem.index) def track_changed(self, track): for songitem in self.walker: