This commit is contained in:
Andrew Dunai 2017-12-28 15:09:40 +02:00
parent 4b4e90be12
commit 38f4691a50
17 changed files with 8228 additions and 0 deletions

6
Makefile Normal file
View file

@ -0,0 +1,6 @@
build:
docker build -t clay .
run: | build
docker run -it clay

93
README.md Normal file
View file

@ -0,0 +1,93 @@
# Clay [alpha]
Standalone command line player for Google Play Music.
This app wouldn't be possible without the wonderful [gmusicapi] and [VLC] libraries.
This project is neither affiliated nor endorsed by Google.
It's being actively developed, but is still in the early alpha version, so many features are missing and/or may be bugged.
![Screenshot](./images/clay.png)
# Requirements
- Python 3.x (native)
- [gmusicapi] (PYPI)
- [urwid] (PYPI)
- [pyyaml] (PYPI)
- lib[vlc] (native, distributed with VLC player)
# What works
- Playback
- Music library
- Playlists
- Queue
- Configuration
- Caching (partially)
- Basic error handling
# What is developer
- Search
- Add to playlist
- Like/dislike
- Caching
- Other functionality that is supported by [gmusicapi]
# Installation
0. Install Python 3 and VLC.
## Method 1 (automatic)
1. Source the 'activate.sh' script. It will initialize the Python virtual env and install the dependencies:
$ source activate.sh
2. Run the player:
$ ./app.py
## Method 2 (manual)
1. Create & activate virtualenv:
$ virtualenv .env
$ source .env/bin/activate
2. Install the requirements:
$ pip install -r requirements.txt
3. Run the player:
$ ./app.py
# Configuration
In order to use this app, you need to know your Device ID. Typically gmusicapi should display possible IDs once you type a wrong one.
Also be aware that this app has not been tested with 2FA yet.
# Controls
- `<UP|DOWN|LEFT|RIGHT>` - nagivate around
- `<ALT> + 1..9` - switch active tab
- `<ENTER>` - play selected track
- `<CTRL> w` - play/pause
- `<CTRL> e` - play next song
- `<SHIFT> <LEFT|RIGHT>` - seek backward/forward by 5% of the song duration
- `<CTRL> s` - toggle shuffle
- `<CTRL> r` - toggle song repeat
# Credits
Made by Andrew Dunai.
Regards to [gmusicapi] and [VLC] who made this possible.
[gmusicapi]: https://github.com/simon-weber/gmusicapi
[VLC]: https://wiki.videolan.org/python_bindings
[urwid]: urwid.org/
[pyyaml]: https://github.com/yaml/pyyaml

10
activate.sh Normal file
View file

@ -0,0 +1,10 @@
#!/bin/bash
if ! [[ -d ".env" ]]
then
virtualenv .env
fi
. .env/bin/activate
pip install -r requirements.txt

214
app.py Executable file
View file

