Merge branch 'porcelain' of https://github.com/and3rson/clay into porcelain

This commit is contained in:
Valentijn 2018-09-18 17:44:32 +02:00
commit 1434ae0fec
16 changed files with 343 additions and 21 deletions

View file

@ -27,7 +27,7 @@
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.
@ -65,7 +65,7 @@ Documentation is [available here](http://clay.readthedocs.io/en/latest/).
- [gmusicapi] (PyPI)
- [urwid] (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')
- [pydbus] (PyPI)
@ -100,8 +100,7 @@ Documentation is [available here](http://clay.readthedocs.io/en/latest/).
# 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)
@ -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.
- 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)
- 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

View file

@ -9,9 +9,13 @@ sys.path.insert(0, '.') # noqa
import argparse
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
player = get_player() # pylint: disable=invalid-name
class MultilineVersionAction(argparse.Action):
"""
An argparser action for multiple lines so we can display the copyright notice

View file

@ -63,6 +63,7 @@ hotkeys:
clay_settings:
unicode: true
player_class: clay.playback.vlc:VLCPlayer
play_settings:
authtoken:

View file

@ -192,6 +192,7 @@ class Track(object):
image = Image.open(BytesIO(data))
image.thumbnail((128, 128))
out = BytesIO()
image = image.convert('RGB')
image.save(out, format='JPEG')
data = out.getvalue()
settings_manager.save_file_to_cache(self.artist_art_filename, data)

View file

@ -7,7 +7,10 @@ import pkg_resources
from pydbus import SessionBus, Variant
from pydbus.generic import signal
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
@ -175,13 +178,16 @@ class MPRIS2:
try:
track = player.get_current_track()
except AttributeError:
track = None
if track is None:
return {}
return {
'mpris:trackid': Variant('o', '/org/clay/' + str(track.store_id)),
'mpris:artUrl': Variant('s', track.artist_art_url),
'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:url': Variant('s', track.cached_url),
}
@ -230,7 +236,7 @@ class MPRIS2:
@property
def Position(self):
return player.play_progress
return player.time
# The following are custom additions to the protocol for features that clay supports
def Mute(self):

View file

View file

@ -1,4 +1,4 @@
"""
<"""
An abstract class for playback
Copyright (c) 2018, Valentijn van de Beek
@ -144,8 +144,6 @@ class AbstractPlayer:
def __init__(self):
self._create_station_notification = None
self._loading = False
self._playing = False
self.queue = _Queue()
# Add notification actions that we are going to use.
@ -170,8 +168,8 @@ class AbstractPlayer:
)
else:
data = dict(
loading=self._loading,
playing=self._playing,
loading=self.loading,
playing=self.playing,
artist=track.artist,
title=track.title,
progress=self.play_progress_seconds,

270
clay/playback/mpv.py Normal file
View 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
View 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

View file

@ -328,6 +328,3 @@ class VLCPlayer(AbstractPlayer):
index
) == 0
self.media_player.set_equalizer(self.equalizer)
player = VLCPlayer() # pylint: disable=invalid-name

0
clay/ui/__init__.py Normal file
View file

View file

@ -3,7 +3,7 @@ import sys
import threading
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 .hotkeys import hotkey_manager
@ -13,6 +13,9 @@ from .songlist import SongListBox
from .pages import *
player = get_player() # pylint: disable=invalid-name
class AppWidget(urwid.Frame):
"""
Root widget.

View file

@ -4,9 +4,13 @@ Components for "Queue" page.
import urwid
from .page import AbstractPage
from clay.playback.vlc import player
from clay.playback.player import get_player
from clay.ui.urwid import SongListBox
player = get_player() # pylint: disable=invalid-name
class QueuePage(urwid.Columns, AbstractPage):
"""
Queue page.

View file

@ -5,9 +5,13 @@ import urwid
from .page import AbstractPage
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
player = get_player() # pylint: disable=invalid-name
class Slider(urwid.Widget):
"""
Represents a (TODO: vertical) slider for equalizer band modification.

View file

@ -5,7 +5,11 @@ PlayBar widget.
import urwid
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):
"""

View file

@ -16,13 +16,16 @@ except ImportError:
import urwid
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 .hotkeys import hotkey_manager
from .clipboard import copy
player = get_player() # pylint: disable=invalid-name
class SongListItem(urwid.Pile):
"""
Widget that represents single song item.