mirror of
https://github.com/vale981/clay
synced 2025-03-06 01:51:38 -05:00
Merge branch 'porcelain' of https://github.com/and3rson/clay into porcelain
This commit is contained in:
commit
1434ae0fec
16 changed files with 343 additions and 21 deletions
15
README.md
15
README.md
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
Standalone command line player for Google Play Music.
|
Standalone command line player for Google Play Music.
|
||||||
|
|
||||||
This app wouldn't be possible without the wonderful [gmusicapi] and [VLC] libraries.
|
This app wouldn't be possible without the wonderful [gmusicapi] and [VLC] & MPV libraries.
|
||||||
|
|
||||||
This project is neither affiliated nor endorsed by Google.
|
This project is neither affiliated nor endorsed by Google.
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ Documentation is [available here](http://clay.readthedocs.io/en/latest/).
|
||||||
- [gmusicapi] (PyPI)
|
- [gmusicapi] (PyPI)
|
||||||
- [urwid] (PyPI)
|
- [urwid] (PyPI)
|
||||||
- [PyYAML] (PyPI)
|
- [PyYAML] (PyPI)
|
||||||
- lib[VLC] (native, distributed with VLC player)
|
- lib[VLC] (native, distributed with VLC player) OR libMPV (native, distributed with MPV)
|
||||||
- [setproctitle] (optional) PyPI, used to change clay process name from 'python' to 'clay')
|
- [setproctitle] (optional) PyPI, used to change clay process name from 'python' to 'clay')
|
||||||
- [pydbus] (PyPI)
|
- [pydbus] (PyPI)
|
||||||
|
|
||||||
|
@ -100,8 +100,7 @@ Documentation is [available here](http://clay.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
1. Install Python 3, pydbus, PyGObject, and VLC from your package manager.
|
1. Install Python 3, pydbus, PyGObject, and VLC or MPV from your package manager.
|
||||||
|
|
||||||
|
|
||||||
## Method 1 (PyPi, automatic)
|
## Method 1 (PyPi, automatic)
|
||||||
|
|
||||||
|
@ -174,6 +173,14 @@ bind the keys to your windowing system of choice.
|
||||||
- You will also need to know your Device ID. Thanks to [gmusicapi], the app should display possible IDs once you enter a wrong one.
|
- You will also need to know your Device ID. Thanks to [gmusicapi], the app should display possible IDs once you enter a wrong one.
|
||||||
- Please be aware that this app has not been tested with 2FA yet.
|
- Please be aware that this app has not been tested with 2FA yet.
|
||||||
- For people with 2FA, you can just create an app password in Google accounts page and proceed normally. (Thanks @j605)
|
- For people with 2FA, you can just create an app password in Google accounts page and proceed normally. (Thanks @j605)
|
||||||
|
- By default VLC is used. If you want to use MPV instead, add the following line to your Clay config file (`~/.config/clay/config.yaml`) in `clay_settings` section:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
clay_settings:
|
||||||
|
player_class: clay.playback.mpv:MPVPlayer
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
# Controls
|
# Controls
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,13 @@ sys.path.insert(0, '.') # noqa
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from clay.core import meta, settings_manager
|
from clay.core import meta, settings_manager
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
import clay.ui.urwid as urwid
|
import clay.ui.urwid as urwid
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class MultilineVersionAction(argparse.Action):
|
class MultilineVersionAction(argparse.Action):
|
||||||
"""
|
"""
|
||||||
An argparser action for multiple lines so we can display the copyright notice
|
An argparser action for multiple lines so we can display the copyright notice
|
||||||
|
|
|
@ -63,6 +63,7 @@ hotkeys:
|
||||||
|
|
||||||
clay_settings:
|
clay_settings:
|
||||||
unicode: true
|
unicode: true
|
||||||
|
player_class: clay.playback.vlc:VLCPlayer
|
||||||
|
|
||||||
play_settings:
|
play_settings:
|
||||||
authtoken:
|
authtoken:
|
||||||
|
|
|
@ -192,6 +192,7 @@ class Track(object):
|
||||||
image = Image.open(BytesIO(data))
|
image = Image.open(BytesIO(data))
|
||||||
image.thumbnail((128, 128))
|
image.thumbnail((128, 128))
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
|
image = image.convert('RGB')
|
||||||
image.save(out, format='JPEG')
|
image.save(out, format='JPEG')
|
||||||
data = out.getvalue()
|
data = out.getvalue()
|
||||||
settings_manager.save_file_to_cache(self.artist_art_filename, data)
|
settings_manager.save_file_to_cache(self.artist_art_filename, data)
|
||||||
|
|
|
@ -7,7 +7,10 @@ import pkg_resources
|
||||||
from pydbus import SessionBus, Variant
|
from pydbus import SessionBus, Variant
|
||||||
from pydbus.generic import signal
|
from pydbus.generic import signal
|
||||||
from clay.core import meta
|
from clay.core import meta
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name,missing-docstring
|
# pylint: disable=invalid-name,missing-docstring
|
||||||
|
@ -175,13 +178,16 @@ class MPRIS2:
|
||||||
try:
|
try:
|
||||||
track = player.get_current_track()
|
track = player.get_current_track()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
track = None
|
||||||
|
|
||||||
|
if track is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'mpris:trackid': Variant('o', '/org/clay/' + str(track.store_id)),
|
'mpris:trackid': Variant('o', '/org/clay/' + str(track.store_id)),
|
||||||
'mpris:artUrl': Variant('s', track.artist_art_url),
|
'mpris:artUrl': Variant('s', track.artist_art_url),
|
||||||
'xesam:title': Variant('s', track.title),
|
'xesam:title': Variant('s', track.title),
|
||||||
'xesam:artist': Variant('s', track.artist),
|
'xesam:artist': Variant('s', track.artist.name),
|
||||||
'xesam:album': Variant('s', track.album_name),
|
'xesam:album': Variant('s', track.album_name),
|
||||||
'xesam:url': Variant('s', track.cached_url),
|
'xesam:url': Variant('s', track.cached_url),
|
||||||
}
|
}
|
||||||
|
@ -230,7 +236,7 @@ class MPRIS2:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Position(self):
|
def Position(self):
|
||||||
return player.play_progress
|
return player.time
|
||||||
|
|
||||||
# The following are custom additions to the protocol for features that clay supports
|
# The following are custom additions to the protocol for features that clay supports
|
||||||
def Mute(self):
|
def Mute(self):
|
||||||
|
|
0
clay/playback/__init__.py
Normal file
0
clay/playback/__init__.py
Normal file
|
@ -1,4 +1,4 @@
|
||||||
"""
|
<"""
|
||||||
An abstract class for playback
|
An abstract class for playback
|
||||||
|
|
||||||
Copyright (c) 2018, Valentijn van de Beek
|
Copyright (c) 2018, Valentijn van de Beek
|
||||||
|
@ -144,8 +144,6 @@ class AbstractPlayer:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._create_station_notification = None
|
self._create_station_notification = None
|
||||||
self._loading = False
|
|
||||||
self._playing = False
|
|
||||||
self.queue = _Queue()
|
self.queue = _Queue()
|
||||||
|
|
||||||
# Add notification actions that we are going to use.
|
# Add notification actions that we are going to use.
|
||||||
|
@ -170,8 +168,8 @@ class AbstractPlayer:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = dict(
|
data = dict(
|
||||||
loading=self._loading,
|
loading=self.loading,
|
||||||
playing=self._playing,
|
playing=self.playing,
|
||||||
artist=track.artist,
|
artist=track.artist,
|
||||||
title=track.title,
|
title=track.title,
|
||||||
progress=self.play_progress_seconds,
|
progress=self.play_progress_seconds,
|
||||||
|
|
270
clay/playback/mpv.py
Normal file
270
clay/playback/mpv.py
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
"""
|
||||||
|
An implementation of the Clay player using VLC
|
||||||
|
|
||||||
|
Copyright (c) 2018, Clay Contributors
|
||||||
|
"""
|
||||||
|
from ctypes import CFUNCTYPE, c_void_p, c_int, c_char_p
|
||||||
|
from clay.core import osd_manager, logger, meta, settings_manager
|
||||||
|
|
||||||
|
import mpv
|
||||||
|
from .abstract import AbstractPlayer
|
||||||
|
|
||||||
|
|
||||||
|
class MPVPlayer(AbstractPlayer):
|
||||||
|
"""
|
||||||
|
Interface to MPV. Uses Queue as a playback plan.
|
||||||
|
Emits various events if playback state, tracks or play flags change.
|
||||||
|
|
||||||
|
Singleton.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.media_player = mpv.MPV()
|
||||||
|
|
||||||
|
self.media_player.observe_property('pause', self._media_state_changed)
|
||||||
|
self.media_player.observe_property('stream-open-filename', self._media_state_changed)
|
||||||
|
self.media_player.observe_property('stream-pos', self._media_position_changed)
|
||||||
|
self.media_player.observe_property('idle-active', self._media_end_reached)
|
||||||
|
|
||||||
|
AbstractPlayer.__init__(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _media_state_changed(self, *_):
|
||||||
|
"""
|
||||||
|
Called when a libVLC playback state changes.
|
||||||
|
Broadcasts playback state & fires :attr:`media_state_changed` event.
|
||||||
|
"""
|
||||||
|
self.broadcast_state()
|
||||||
|
self.media_state_changed.fire(self.loading, self.playing)
|
||||||
|
|
||||||
|
def _media_end_reached(self, event, value):
|
||||||
|
"""
|
||||||
|
Called when end of currently played track is reached.
|
||||||
|
Advances to the next track.
|
||||||
|
"""
|
||||||
|
if value:
|
||||||
|
self.next()
|
||||||
|
|
||||||
|
def _media_position_changed(self, *_):
|
||||||
|
"""
|
||||||
|
Called when playback position changes (this happens few times each second.)
|
||||||
|
Fires :attr:`.media_position_changed` event.
|
||||||
|
"""
|
||||||
|
self.broadcast_state()
|
||||||
|
self.media_position_changed.fire(
|
||||||
|
self.play_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_station_ready(self, station, error):
|
||||||
|
"""
|
||||||
|
Called when a station is created.
|
||||||
|
If *error* is ``None``, load new station's tracks into queue.
|
||||||
|
"""
|
||||||
|
if error:
|
||||||
|
self._create_station_notification.update(
|
||||||
|
'Failed to create station: {}'.format(str(error))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not station.get_tracks():
|
||||||
|
self._create_station_notification.update(
|
||||||
|
'Newly created station is empty :('
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.load_queue(station.get_tracks())
|
||||||
|
self._create_station_notification.update('Station ready!')
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
"""
|
||||||
|
Pick current track from a queue and requests media stream URL.
|
||||||
|
Completes in background.
|
||||||
|
"""
|
||||||
|
track = self.queue.get_current_track()
|
||||||
|
if track is None:
|
||||||
|
return
|
||||||
|
self._loading = True
|
||||||
|
self.broadcast_state()
|
||||||
|
self.track_changed.fire(track)
|
||||||
|
|
||||||
|
if settings_manager.get('download_tracks', 'play_settings') or \
|
||||||
|
settings_manager.get_is_file_cached(track.filename):
|
||||||
|
path = settings_manager.get_cached_file_path(track.filename)
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
logger.debug('Track %s not in cache, downloading...', track.store_id)
|
||||||
|
track.get_url(callback=self._download_track)
|
||||||
|
else:
|
||||||
|
logger.debug('Track %s in cache, playing', track.store_id)
|
||||||
|
self._play_ready(path, None, track)
|
||||||
|
else:
|
||||||
|
logger.debug('Starting to stream %s', track.store_id)
|
||||||
|
track.get_url(callback=self._play_ready)
|
||||||
|
|
||||||
|
def _play_ready(self, url, error, track):
|
||||||
|
"""
|
||||||
|
Called once track's media stream URL request completes.
|
||||||
|
If *error* is ``None``, tell libVLC to play media by *url*.
|
||||||
|
"""
|
||||||
|
self._loading = False
|
||||||
|
if error:
|
||||||
|
#notification_area.notify('Failed to request media URL: {}'.format(str(error)))
|
||||||
|
logger.error(
|
||||||
|
'Failed to request media URL for track %s: %s',
|
||||||
|
track.original_data,
|
||||||
|
str(error)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
assert track
|
||||||
|
|
||||||
|
self.media_player.play(url)
|
||||||
|
|
||||||
|
osd_manager.notify(track.title, "by {}\nfrom {}\n".format(track.artist, track.album_name),
|
||||||
|
("media-skip-backward", "media-playback-pause", "media-skip-forward"),
|
||||||
|
track.get_artist_art_filename())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playing(self):
|
||||||
|
"""
|
||||||
|
True if a song is being played at the moment.
|
||||||
|
"""
|
||||||
|
return not self.media_player.pause
|
||||||
|
|
||||||
|
def play_pause(self):
|
||||||
|
"""
|
||||||
|
Toggle playback, i.e. play if paused or pause if playing.
|
||||||
|
"""
|
||||||
|
self.media_player.pause = not self.media_player.pause
|
||||||
|
|
||||||
|
@property
|
||||||
|
def play_progress(self):
|
||||||
|
"""
|
||||||
|
Return current playback position in range ``[0;1]`` (``float``).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.media_player.playback_time / self.media_player.duration
|
||||||
|
except TypeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def play_progress_seconds(self):
|
||||||
|
"""
|
||||||
|
Return current playback position in seconds (``int``).
|
||||||
|
"""
|
||||||
|
progress = self.media_player.playback_time
|
||||||
|
if progress is None:
|
||||||
|
return 0
|
||||||
|
return int(progress)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
"""
|
||||||
|
Return currently played track's length in microseconds (``int``).
|
||||||
|
"""
|
||||||
|
return self.length_seconds * 1e6
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length_seconds(self):
|
||||||
|
"""
|
||||||
|
Return currently played track's length in seconds (``int``).
|
||||||
|
"""
|
||||||
|
duration = self.media_player.duration
|
||||||
|
if duration is None:
|
||||||
|
duration = 0
|
||||||
|
return int(duration)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time(self):
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
Get their current movie length in microseconds
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return int(self.media_player.playback_time * 1e6)
|
||||||
|
except TypeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@time.setter
|
||||||
|
def time(self, time):
|
||||||
|
"""
|
||||||
|
Sets the current time in microseconds.
|
||||||
|
This is a pythonic alternative to seeking using absolute times instead of percentiles.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time: Time in microseconds.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.media_player.playback_time = int(time / 1e6)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._seeked()
|
||||||
|
|
||||||
|
def seek(self, delta):
|
||||||
|
"""
|
||||||
|
Seek to relative position.
|
||||||
|
*delta* must be a ``float`` in range ``[-1;1]``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.media_player.seek(int(self.length_seconds * delta))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def seek_absolute(self, position):
|
||||||
|
"""
|
||||||
|
Seek to absolute position.
|
||||||
|
*position* must be a ``float`` in range ``[0;1]``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.media_player.seek(int(self.length_seconds * position), reference='absolute')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_equalizer_freqs():
|
||||||
|
"""
|
||||||
|
Return a list of equalizer frequencies for each band.
|
||||||
|
"""
|
||||||
|
return [0] * 8
|
||||||
|
|
||||||
|
def get_equalizer_amps(self):
|
||||||
|
"""
|
||||||
|
Return a list of equalizer amplifications for each band.
|
||||||
|
"""
|
||||||
|
return [0] * 8
|
||||||
|
|
||||||
|
def set_equalizer_value(self, index, amp):
|
||||||
|
"""
|
||||||
|
Set equalizer amplification for specific band.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_equalizer_values(self, amps):
|
||||||
|
"""
|
||||||
|
Set a list of equalizer amplifications for each band.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume(self):
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
The current volume of in percentiles (0 = mute, 100 = 0dB)
|
||||||
|
"""
|
||||||
|
return self.media_player.volume
|
||||||
|
|
||||||
|
@volume.setter
|
||||||
|
def volume(self, volume):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
volume: the volume in percentiles (0 = mute, 1000 = 0dB)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The current volume of in percentiles (0 = mute, 100 = 0dB)
|
||||||
|
"""
|
||||||
|
self.media_player.volume = volume
|
||||||
|
|
||||||
|
def mute(self):
|
||||||
|
"""
|
||||||
|
Mutes or unmutes the volume
|
||||||
|
"""
|
||||||
|
self.media_player.mute = not self.media_player.mute
|
||||||
|
|
20
clay/playback/player.py
Normal file
20
clay/playback/player.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from clay.core.settings import settings_manager
|
||||||
|
|
||||||
|
_PLAYER = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_player():
|
||||||
|
global _PLAYER
|
||||||
|
if _PLAYER is None:
|
||||||
|
player_import_str = settings_manager.get('player_class', 'clay_settings')
|
||||||
|
if player_import_str is None:
|
||||||
|
player_import_str = 'clay.playback.vlc:VLCPlayer'
|
||||||
|
|
||||||
|
player_module, _, player_var = player_import_str.rpartition(':')
|
||||||
|
|
||||||
|
module = importlib.import_module(player_module)
|
||||||
|
player_class = getattr(module, player_var)
|
||||||
|
_PLAYER = player_class()
|
||||||
|
return _PLAYER
|
|
@ -328,6 +328,3 @@ class VLCPlayer(AbstractPlayer):
|
||||||
index
|
index
|
||||||
) == 0
|
) == 0
|
||||||
self.media_player.set_equalizer(self.equalizer)
|
self.media_player.set_equalizer(self.equalizer)
|
||||||
|
|
||||||
|
|
||||||
player = VLCPlayer() # pylint: disable=invalid-name
|
|
||||||
|
|
0
clay/ui/__init__.py
Normal file
0
clay/ui/__init__.py
Normal file
|
@ -3,7 +3,7 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from clay.core import gp, settings_manager
|
from clay.core import gp, settings_manager
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
|
|
||||||
from .clipboard import copy
|
from .clipboard import copy
|
||||||
from .hotkeys import hotkey_manager
|
from .hotkeys import hotkey_manager
|
||||||
|
@ -13,6 +13,9 @@ from .songlist import SongListBox
|
||||||
from .pages import *
|
from .pages import *
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class AppWidget(urwid.Frame):
|
class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
Root widget.
|
Root widget.
|
||||||
|
|
|
@ -4,9 +4,13 @@ Components for "Queue" page.
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from .page import AbstractPage
|
from .page import AbstractPage
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
from clay.ui.urwid import SongListBox
|
from clay.ui.urwid import SongListBox
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class QueuePage(urwid.Columns, AbstractPage):
|
class QueuePage(urwid.Columns, AbstractPage):
|
||||||
"""
|
"""
|
||||||
Queue page.
|
Queue page.
|
||||||
|
|
|
@ -5,9 +5,13 @@ import urwid
|
||||||
|
|
||||||
from .page import AbstractPage
|
from .page import AbstractPage
|
||||||
from clay.core import settings_manager
|
from clay.core import settings_manager
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
from clay.ui.urwid import hotkey_manager
|
from clay.ui.urwid import hotkey_manager
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class Slider(urwid.Widget):
|
class Slider(urwid.Widget):
|
||||||
"""
|
"""
|
||||||
Represents a (TODO: vertical) slider for equalizer band modification.
|
Represents a (TODO: vertical) slider for equalizer band modification.
|
||||||
|
|
|
@ -5,7 +5,11 @@ PlayBar widget.
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.core import settings_manager, meta
|
from clay.core import settings_manager, meta
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class ProgressBar(urwid.Widget):
|
class ProgressBar(urwid.Widget):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -16,13 +16,16 @@ except ImportError:
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.core import gp, settings_manager
|
from clay.core import gp, settings_manager
|
||||||
from clay.playback.vlc import player
|
from clay.playback.player import get_player
|
||||||
|
|
||||||
from .notifications import notification_area
|
from .notifications import notification_area
|
||||||
from .hotkeys import hotkey_manager
|
from .hotkeys import hotkey_manager
|
||||||
from .clipboard import copy
|
from .clipboard import copy
|
||||||
|
|
||||||
|
|
||||||
|
player = get_player() # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class SongListItem(urwid.Pile):
|
class SongListItem(urwid.Pile):
|
||||||
"""
|
"""
|
||||||
Widget that represents single song item.
|
Widget that represents single song item.
|
||||||
|
|
Loading…
Add table
Reference in a new issue