@ -0,0 +1,214 @@
#!/usr/bin/env python3
from collections import OrderedDict
import urwid
from player import player
from pages import StartUp, Error
from mylibrary import MyLibrary
from myplaylists import MyPlaylists
from playerqueue import Queue
from settings import Settings
loop = None
PALETTE = [
('logo', '', '', '', '#F54', ''),
('bg', '', '', '', '#FFF', '#222'),
('primary', '', '', '', '#F54', '#FFF'),
('secondary', '', '', '', '#17F', '#FFF'),
('selected', '', '', '', '#FFF', '#444'),
('primary_inv', '', '', '', '#FFF', '#17F'),
('secondary_inv', '', '', '', '#FFF', '#F17'),
('progress', '', '', '', '#FFF', '#F54'),
('progress_remaining', '', '', '', '#FFF', '#444'),
('panel', '', '', '', '#FFF', '#222'),
('panel_focus', '', '', '', '#FFF', '#F54'),
('line1', '', '', '', '#FFF', ''),
('line1_focus', '', '', '', '#FFF', '#444'),
('line1_active', '', '', '', '#F54', ''),
('line1_active_focus', '', '', '', '#F54', '#444'),
('line2', '', '', '', '#AAA', ''),
('line2_focus', '', '', '', '#AAA', '#444'),
('input', '', '', '', '#FFF', '#444'),
('input_focus', '', '', '', '#FFF', '#F54'),
]
class PlayProgress(urwid.ProgressBar):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.track = None
def get_text(self):
if self.track is None:
return 'Idle'
progress = player.get_play_progress_seconds()
total = player.get_length_seconds()
return ' {} - {} [{:02d}:{:02d} / {:02d}:{:02d}] {} {}'.format(
self.track.artist,
self.track.title,
progress // 60,
progress % 60,
total // 60,
total % 60,
'' if player.get_is_random() else ' ',
'' if player.get_is_repeat_one() else ' ',
)
def set_track(self, track):
self.track = track
def update(self):
self._invalidate()
class AppWidget(urwid.Frame):
class Tab(urwid.Text):
def __init__(self, page_class):
self.page_class = page_class
# self.attrwrap = urwid.AttrWrap(urwid.Text(), 'panel')
super().__init__(
self.get_title()
)
def set_active(self, active):
self.set_text(
[('panel_focus' if active else 'panel', self.get_title())]
)
def get_title(self):
return ' {} {} '.format(
self.page_class.key,
self.page_class.name
)
def __init__(self):
self.pages = [
StartUp,
Error,
MyLibrary,
MyPlaylists,
Queue,
Settings
]
self.tabs = [
AppWidget.Tab(page_class)
for page_class
in [
MyLibrary,
MyPlaylists,
Queue,
Settings
]
]
print(self.tabs)
self.header = urwid.Pile([
# urwid.Divider('\u2500'),
urwid.AttrWrap(urwid.Columns([
('pack', tab)
for tab
in self.tabs
], dividechars=1), 'panel'),
# urwid.Divider('\u2500')
])
self.seekbar = PlayProgress(
'progress_remaining',
'progress',
current=0,
done=100,
satt='bg'
)
self.panel = urwid.Pile([
urwid.Divider('\u2500'),
self.seekbar
])
self.current_page = StartUp(self)
super().__init__(
header=self.header,
footer=self.panel,
body=self.current_page
)
player.media_position_changed += self.media_position_changed
player.track_changed += self.track_changed
player.playback_flags_changed += self.playback_flags_changed
def media_position_changed(self, progress):
if progress < 0:
progress = 0
self.seekbar.set_completion(progress * 100)
loop.draw_screen()
# sleep(0.2)
# self.set_page(MyLibrary())
def track_changed(self, track):
self.seekbar.set_track(track)
def playback_flags_changed(self):
self.seekbar.update()
def set_page(self, page_class, *args):
if isinstance(page_class, str):
page_class = [
page
for page
in self.pages
if page.__name__ == page_class
][0]
self.current_page = page_class(self, *args)
self.contents['body'] = (self.current_page, None)
for tab in self.tabs:
tab.set_active(False)
if tab.page_class == page_class:
tab.set_active(True)
self.redraw()
def redraw(self):
if loop:
loop.draw_screen()
def keypress(self, size, key):
if isinstance(self.current_page, StartUp):
return
for tab in self.tabs:
if 'meta {}'.format(tab.page_class.key) == key:
self.set_page(tab.page_class)
return
if key == 'ctrl w':
player.play_pause()
elif key == 'ctrl e':
player.next(True)
elif key == 'shift right':
player.seek(0.05)
elif key == 'shift left':
player.seek(-0.05)
elif key == 'ctrl s':
player.set_random(not player.get_is_random())
elif key == 'ctrl r':
player.set_repeat_one(not player.get_is_repeat_one())
else:
super().keypress(size, key)
def main():
global loop
app_widget = AppWidget()
loop = urwid.MainLoop(app_widget, PALETTE)
loop.screen.set_terminal_properties(256)
loop.run()
if __name__ == '__main__':
main()

16
eventhook.py Normal file
View file

@ -0,0 +1,16 @@
class EventHook(object):
def __init__(self):
self.event_handlers = []
def __iadd__(self, handler):
self.event_handlers.append(handler)
return self
def __isub__(self, handler):
self.event_handlers.remove(handler)
return self
def fire(self, *args, **kwargs):
for handler in self.event_handlers:
handler(*args, **kwargs)

