mirror of
https://github.com/vale981/clay
synced 2025-03-05 17:41:42 -05:00
An implementation of MPRIS2 to replace keybinder and change some getter/setter to properties
This commit is contained in:
parent
890e51bced
commit
91681acb5b
16 changed files with 519 additions and 197 deletions
18
clay/app.py
18
clay/app.py
|
@ -47,29 +47,11 @@ def main():
|
|||
|
||||
parser.add_argument("-v", "--version", action=MultilineVersionAction)
|
||||
|
||||
keybinds_group = parser.add_mutually_exclusive_group()
|
||||
|
||||
keybinds_group.add_argument(
|
||||
"--with-x-keybinds",
|
||||
help="define global X keybinds (requires Keybinder and PyGObject)",
|
||||
action='store_true'
|
||||
)
|
||||
|
||||
keybinds_group.add_argument(
|
||||
"--without-x-keybinds",
|
||||
help="Don't define global keybinds (overrides configuration file)",
|
||||
action='store_true'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
exit(0)
|
||||
|
||||
if (args.with_x_keybinds or settings_manager.get('x_keybinds', 'clay_settings')) \
|
||||
and not args.without_x_keybinds:
|
||||
player.enable_xorg_bindings()
|
||||
|
||||
urwid.main()
|
||||
|
||||
|
||||
|
|
|
@ -3,3 +3,4 @@ from .gp import gp
|
|||
from .log import logger
|
||||
from .settings import settings_manager
|
||||
from .osd import osd_manager
|
||||
from .mpris2 import MPRIS2
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
hotkeys:
|
||||
mod_key: ctrl
|
||||
|
||||
x_hotkeys:
|
||||
play_pause: XF86AudioPlay
|
||||
next: XF86AudioNext
|
||||
prev: XF86AudioPrev
|
||||
|
||||
clay_hotkeys:
|
||||
global:
|
||||
seek_start: mod + q
|
||||
|
@ -60,7 +55,6 @@ hotkeys:
|
|||
equalizer_down: "-"
|
||||
|
||||
clay_settings:
|
||||
x_keybinds: false
|
||||
unicode: true
|
||||
|
||||
play_settings:
|
||||
|
|
|
@ -131,7 +131,7 @@ class Track(object):
|
|||
self.artist_art_filename = sha1(
|
||||
self.artist_art_url.encode('utf-8')
|
||||
).hexdigest() + u'.jpg'
|
||||
self.explicit_rating = (int(data['explicitType']))
|
||||
self.explicit_rating = (int(data['explicitType'] if 'explicitType' in data else 0))
|
||||
|
||||
if self.rating == 5:
|
||||
gp.cached_liked_songs.add_liked_song(self)
|
||||
|
|
42
clay/core/mpris/org.mpris.MediaPlayer2.Player.xml
Normal file
42
clay/core/mpris/org.mpris.MediaPlayer2.Player.xml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<node>
|
||||
<interface name="org.mpris.MediaPlayer2.Player">
|
||||
<property name="PlaybackStatus" type="s" access="read"/>
|
||||
<property name="LoopStatus" type="s" access="readwrite"/>
|
||||
<property name="Shuffle" type="b" access="readwrite"/>
|
||||
<property name="Rate" type="d" access="readwrite"/>
|
||||
<property name="Metadata" type="a{sv}" access="read"/>
|
||||
<property name="Volume" type="d" access="readwrite"/>
|
||||
<property name="Position" type="x" access="read"/>
|
||||
<property name="MinimumRate" type="d" access="readwrite"/>
|
||||
<property name="MaximumRate" type="d" access="readwrite"/>
|
||||
<property name="CanGoNext" type="b" access="read"/>
|
||||
|
||||
<property name="CanGoPrevious" type="b" access="read"/>
|
||||
<property name="CanPlay" type="b" access="read"/>
|
||||
<property name="CanPause" type="b" access="read"/>
|
||||
<property name="CanSeek" type="b" access="read"/>
|
||||
<property name="CanControl" type="b" access="read"/>
|
||||
|
||||
<method name="Next"/>
|
||||
<method name="Previous"/>
|
||||
<method name="Pause"/>
|
||||
<method name="PlayPause"/>
|
||||
<method name="Stop"/>
|
||||
<method name="Play"/>
|
||||
<method name="Seek">
|
||||
<arg type="x" direction="in"/>
|
||||
</method>
|
||||
<method name="SetPosition">
|
||||
<arg type="o" direction="in"/>
|
||||
<arg type="x" direction="in"/>
|
||||
</method>
|
||||
<method name="OpenUri">
|
||||
<arg type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<!-- Clay additions -->
|
||||
<method name="Mute"/>
|
||||
<property name="Rating" type="x" access="readwrite"/>
|
||||
<property name="Explicit" type="b" access="read"/>
|
||||
</interface>
|
||||
</node>
|
18
clay/core/mpris/org.mpris.MediaPlayer2.Playlists.xml
Normal file
18
clay/core/mpris/org.mpris.MediaPlayer2.Playlists.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<node>
|
||||
<interface name="org.mpris.MediaPlayer2.Playlists">
|
||||
<property name="PlaylistCount" type="u" access="read"/>
|
||||
<property name="Orderings" type="as" access="read"/>
|
||||
<property name="ActivePlaylist" type="(b(oss))" access="read"/>
|
||||
|
||||
<method name="ActivatePlaylist">
|
||||
<arg type="o" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="GetPlaylists">
|
||||
<arg type="u" direction="in"/>
|
||||
<arg type="u" direction="in"/>
|
||||
<arg type="s" direction="in"/>
|
||||
<arg type="b" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
24
clay/core/mpris/org.mpris.MediaPlayer2.TrackList.xml
Normal file
24
clay/core/mpris/org.mpris.MediaPlayer2.TrackList.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<node>
|
||||
<interface name="org.mpris.MediaPlayer2.TrackList">
|
||||
<property name="Tracks" type="oa" access="read"/>
|
||||
<property name="CanEditTracks" type="b" access="read"/>
|
||||
|
||||
<method name="GetTracksMetadata">
|
||||
<arg type="ao" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="AddTrack">
|
||||
<arg type="s" direction="in"/>
|
||||
<arg type="o" direction="in"/>
|
||||
<arg type="b" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="RemoveTrack">
|
||||
<arg type="o" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="GoTo">
|
||||
<arg type="o" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
15
clay/core/mpris/org.mpris.MediaPlayer2.xml
Normal file
15
clay/core/mpris/org.mpris.MediaPlayer2.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<node>
|
||||
<interface name="org.mpris.MediaPlayer2">
|
||||
<property name="CanQuit" type="b" access="read"/>
|
||||
<property name="Fullscreen" type="b" access="readwrite"/>
|
||||
<property name="CanSetFullscreen" type="b" access="read"/>
|
||||
<property name="CanRaise" type="b" access="read"/>
|
||||
<property name="HasTrackList" type="b" access="read"/>
|
||||
<property name="Identity" type="s" access="read"/>
|
||||
<property name="DesktopEntry" type="s" access="read"/>
|
||||
<property name="SupportedMimeTypes" type="as" access="read"/>
|
||||
<property name="SupportedUriSchemes" type="as" access="read"/>
|
||||
<method name="Quit"/>
|
||||
<method name="Raise"/>
|
||||
</interface>
|
||||
</node>
|
278
clay/core/mpris2.py
Normal file
278
clay/core/mpris2.py
Normal file
|
@ -0,0 +1,278 @@
|
|||
"""
|
||||
This module defines and starts a MPRIS2 dbus interface
|
||||
"""
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from pydbus import SessionBus, Variant
|
||||
from clay.core import meta
|
||||
from clay.playback.vlc import player
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,missing-docstring
|
||||
class MPRIS2:
|
||||
"""
|
||||
An object that defines and implements the MPRIS2 protocol for Clay
|
||||
"""
|
||||
def __init__(self):
|
||||
self._stopped = False
|
||||
|
||||
# MediaPlayer2 interface
|
||||
def Raise(self):
|
||||
pass
|
||||
|
||||
# TODO: Cleanup after ourselves
|
||||
def Quit(self):
|
||||
sys.exit(0)
|
||||
|
||||
@property
|
||||
def CanQuit(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def Fullscreen(self):
|
||||
pass
|
||||
|
||||
@Fullscreen.setter
|
||||
def Fullscreen(self, _):
|
||||
# We aren't graphical so we just ignore this call
|
||||
pass
|
||||
|
||||
@property
|
||||
def CanSetFullscreen(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def CanRaise(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def HasTrackList(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def Identity(self):
|
||||
return "Clay Player"
|
||||
|
||||
@property
|
||||
def DesktopEntry(self):
|
||||
return "clay"
|
||||
|
||||
@property
|
||||
def SupportedMimeTypes(self):
|
||||
return []
|
||||
|
||||
@property
|
||||
def SupportedUriSchemes(self):
|
||||
return []
|
||||
|
||||
# MediaPlayer2 Player interface
|
||||
def Next(self):
|
||||
"""
|
||||
Goes to the next song in the queue.
|
||||
"""
|
||||
player.next()
|
||||
|
||||
def Previous(self):
|
||||
"""
|
||||
Goes to previous in the queue
|
||||
"""
|
||||
player.prev()
|
||||
|
||||
def Pause(self):
|
||||
"""
|
||||
Pauses the backback
|
||||
"""
|
||||
if player.is_playing:
|
||||
player.play_pause()
|
||||
|
||||
def PlayPause(self):
|
||||
"""
|
||||
Toggles playback, i.e. play if pause or pause if playing
|
||||
"""
|
||||
player.play_pause()
|
||||
|
||||
def Stop(self):
|
||||
"""
|
||||
Stops playback and returns to the beginning of the song.
|
||||
"""
|
||||
self._stopped = True
|
||||
self.Pause()
|
||||
player.seek(-1)
|
||||
|
||||
def Play(self):
|
||||
"""
|
||||
Starts or resumes playback.
|
||||
"""
|
||||
if self._stopped:
|
||||
self._stopped = False
|
||||
|
||||
if not player.is_playing:
|
||||
player.play_pause()
|
||||
|
||||
def Seek(self, offset):
|
||||
"""
|
||||
Seeks forward in the current track by the specified number of microseconds.
|
||||
A negative value seeks backwards in the track until the value is current - offset
|
||||
Or if that value would be lower to zero, zero.
|
||||
|
||||
Args:
|
||||
offset: the number of microseconds to seek forwards.
|
||||
"""
|
||||
player.time = player.time + offset
|
||||
|
||||
def SetPosition(self, track_id, position):
|
||||
"""
|
||||
Sets the current position in microseconds.
|
||||
"""
|
||||
pass
|
||||
|
||||
# pylint: disable=no-else-return
|
||||
@property
|
||||
def PlaybackStatus(self):
|
||||
"""
|
||||
Returns the current status of clay.
|
||||
"""
|
||||
if self._stopped or player.queue.get_tracks() == []:
|
||||
return "Stopped"
|
||||
elif player.is_playing:
|
||||
return "Playing"
|
||||
else:
|
||||
return "Paused"
|
||||
|
||||
@property
|
||||
def LoopStatus(self):
|
||||
"""
|
||||
Returns:
|
||||
Whether the song is not, single or playlist looping
|
||||
"""
|
||||
if player.get_is_repeat_one():
|
||||
return "Track"
|
||||
else:
|
||||
return "None"
|
||||
|
||||
# TODO: We don't allow someone to control playback atm so this doesn't do anything
|
||||
@property
|
||||
def MinimumRate(self):
|
||||
return -1.0
|
||||
|
||||
@property
|
||||
def MaximumRate(self):
|
||||
return 1.0
|
||||
|
||||
@property
|
||||
def Rate(self):
|
||||
"""
|
||||
Returns the playback rate of the current song.
|
||||
"""
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def Metadata(self):
|
||||
try:
|
||||
track = player.get_current_track()
|
||||
except AttributeError:
|
||||
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:album': Variant('s', track.album_name),
|
||||
'xesam:url': Variant('s', track.cached_url),
|
||||
}
|
||||
|
||||
@property
|
||||
def CanPause(self):
|
||||
return player.get_current_track() is not None
|
||||
|
||||
@property
|
||||
def CanPlay(self):
|
||||
return player.get_current_track() is not None
|
||||
|
||||
@property
|
||||
def CanGoNext(self):
|
||||
return len(player.queue.get_tracks()) > 1
|
||||
|
||||
@property
|
||||
def CanGoPrevious(self):
|
||||
#TODO fix
|
||||
return len(player.queue.get_tracks()) > 1
|
||||
|
||||
@property
|
||||
def CanSeek(self):
|
||||
return player.get_current_track() is not None
|
||||
|
||||
@property
|
||||
def CanControl(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def Shuffle(self):
|
||||
return player.get_is_random()
|
||||
|
||||
@property
|
||||
def Volume(self):
|
||||
return player.volume / 100
|
||||
|
||||
@Volume.setter
|
||||
def Volume(self, volume):
|
||||
# Don't blast someone's ears off because they entered the wrong thing.
|
||||
# Just enter it raw into volume since that is probably what they meant to do.
|
||||
if volume > 1.0:
|
||||
player.volume = int(volume)
|
||||
else:
|
||||
player.volume = int(volume * 100)
|
||||
|
||||
@property
|
||||
def Position(self):
|
||||
return player.play_progress
|
||||
|
||||
# The following are custom additions to the protocol for features that clay supports
|
||||
def Mute(self):
|
||||
"""
|
||||
Mutes or unmutes the volume.
|
||||
"""
|
||||
player.mute()
|
||||
|
||||
@property
|
||||
def Rating(self):
|
||||
"""
|
||||
Returns:
|
||||
The rating of the current song.
|
||||
"""
|
||||
try:
|
||||
return player.get_current_track().rating
|
||||
except AttributeError:
|
||||
return 0
|
||||
|
||||
@Rating.setter
|
||||
def Rating(self, rating):
|
||||
"""
|
||||
Takes a rating and sets the current song to that rating.
|
||||
|
||||
1-2 thumbs down
|
||||
4-5 thumbs up
|
||||
0 None
|
||||
"""
|
||||
try:
|
||||
player.get_current_track().rate_song(rating)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def Explicit(self):
|
||||
track = player.get_current_track()
|
||||
if track is None:
|
||||
return False
|
||||
else:
|
||||
return track.explicit_rating != 0
|
||||
|
||||
bus = SessionBus()
|
||||
MPRIS2.dbus = [pkg_resources.resource_string(__name__, "mpris/org.mpris.MediaPlayer2" +name+ ".xml")
|
||||
.decode("utf-8")
|
||||
for name in ("", ".Player", ".Playlists", ".TrackList")]
|
||||
|
||||
bus.publish("org.mpris.MediaPlayer2.clay", MPRIS2(),
|
||||
('/org/mpris/MediaPlayer2', MPRIS2()))
|
|
@ -9,7 +9,7 @@ from clay.core import meta
|
|||
IS_INIT = False
|
||||
|
||||
try:
|
||||
from dbus import SessionBus, Interface
|
||||
from pydbus import SessionBus
|
||||
IS_INIT = True
|
||||
except ImportError:
|
||||
ERROR_MESSAGE = 'Could not import dbus. OSD notifications will be disabled.'
|
||||
|
@ -29,11 +29,7 @@ class _OSDManager(object):
|
|||
|
||||
if IS_INIT:
|
||||
self.bus = SessionBus()
|
||||
self.notifcations = self.bus.get_object(
|
||||
"org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications"
|
||||
)
|
||||
self.notify_interface = Interface(self.notifcations, "org.freedesktop.Notifications")
|
||||
self.notifications = self.bus.get(".Notifications")
|
||||
|
||||
def notify(self, track):
|
||||
"""
|
||||
|
@ -46,7 +42,7 @@ class _OSDManager(object):
|
|||
|
||||
def _notify(self, track):
|
||||
artist_art_filename = track.get_artist_art_filename()
|
||||
self._last_id = self.notify_interface.Notify(
|
||||
self._last_id = self.notifications.Notify(
|
||||
meta.APP_NAME,
|
||||
self._last_id,
|
||||
artist_art_filename if artist_art_filename is not None else 'audio-headphones',
|
||||
|
|
|
@ -166,25 +166,14 @@ class AbstractPlayer:
|
|||
playing=self._is_playing,
|
||||
artist=track.artist,
|
||||
title=track.title,
|
||||
progress=self.get_play_progress_seconds(),
|
||||
length=self.get_length_seconds(),
|
||||
progress=self.play_progress_seconds,
|
||||
length=self.length_seconds,
|
||||
album_name=track.album_name,
|
||||
album_url=track.album_url
|
||||
)
|
||||
with open('/tmp/clay.json', 'w') as statefile:
|
||||
statefile.write(json.dumps(data, indent=4))
|
||||
|
||||
def enable_xorg_bindings(self):
|
||||
"""Enable the global X bindings using keybinder"""
|
||||
if os.environ.get("DISPLAY") is None:
|
||||
logger.debug("X11 isn't running so we can't load the global keybinds")
|
||||
return
|
||||
|
||||
from clay.ui.urwid.hotkeys import hotkey_manager
|
||||
hotkey_manager.play_pause += self.play_pause
|
||||
hotkey_manager.next += self.next
|
||||
hotkey_manager.prev += lambda: self.seek_absolute(0)
|
||||
|
||||
def load_queue(self, data, current_index=None):
|
||||
"""
|
||||
Load queue & start playbac
|
||||
|
@ -264,12 +253,6 @@ class AbstractPlayer:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_play_progress(self):
|
||||
"""
|
||||
Return the current playback position in range ``[0;1]`` (``float``)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _download_track(self, url, error, track):
|
||||
if error:
|
||||
logger.error(
|
||||
|
@ -297,19 +280,74 @@ class AbstractPlayer:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_play_progress(self):
|
||||
@property
|
||||
def play_progress(self):
|
||||
"""
|
||||
Return current playback position in range ``[0;1]`` (``float``)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_play_progress_seconds(self):
|
||||
@property
|
||||
def play_progress_seconds(self):
|
||||
"""
|
||||
Return the current playback position in seconds (``int``)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_length_seconds(self):
|
||||
@property
|
||||
def time(self):
|
||||
"""
|
||||
Returns:
|
||||
Get their current movie length in microseconds
|
||||
e """
|
||||
raise NotImplementedError
|
||||
|
||||
@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.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
"""
|
||||
Returns:
|
||||
The current volume of in percentiles (0 = mute, 100 = 0dB)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@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)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def mute(self):
|
||||
"""
|
||||
Mutes or unmutes the volume
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""
|
||||
Returns:
|
||||
The current playback position in microseconds
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def length_seconds(self):
|
||||
"""
|
||||
Return currently played track's length in seconds (``int``).
|
||||
"""
|
||||
|
|
|
@ -94,17 +94,9 @@ class VLCPlayer(AbstractPlayer):
|
|||
assert event
|
||||
self.broadcast_state()
|
||||
self.media_position_changed.fire(
|
||||
self.get_play_progress()
|
||||
self.play_progress
|
||||
)
|
||||
|
||||
# def create_station_from_track(self, track):
|
||||
# """
|
||||
# Request creation of new station from some track.
|
||||
# Runs in background.
|
||||
# """
|
||||
# #self._create_station_notification = notification_area.notify('Creating station...')
|
||||
# track.create_station_async(callback=self._create_station_from_track_ready)
|
||||
|
||||
def _create_station_ready(self, station, error):
|
||||
"""
|
||||
Called when a station is created.
|
||||
|
@ -195,31 +187,89 @@ class VLCPlayer(AbstractPlayer):
|
|||
self.media_player.pause()
|
||||
else:
|
||||
self.media_player.play()
|
||||
|
||||
def get_play_progress(self):
|
||||
@property
|
||||
def play_progress(self):
|
||||
"""
|
||||
Return current playback position in range ``[0;1]`` (``float``).
|
||||
Returns:
|
||||
A float of the current playback position in range of 0 to 1.
|
||||
"""
|
||||
return self.media_player.get_position()
|
||||
|
||||
def get_play_progress_seconds(self):
|
||||
@property
|
||||
def play_progress_seconds(self):
|
||||
"""
|
||||
Return current playback position in seconds (``int``).
|
||||
"""
|
||||
return int(self.media_player.get_position() * self.media_player.get_length() / 1000)
|
||||
Returns:
|
||||
The current playback position in seconds.
|
||||
|
||||
def get_length_seconds(self):
|
||||
"""
|
||||
Return currently played track's length in seconds (``int``).
|
||||
return int(self.play_progress * self.media_player.get_length() / 1000)
|
||||
|
||||
@property
|
||||
def time(self):
|
||||
"""
|
||||
return int(self.media_player.get_length() // 1000)
|
||||
Returns:
|
||||
Get their current movie length in microseconds
|
||||
"""
|
||||
return self.media_player.get_time()
|
||||
|
||||
@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.
|
||||
"""
|
||||
self.media_player.set_time(time)
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
"""
|
||||
Returns:
|
||||
The current volume of in percentiles (0 = mute, 100 = 0dB)
|
||||
"""
|
||||
return self.media_player.audio_get_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)
|
||||
"""
|
||||
return self.media_player.audio_set_volume(volume)
|
||||
|
||||
def mute(self):
|
||||
"""
|
||||
Mutes or unmutes the volume
|
||||
"""
|
||||
self.media_player.set_mute(not self.media_player.audio_get_mute())
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""
|
||||
Returns:
|
||||
The current playback position in microseconds
|
||||
"""
|
||||
return self.media_player.get_length()
|
||||
|
||||
@property
|
||||
def length_seconds(self):
|
||||
"""
|
||||
Returns:
|
||||
The current playback in position in seconds
|
||||
"""
|
||||
return self.length // 1000
|
||||
|
||||
def seek(self, delta):
|
||||
"""
|
||||
Seek to relative position.
|
||||
*delta* must be a ``float`` in range ``[-1;1]``.
|
||||
"""
|
||||
self.media_player.set_position(self.get_play_progress() + delta)
|
||||
self.media_player.set_position(self.play_progress + delta)
|
||||
|
||||
def seek_absolute(self, position):
|
||||
"""
|
||||
|
@ -276,4 +326,5 @@ class VLCPlayer(AbstractPlayer):
|
|||
) == 0
|
||||
self.media_player.set_equalizer(self.equalizer)
|
||||
|
||||
|
||||
player = VLCPlayer() # pylint: disable=invalid-name
|
||||
|
|
|
@ -210,11 +210,6 @@ class AppWidget(urwid.Frame):
|
|||
Handle keypress.
|
||||
Can switch tabs, control playback, flags, notifications and app state.
|
||||
"""
|
||||
# for tab in self.tabs:
|
||||
# if 'meta {}'.format(tab.page.key) == key:
|
||||
# self.set_page(tab.page.__class__.__name__)
|
||||
# return
|
||||
|
||||
hotkey_manager.keypress("global", self, super(AppWidget, self), size, key)
|
||||
|
||||
def show_debug(self):
|
||||
|
@ -306,7 +301,6 @@ class AppWidget(urwid.Frame):
|
|||
Quit app.
|
||||
"""
|
||||
self.loop = None
|
||||
hotkey_manager.quit()
|
||||
sys.exit(0)
|
||||
|
||||
def handle_escape(self):
|
||||
|
@ -335,7 +329,7 @@ def main():
|
|||
|
||||
# Run the actual program
|
||||
app_widget = AppWidget()
|
||||
loop = urwid.MainLoop(app_widget, palette)
|
||||
loop = urwid.MainLoop(app_widget, palette, event_loop=urwid.GLibEventLoop())
|
||||
app_widget.set_loop(loop)
|
||||
loop.screen.set_terminal_properties(256)
|
||||
loop.run()
|
||||
|
|
|
@ -3,108 +3,21 @@ Hotkeys management.
|
|||
Requires "gi" package and "Gtk" & "Keybinder" modules.
|
||||
"""
|
||||
# pylint: disable=broad-except
|
||||
import os
|
||||
import threading
|
||||
|
||||
|
||||
from clay.core import EventHook, settings_manager, logger
|
||||
from .notifications import notification_area
|
||||
|
||||
|
||||
IS_INIT = False
|
||||
|
||||
from clay.core import settings_manager, logger
|
||||
|
||||
def report_error(exc):
|
||||
"Print an error message to the debug screen"
|
||||
logger.error("{0}: {1}".format(exc.__class__.__name__, exc))
|
||||
|
||||
|
||||
try:
|
||||
# pylint: disable=import-error
|
||||
import gi
|
||||
gi.require_version('Keybinder', '3.0') # noqa
|
||||
gi.require_version('Gtk', '3.0') # noqa
|
||||
from gi.repository import Keybinder, Gtk
|
||||
# pylint: enable=import-error
|
||||
except ImportError as error:
|
||||
report_error(error)
|
||||
ERROR_MESSAGE = "Couldn't import PyGObject"
|
||||
except ValueError as error:
|
||||
report_error(error)
|
||||
ERROR_MESSAGE = "Couldn't find the Keybinder and/or Gtk modules"
|
||||
except Exception as error:
|
||||
report_error(error)
|
||||
ERROR_MESSAGE = "There was unknown error: '{}'".format(error)
|
||||
else:
|
||||
IS_INIT = True
|
||||
|
||||
|
||||
class _HotkeyManager(object):
|
||||
"""
|
||||
Manages configs.
|
||||
Runs Gtk main loop in a thread.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._x_hotkeys = {}
|
||||
self._hotkeys = self._parse_hotkeys()
|
||||
self.config = None
|
||||
|
||||
self.play_pause = EventHook()
|
||||
self.next = EventHook()
|
||||
self.prev = EventHook()
|
||||
|
||||
if IS_INIT and os.environ.get("DISPLAY") is not None and \
|
||||
settings_manager.get('x_keybinds', 'clay_settings'):
|
||||
Keybinder.init()
|
||||
self.initialize()
|
||||
threading.Thread(target=Gtk.main).start()
|
||||
elif not IS_INIT:
|
||||
logger.debug("Not loading the global shortcuts.")
|
||||
notification_area.notify(
|
||||
ERROR_MESSAGE +
|
||||
", this means the global shortcuts will not work.\n" +
|
||||
"You can check the log for more details."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _to_gtk_modifier(key):
|
||||
"""
|
||||
Translates the modifies to the way that GTK likes them.
|
||||
"""
|
||||
key = key.strip()
|
||||
|
||||
if key == "meta":
|
||||
key = "<alt>"
|
||||
elif key in ("ctrl", "alt", "shift"):
|
||||
key = "<" + key + ">"
|
||||
else:
|
||||
key = key
|
||||
|
||||
return key
|
||||
|
||||
def _parse_x_hotkeys(self):
|
||||
"""
|
||||
Reads out them configuration file and parses them into hotkeys readable by GTK.
|
||||
"""
|
||||
hotkey_default_config = settings_manager.get_default_config_section('hotkeys', 'x_hotkeys')
|
||||
mod_key = settings_manager.get('mod_key', 'hotkeys')
|
||||
hotkeys = {}
|
||||
|
||||
for action in hotkey_default_config:
|
||||
key_seq = settings_manager.get(action, 'hotkeys', 'x_hotkeys')
|
||||
|
||||
for key in key_seq.split(', '):
|
||||
hotkey = key.split(' + ')
|
||||
|
||||
if hotkey[0].strip() == 'mod':
|
||||
hotkey[0] = mod_key
|
||||
|
||||
hotkey = [self._to_gtk_modifier(key) for key in hotkey]
|
||||
|
||||
hotkeys[action] = ''.join(hotkey)
|
||||
|
||||
return hotkeys
|
||||
|
||||
def _parse_hotkeys(self):
|
||||
"""
|
||||
Reads out the configuration file and parse them into a hotkeys for urwid.
|
||||
|
@ -128,10 +41,6 @@ class _HotkeyManager(object):
|
|||
|
||||
return hotkeys
|
||||
|
||||
def quit(self):
|
||||
"""Quits the keybinder"""
|
||||
Gtk.main_quit()
|
||||
|
||||
def keypress(self, name, caller, super_, size, key):
|
||||
"""
|
||||
Process the pressed key by looking it up in the configuration file
|
||||
|
@ -148,25 +57,4 @@ class _HotkeyManager(object):
|
|||
|
||||
return ret
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Unbind previous hotkeys, re-read config & bind new hotkeys.
|
||||
"""
|
||||
for operation, key in self._x_hotkeys.items():
|
||||
Keybinder.unbind(key)
|
||||
|
||||
self._x_hotkeys = self._parse_x_hotkeys()
|
||||
|
||||
for operation, key in self._x_hotkeys.items():
|
||||
Keybinder.bind(key, self.fire_hook, operation)
|
||||
|
||||
def fire_hook(self, key, operation):
|
||||
"""
|
||||
Fire hook by name.
|
||||
"""
|
||||
assert key
|
||||
getattr(self, operation).fire()
|
||||
|
||||
|
||||
|
||||
hotkey_manager = _HotkeyManager() # pylint: disable=invalid-name
|
||||
|
|
|
@ -131,8 +131,8 @@ class PlayBar(urwid.Pile):
|
|||
meta.APP_NAME,
|
||||
meta.VERSION_WITH_CODENAME
|
||||
)
|
||||
progress = player.get_play_progress_seconds()
|
||||
total = player.get_length_seconds()
|
||||
progress = player.play_progress_seconds
|
||||
total = player.length_seconds
|
||||
return (self.get_style(), u' {} {} - {} {} [{:02d}:{:02d} / {:02d}:{:02d}]'.format(
|
||||
# u'|>' if player.is_playing else u'||',
|
||||
# self.get_rotating_bar(),
|
||||
|
@ -155,7 +155,7 @@ class PlayBar(urwid.Pile):
|
|||
e.g. current track or playback flags.
|
||||
"""
|
||||
self.text.set_text(self.get_text())
|
||||
self.progressbar.set_progress(player.get_play_progress())
|
||||
self.progressbar.set_progress(player.play_progress)
|
||||
self.progressbar.set_done_style(
|
||||
'progressbar_done'
|
||||
if player.is_playing
|
||||
|
|
|
@ -2,3 +2,4 @@ gmusicapi==10.1.2
|
|||
PyYAML==3.13
|
||||
urwid==2.0.0
|
||||
codename==1.1
|
||||
pydbus==0.6.0
|
||||
|
|
Loading…
Add table
Reference in a new issue