mirror of
https://github.com/vale981/clay
synced 2025-03-05 17:41:42 -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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -63,6 +63,7 @@ hotkeys:
|
|||
|
||||
clay_settings:
|
||||
unicode: true
|
||||
player_class: clay.playback.vlc:VLCPlayer
|
||||
|
||||
play_settings:
|
||||
authtoken:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
0
clay/playback/__init__.py
Normal file
0
clay/playback/__init__.py
Normal 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
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
|
||||
) == 0
|
||||
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
|
||||
|
||||
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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Reference in a new issue