83
gp.py Normal file
View file

@ -0,0 +1,83 @@
from gmusicapi.clients import Mobileclient
from threading import Thread, Lock
def async(fn):
def wrapper(*args, **kwargs):
callback = kwargs.pop('callback')
extra = kwargs.pop('extra', dict())
def process():
try:
result = fn(*args, **kwargs)
except Exception as e:
callback(None, e, **extra)
else:
callback(result, None, **extra)
Thread(target=process).start()
return wrapper
def synchronized(fn):
lock = Lock()
def wrapper(*args, **kwargs):
try:
lock.acquire()
return fn(*args, **kwargs)
finally:
lock.release()
return wrapper
class GP(object):
def __init__(self):
self.mc = Mobileclient()
self.invalidate_caches()
def invalidate_caches(self):
self.cached_songs = None
self.cached_playlists = None
@async
@synchronized
def login(self, email, password, device_id):
self.mc.logout()
self.invalidate_caches()
# TODO: Move device_id to settings
return self.mc.login(email, password, device_id)
@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
@async
def get_stream_url(self, id):
return self.mc.get_stream_url(id)
@async
@synchronized
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']
return self.cached_playlists
gp = GP()

BIN
images/clay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

2
meta.py Normal file
View file

@ -0,0 +1,2 @@
VERSION = '0.1b'

27
mylibrary.py Normal file
View file

@ -0,0 +1,27 @@
import urwid
from gp import gp
from songlist import SongListBox
from player import Track
class MyLibrary(urwid.Columns):
name = 'Library'
key = 1
def __init__(self, app):
self.app = app
self.songlist = SongListBox(app)
gp.get_all_songs(callback=self.on_get_all_songs)
return super().__init__([
self.songlist
])
def on_get_all_songs(self, results, error):
if error:
self.app.set_page('Error', error)
return
self.songlist.populate(Track.from_data(results, many=True))
self.app.redraw()

98
myplaylists.py Normal file
View file

@ -0,0 +1,98 @@
import urwid
from gp import gp
from player import Track
from songlist import SongListBox
class PlaylistListItem(urwid.Columns):
signals = ['activate']
def __init__(self, data):
self.data = data
self.content = urwid.AttrWrap(
urwid.SelectableIcon('{} ({})'.format(
self.data['name'],
len(self.data['tracks'])
), cursor_position=3),
'default',
'selected'
)
super().__init__([self.content])
def keypress(self, size, key):
if key == 'enter':
urwid.emit_signal(self, 'activate', self)
return
return super().keypress(size, key)
def get_tracks(self):
return Track.from_data([
item['track']
for item
in self.data['tracks']
], many=True)
class PlaylistListBox(urwid.ListBox):
signals = ['activate']
def __init__(self, app):
self.app = app
self.walker = urwid.SimpleListWalker([
urwid.Text('Loading playlists...')
])
gp.get_all_user_playlist_contents(callback=self.on_get_playlists)
super().__init__(self.walker)
def on_get_playlists(self, playlists, error):
if error:
self.app.set_page(
'Error',
str(error)
)
return
items = []
for playlist in playlists:
playlistlistitem = PlaylistListItem(playlist)
urwid.connect_signal(
playlistlistitem, 'activate', self.item_activated
)
items.append(playlistlistitem)
self.walker[:] = items
self.app.redraw()
def item_activated(self, playlistlistitem):
urwid.emit_signal(self, 'activate', playlistlistitem)
class MyPlaylists(urwid.Columns):
name = 'Playlists'
key = 2
def __init__(self, app):
self.app = app
self.playlistlist = PlaylistListBox(app)
self.songlist = SongListBox(app)
self.songlist.populate([])
urwid.connect_signal(
self.playlistlist, 'activate', self.playlistlistitem_activated
)
return super().__init__([
self.playlistlist,
self.songlist
])
def playlistlistitem_activated(self, playlistlistitem):
self.songlist.populate(
playlistlistitem.get_tracks()
)

66
pages.py Normal file
View file

@ -0,0 +1,66 @@
import urwid
from settings import Settings
from gp import gp
from meta import VERSION
class StartUp(urwid.Filler):
def __init__(self, app):
self.app = app
if Settings.is_config_valid():
config = Settings.get_config()
gp.login(
config['username'],
config['password'],
config['device_id'],
callback=self.on_login
)
else:
self.app.set_page(
'Error',
'Please set your credentials on the settings page.'
)
super().__init__(
urwid.Pile([
urwid.Padding(
urwid.AttrWrap(urwid.BigText(
'Clay'.format(VERSION),
urwid.font.HalfBlock5x4Font()
), 'logo'),
'center',
None
),
urwid.AttrWrap(urwid.Text('Version {}'.format(VERSION), align='center'), 'line1'),
urwid.AttrWrap(urwid.Text('Authorizing...', align='center'), 'line2')
])
# urwid.Text('Loading...'),
# valign='top'
)
def on_login(self, success, error):
if error:
self.app.set_page(
'Error',
'Failed to log in: {}'.format(str(error))
)
return
if not success:
self.app.set_page(
'Error',
'Google Play Music login failed '
'(API returned false)'
)
return
self.app.set_page('MyLibrary')
class Error(urwid.Filler):
def __init__(self, app, error):
super().__init__(
urwid.Text('Error:\n\n{}'.format(str(error))),
valign='top'
)

218
player.py Normal file
View file

@ -0,0 +1,218 @@
from random import randint
# import dbus
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):
def __init__(self):
self.random = False
self.repeat_one = False
self.tracks = []
self.current_track = None
def load(self, tracks, current_track=None):
self.tracks = tracks[:]
if current_track is None and len(self.tracks):
current_track = self.tracks[0]
self.current_track = current_track
def get_current_track(self):
if self.current_track is None:
return None
return self.tracks[self.current_track]
def next(self, force=False):
if self.current_track is None:
if not len(self.tracks):
return None
self.current_track = self.tracks[0]
if self.repeat_one and not force:
return self.get_current_track()
if self.random:
self.current_track = randint(0, len(self.tracks) - 1)
return self.get_current_track()
self.current_track += 1
if (self.current_track + 1) >= len(self.tracks):
self.current_track = 0
return self.get_current_track()
def get_tracks(self):
return self.tracks
class Player(object):
media_position_changed = EventHook()
media_state_changed = EventHook()
track_changed = EventHook()
playback_flags_changed = EventHook()
def __init__(self):
self.mp = vlc.MediaPlayer()
# self.bus = dbus.SessionBus()
# try:
# self.obj = self.bus.get_object('org.awesomewm.awful', '/org/dunai/clay')
# except dbus.DBusException as e:
# print(e)
# self.interface = None
# else:
# self.interface = dbus.Interface(self.obj, 'org.dunai.clay')
# self.statusfile = open('/tmp/clay.json', 'w')
self.mp.event_manager().event_attach(
vlc.EventType.MediaPlayerPlaying,
self._media_state_changed
)
self.mp.event_manager().event_attach(
vlc.EventType.MediaPlayerPaused,
self._media_state_changed
)
self.mp.event_manager().event_attach(
vlc.EventType.MediaPlayerEndReached,
self._media_end_reached
)
self.mp.event_manager().event_attach(
vlc.EventType.MediaPlayerPositionChanged,
self._media_position_changed
)
self.playlist = Playlist()
def broadcast_state(self):
track = self.playlist.get_current_track()
if track is None:
data = dict(
playing=False,
artist=None,
title=None,
progress=None,
length=None
)
else:
data = dict(
playing=self.is_playing,
artist=track.artist,
title=track.title,
progress=self.get_play_progress_seconds(),
length=self.get_length_seconds()
)
with open('/tmp/clay.json', 'w') as f:
f.write(json.dumps(data, indent=4))
def _media_state_changed(self, e):
self.broadcast_state()
self.media_state_changed.fire(self.is_playing)
def _media_end_reached(self, e):
self.next()
def _media_position_changed(self, e):
self.broadcast_state()
self.media_position_changed.fire(
self.get_play_progress()
)
def load_playlist(self, data, current_index):
self.playlist.load(data, current_index)
self._play()
def get_is_random(self):
return self.playlist.random
def get_is_repeat_one(self):
return self.playlist.repeat_one
def set_random(self, value):
self.playlist.random = value
self.playback_flags_changed.fire()
def set_repeat_one(self, value):
self.playlist.repeat_one = value
self.playback_flags_changed.fire()
def get_queue(self):
return self.playlist.get_tracks()
def _play(self):
track = self.playlist.get_current_track()
if track is None:
return
track.get_url(callback=self._play_ready)
self.broadcast_state()
self.track_changed.fire(track)
def _play_ready(self, url, error, track):
if error:
raise error
self.mp.set_media(vlc.Media(url))
self.mp.play()
@property
def is_playing(self):
return self.mp.get_state() == vlc.State.Playing
def play_pause(self):
if self.is_playing:
self.mp.pause()
else:
self.mp.play()
def get_play_progress(self):
return self.mp.get_position()
def get_play_progress_seconds(self):
return int(self.mp.get_position() * self.mp.get_length() / 1000)
def get_length_seconds(self):
return int(self.mp.get_length() // 1000)
def next(self, force=False):
self.playlist.next(force)
self._play()
def get_current_track(self):
return self.playlist.get_current_track()
# def prev(self):
# self.playlist.prev()
# self._play()
def seek(self, delta):
self.mp.set_position(self.get_play_progress() + delta)
player = Player()

19
playerqueue.py Normal file
View file

@ -0,0 +1,19 @@
import urwid
from songlist import SongListBox
from player import player
class Queue(urwid.Columns):
name = 'Queue'
key = 3
def __init__(self, app):
self.app = app
self.songlist = SongListBox(app)
self.songlist.populate(player.get_queue())
return super().__init__([
self.songlist
])

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
gmusicapi==10.1.2
PyYAML==3.12
urwid==1.3.1

81
settings.py Normal file
View file

@ -0,0 +1,81 @@
import os
import yaml
import appdirs
import urwid
class Settings(urwid.Columns):
name = 'Settings'
key = 9
def __init__(self, app):
self.app = app
config = self.__class__.get_config()
self.username = urwid.Edit(
edit_text=config.get('username', '')
)
self.password = urwid.Edit(
mask='*', edit_text=config.get('password', '')
)
self.device_id = urwid.Edit(
edit_text=config.get('device_id', '')
)
return super().__init__([urwid.ListBox(urwid.SimpleListWalker([
urwid.Text('Settings'),
urwid.Divider(' '),
urwid.Text('Username'),
urwid.AttrWrap(self.username, 'input', 'input_focus'),
urwid.Divider(' '),
urwid.Text('Password'),
urwid.AttrWrap(self.password, 'input', 'input_focus'),
urwid.Divider(' '),
urwid.Text('Device ID'),
urwid.AttrWrap(self.device_id, 'input', 'input_focus'),
urwid.Divider(' '),
urwid.AttrWrap(urwid.Button(
'Save', on_press=self.on_save
), 'input', 'input_focus')
]))])
def on_save(self, button):
self.__class__.set_config(dict(
username=self.username.edit_text,
password=self.password.edit_text,
device_id=self.device_id.edit_text
))
self.app.set_page('StartUp')
# self.app.set_page('MyLibrary')
@classmethod
def get_config_filename(cls):
filedir = appdirs.user_config_dir('clay', 'Clay')
os.makedirs(filedir, exist_ok=True)
path = os.path.join(filedir, 'config.json')
if not os.path.exists(path):
with open(path, 'w') as f:
f.write('{}')
return path
@classmethod
def get_config(cls):
with open(cls.get_config_filename(), 'r') as f:
return yaml.load(f.read())
@classmethod
def set_config(cls, new_config):
config = cls.get_config()
config.update(new_config)
with open(cls.get_config_filename(), 'w') as f:
f.write(yaml.dump(config, default_flow_style=False))
@classmethod
def is_config_valid(cls):
config = cls.get_config()
return all([
config.get(x, None)
for x
in ('username', 'password', 'device_id')
])

167
songlist.py Normal file
View file

@ -0,0 +1,167 @@
import urwid
from player import player
class SongListItem(urwid.Pile):
signals = ['activate']
STATE_IDLE = 0
STATE_LOADING = 1
STATE_PLAYING = 2
STATE_PAUSED = 3
STATE_ICONS = {
0: ' ',
1: '\uF141',
2: '\uF04B',
3: '\uF04C'
}
def __init__(self, track, index):
self.track = track
self.index = index
self.state = SongListItem.STATE_IDLE
self.line1 = urwid.SelectableIcon('', cursor_position=6)
self.line2 = urwid.AttrWrap(
urwid.Text(''),
'line2'
)
self.content = urwid.AttrWrap(
urwid.Pile([
self.line1,
self.line2
]),
'line1',
'line1_focus'
)
super().__init__([
self.content
])
self.update_text()
def set_state(self, state):
self.state = state
self.update_text()
def get_state_icon(self, state):
return SongListItem.STATE_ICONS[state]
def update_text(self):
self.line1.set_text(
'{index:3d} {icon} {title} [{minutes:02d}:{seconds:02d}]'.format(
index=self.index,
icon=self.get_state_icon(self.state),
title=self.track.title,
minutes=self.track.duration // (1000 * 60),
seconds=(self.track.duration // 1000) % 60
)
)
self.line2.set_text(
' {}'.format(self.track.artist)
)
if self.state == SongListItem.STATE_IDLE:
self.content.set_attr('line1')
self.content.set_focus_attr('line1_focus')
else:
self.content.set_attr('line1_active')
self.content.set_focus_attr('line1_active_focus')
def keypress(self, size, key):
if key == 'enter':
urwid.emit_signal(self, 'activate', self)
return
return super().keypress(size, key)
# def render(self, size, focus=False):
# # if focus:
# # self.line1.attr = 'line1_focus'
# # self.line2.attr = 'line2_focus'
# # else:
# # self.line1.attr = 'line1'
# # self.line2.attr = 'line2'
# urwid.Pile.render(self, size, focus)
class SongListBox(urwid.ListBox):
signals = ['activate']
def __init__(self, app):
self.app = app
self.current_item = None
self.tracks = []
self.walker = urwid.SimpleFocusListWalker([
urwid.Text('\n  Loading song list...', align='center')
])
player.track_changed += self.track_changed
player.media_state_changed += self.media_state_changed
return super().__init__(self.walker)
def tracks_to_songlist(self, tracks):
current_track = player.get_current_track()
items = []
for index, track in enumerate(tracks):
songitem = SongListItem(track, index)
if current_track is not None and current_track.id == track.id:
songitem.set_state(SongListItem.STATE_PLAYING)
urwid.connect_signal(songitem, 'activate', self.item_activated)
items.append(songitem)
return items
def item_activated(self, songitem):
player.load_playlist(self.tracks, songitem.index)
def track_changed(self, track):
for songitem in self.walker:
if songitem.track.id == track.id:
songitem.set_state(SongListItem.STATE_PLAYING)
elif songitem.state != SongListItem.STATE_IDLE:
songitem.set_state(SongListItem.STATE_IDLE)
def media_state_changed(self, is_playing):
current_track = player.get_current_track()
if current_track is None:
return
for songitem in self.walker:
if songitem.track.id == current_track.id:
songitem.set_state(
SongListItem.STATE_PLAYING
if is_playing
else SongListItem.STATE_PAUSED
)
self.app.redraw()
# if self.current_item:
# self.current_item.set_state(SongList.Song.State.IDLE)
# self.current_item = item
# item.set_state(SongList.Song.State.LOADING)
# gp.get_stream_url(
# item.data['id'],
# callback=self.got_stream_url, extra=dict(item=item)
# )
# def got_stream_url(self, url, e, item):
# if item != self.current_item:
# # Another song play requested while we were fetching stream URL
# return
# if e:
# raise e
# item.set_state(SongList.Song.State.PLAYING)
# player.play(url)
# # urwid.emit_signal(self, 'activate', item)
def populate(self, tracks):
self.tracks = tracks
self.walker[:] = self.tracks_to_songlist(self.tracks)
# self.walker.set_focus(5)
# def keypress(self, size, key):
# # print(key)
# super().keypress(size, key)
# # if key == 'up':
# # self.walker.set_focus(

7124
vlc.py Normal file

File diff suppressed because it is too large Load diff