15 Commits

Author SHA1 Message Date
Martin Wagner
d2bb684253 fixed description in README.md and appdata file 2020-12-27 20:10:18 +01:00
Martin Wagner
8d01ba7501 fixed typo 2020-12-27 20:06:50 +01:00
Martin Wagner
780469e2c9 preparations for 0.9.8 2020-12-27 20:01:33 +01:00
Martin Wagner
656767e249 fixed translation in flatpak environment 2020-12-27 18:01:45 +01:00
Martin Wagner
60113344e0 set default icon name 2020-12-27 12:09:25 +01:00
Martin Wagner
170c7e6db8 fixed gschema id and path 2020-12-27 11:52:10 +01:00
Martin Wagner
8ad3ffe826 fixed .desktop icon name 2020-12-27 11:48:11 +01:00
Martin Wagner
ae0ade98a5 unified app-id and added an appdata file 2020-12-27 11:42:32 +01:00
Martin Wagner
0269fa87f0 use toggle_play in MPRISInterface.PlayPause 2020-12-24 15:22:54 +01:00
Martin Wagner
170165e0fd small fix in SearchWindow 2020-12-24 13:55:04 +01:00
Martin Wagner
612493ba90 fixed _get_loop_status 2020-12-24 13:47:28 +01:00
Martin Wagner
b6be0f0430 migrated MPRISInterface to Gio 2020-12-24 13:21:08 +01:00
Martin Wagner
7ae0f1e216 improved mpd permissons handling 2020-12-22 12:33:21 +01:00
Martin Wagner
5953d73389 fixed error in PlaybackControl._refresh_tooltips() 2020-12-19 00:14:55 +01:00
Martin Wagner
604b17ea3a added tooltips to prev/next buttons indicating number of tracks before/after current track 2020-12-18 22:12:50 +01:00
16 changed files with 342 additions and 221 deletions

View File

@@ -1,6 +1,6 @@
README for mpdevil
==================
Mpdevil is focused on playing your local music directly instead of managing playlists or playing network streams. So it neither supports saving playlists nor restoring them. Therefore mpdevil is mainly a simple music browser which aims to be easy to use. Instead of maintaining a client side database of your music library mpdevil loads all tags and covers on demand. So you'll never see any outdated information in your browser. Mpdevil strongly relies on tags.
Mpdevil is a simple music browser for the Music Player Daemon (MPD) which is focused on playing local music without the need of managing playlists. Instead of maintaining a client side database of your music library mpdevil loads all tags and covers on demand. So you'll never see any outdated information in the browser. Mpdevil strongly relies on tags.
![ScreenShot](screenshots/mainwindow_0.9.5.png)
@@ -61,7 +61,6 @@ Python modules:
- gi (Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify)
- requests
- bs4 (beautifulsoup)
- dbus
Run:
```bash

View File

@@ -26,21 +26,17 @@ from mpd import MPDClient, base as MPDBase
import requests
from bs4 import BeautifulSoup
import threading
import gettext
gettext.textdomain("mpdevil")
_=gettext.gettext
import datetime
import os
import sys
import re
import gettext
gettext.textdomain("mpdevil")
if os.path.isfile("/.flatpak-info"): # test for flatpak environment
gettext.bindtextdomain("mpdevil", "/app/share/locale")
_=gettext.gettext
# MPRIS modules
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
VERSION="0.9.7" # sync with setup.py
VERSION="0.9.8" # sync with setup.py
COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$"
@@ -48,23 +44,134 @@ COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$"
# MPRIS #
#########
class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
class MPRISInterface: # TODO emit Seeked if needed
"""
based on 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun <eon@patapon.info>, Mantas Mikulėnas <grawity@gmail.com>
based on 'Lollypop' (master 22.12.2020) by Cedric Bellegarde <cedric.bellegarde@adishatz.org>
and 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun <eon@patapon.info>, Mantas Mikulėnas <grawity@gmail.com>
"""
_MPRIS_IFACE="org.mpris.MediaPlayer2"
_MPRIS_PLAYER_IFACE="org.mpris.MediaPlayer2.Player"
_MPRIS_NAME="org.mpris.MediaPlayer2.mpdevil"
_MPRIS_PATH="/org/mpris/MediaPlayer2"
_INTERFACES_XML="""
<!DOCTYPE node PUBLIC
"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" direction="out" type="s"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface" direction="in" type="s"/>
<arg name="property" direction="in" type="s"/>
<arg name="value" direction="out" type="v"/>
</method>
<method name="Set">
<arg name="interface_name" direction="in" type="s"/>
<arg name="property_name" direction="in" type="s"/>
<arg name="value" direction="in" type="v"/>
</method>
<method name="GetAll">
<arg name="interface" direction="in" type="s"/>
<arg name="properties" direction="out" type="a{sv}"/>
</method>
</interface>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise">
</method>
<method name="Quit">
</method>
<property name="CanQuit" 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="SupportedUriSchemes" type="as" access="read"/>
<property name="SupportedMimeTypes" type="as" access="read"/>
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next"/>
<method name="Previous"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Seek">
<arg direction="in" name="Offset" type="x"/>
</method>
<method name="SetPosition">
<arg direction="in" name="TrackId" type="o"/>
<arg direction="in" name="Position" type="x"/>
</method>
<method name="OpenUri">
<arg direction="in" name="Uri" type="s"/>
</method>
<signal name="Seeked">
<arg name="Position" type="x"/>
</signal>
<property name="PlaybackStatus" type="s" access="read"/>
<property name="LoopStatus" type="s" access="readwrite"/>
<property name="Rate" type="d" access="readwrite"/>
<property name="Shuffle" type="b" 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="read"/>
<property name="MaximumRate" type="d" access="read"/>
<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"/>
</interface>
</node>
"""
def __init__(self, window, client, settings):
super().__init__(dbus.SessionBus(), "/org/mpris/MediaPlayer2")
self._name="org.mpris.MediaPlayer2.mpdevil"
def __init__(self, window, client, settings):
# adding vars
self._window=window
self._client=client
self._settings=settings
self._metadata={}
self._bus=dbus.SessionBus()
# MPRIS property mappings
self._prop_mapping={
self._MPRIS_IFACE:
{"CanQuit": (GLib.Variant("b", False), None),
"CanRaise": (GLib.Variant("b", True), None),
"HasTrackList": (GLib.Variant("b", False), None),
"Identity": (GLib.Variant("s", "mpdevil"), None),
"DesktopEntry": (GLib.Variant("s", "org.mpdevil.mpdevil"), None),
"SupportedUriSchemes": (GLib.Variant("s", "None"), None),
"SupportedMimeTypes": (GLib.Variant("s", "None"), None)},
self._MPRIS_PLAYER_IFACE:
{"PlaybackStatus": (self._get_playback_status, None),
"LoopStatus": (self._get_loop_status, self._set_loop_status),
"Rate": (GLib.Variant("d", 1.0), None),
"Shuffle": (self._get_shuffle, self._set_shuffle),
"Metadata": (self._get_metadata, None),
"Volume": (self._get_volume, self._set_volume),
"Position": (self._get_position, None),
"MinimumRate": (GLib.Variant("d", 1.0), None),
"MaximumRate": (GLib.Variant("d", 1.0), None),
"CanGoNext": (self._get_can_next_prev, None),
"CanGoPrevious": (self._get_can_next_prev, None),
"CanPlay": (self._get_can_play_pause_seek, None),
"CanPause": (self._get_can_play_pause_seek, None),
"CanSeek": (self._get_can_play_pause_seek, None),
"CanControl": (GLib.Variant("b", True), None)},
}
# start
self._bus_name=dbus.service.BusName(self._name, bus=self._bus, allow_replacement=True, replace_existing=True) # TODO
self._bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
Gio.bus_own_name_on_connection(self._bus, self._MPRIS_NAME, Gio.BusNameOwnerFlags.NONE, None, None)
self._node_info=Gio.DBusNodeInfo.new_for_xml(self._INTERFACES_XML)
for interface in self._node_info.interfaces:
self._bus.register_object(self._MPRIS_PATH, interface, self._handle_method_call, None, None)
# connect
self._client.emitter.connect("state", self._on_state_changed)
@@ -76,26 +183,25 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
self._client.emitter.connect("connection_error", self._on_connection_error)
self._client.emitter.connect("reconnected", self._on_reconnected)
# Interfaces
__prop_interface=dbus.PROPERTIES_IFACE
__root_interface="org.mpris.MediaPlayer2"
__root_props={
"CanQuit": (False, None),
"CanRaise": (True, None),
"DesktopEntry": ("mpdevil", None),
"HasTrackList": (False, None),
"Identity": ("mpdevil", None),
"SupportedUriSchemes": (dbus.Array(signature="s"), None),
"SupportedMimeTypes": (dbus.Array(signature="s"), None)
}
def _handle_method_call(self, connection, sender, object_path, interface_name, method_name, parameters, invocation):
args=list(parameters.unpack())
result=getattr(self, method_name)(*args)
out_args=self._node_info.lookup_interface(interface_name).lookup_method(method_name).out_args
if out_args == []:
invocation.return_value(None)
else:
signature="("+"".join([arg.signature for arg in out_args])+")"
variant=GLib.Variant(signature, (result,))
invocation.return_value(variant)
def __get_playback_status(self):
# setter and getter
def _get_playback_status(self):
if self._client.connected():
status=self._client.wrapped_call("status")
return {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]]
return "Stopped"
return GLib.Variant("s", {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]])
return GLib.Variant("s", "Stopped")
def __set_loop_status(self, value):
def _set_loop_status(self, value):
if self._client.connected():
if value == "Playlist":
self._client.wrapped_call("repeat", 1)
@@ -106,177 +212,138 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
elif value == "None":
self._client.wrapped_call("repeat", 0)
self._client.wrapped_call("single", 0)
else:
raise dbus.exceptions.DBusException("Loop mode '{}' not supported".format(value))
return
def __get_loop_status(self):
def _get_loop_status(self):
if self._client.connected():
status=self._client.wrapped_call("status")
if int(status["repeat"]) == 1:
if int(status.get("single", 0)) == 1:
return "Track"
if status["repeat"] == "1":
if status.get("single", "0") == "0":
return GLib.Variant("s", "Playlist")
else:
return "Playlist"
return GLib.Variant("s", "Track")
else:
return "None"
return "None"
return GLib.Variant("s", "None")
return GLib.Variant("s", "None")
def __set_shuffle(self, value):
def _set_shuffle(self, value):
if self._client.connected():
self._client.wrapped_call("random", value)
return
def __get_shuffle(self):
if self._client.connected():
if int(self._client.wrapped_call("status")["random"]) == 1:
return True
if value:
self._client.wrapped_call("random", "1")
else:
return False
return False
self._client.wrapped_call("random", "0")
def __get_metadata(self):
return dbus.Dictionary(self._metadata, signature="sv")
def __get_volume(self):
def _get_shuffle(self):
if self._client.connected():
return float(self._client.wrapped_call("status").get("volume", 0))/100
return 0.0
if self._client.wrapped_call("status")["random"] == "1":
return GLib.Variant("b", True)
else:
return GLib.Variant("b", False)
return GLib.Variant("b", False)
def __set_volume(self, value):
def _get_metadata(self):
return GLib.Variant("a{sv}", self._metadata)
def _get_volume(self):
if self._client.connected():
return GLib.Variant("d", float(self._client.wrapped_call("status").get("volume", 0))/100)
return GLib.Variant("d", 0)
def _set_volume(self, value):
if self._client.connected():
if value >= 0 and value <= 1:
self._client.wrapped_call("setvol", int(value * 100))
return
def __get_position(self):
def _get_position(self):
if self._client.connected():
status=self._client.wrapped_call("status")
return dbus.Int64(float(status.get("elapsed", 0))*1000000)
return dbus.Int64(0)
return GLib.Variant("x", float(status.get("elapsed", 0))*1000000)
return GLib.Variant("x", 0)
def __get_can_next_prev(self):
def _get_can_next_prev(self):
if self._client.connected():
status=self._client.wrapped_call("status")
if status["state"] == "stop":
return False
return GLib.Variant("b", False)
else:
return True
return False
return GLib.Variant("b", True)
return GLib.Variant("b", False)
def __get_can_play_pause_seek(self):
return self._client.connected()
def _get_can_play_pause_seek(self):
return GLib.Variant("b", self._client.connected())
__player_interface="org.mpris.MediaPlayer2.Player"
__player_props={
"PlaybackStatus": (__get_playback_status, None),
"LoopStatus": (__get_loop_status, __set_loop_status),
"Rate": (1.0, None),
"Shuffle": (__get_shuffle, __set_shuffle),
"Metadata": (__get_metadata, None),
"Volume": (__get_volume, __set_volume),
"Position": (__get_position, None),
"MinimumRate": (1.0, None),
"MaximumRate": (1.0, None),
"CanGoNext": (__get_can_next_prev, None),
"CanGoPrevious": (__get_can_next_prev, None),
"CanPlay": (__get_can_play_pause_seek, None),
"CanPause": (__get_can_play_pause_seek, None),
"CanSeek": (__get_can_play_pause_seek, None),
"CanControl": (True, None),
}
__prop_mapping={
__player_interface: __player_props,
__root_interface: __root_props,
}
# introspect methods
def Introspect(self):
return self._INTERFACES_XML
# Prop methods
@dbus.service.signal(__prop_interface, signature="sa{sv}as")
def PropertiesChanged(self, interface, changed_properties, invalidated_properties):
pass
@dbus.service.method(__prop_interface, in_signature="ss", out_signature="v")
def Get(self, interface, prop):
getter, setter=self.__prop_mapping[interface][prop]
# property methods
def Get(self, interface_name, prop):
getter, setter=self._prop_mapping[interface_name][prop]
if callable(getter):
return getter(self)
return getter()
return getter
@dbus.service.method(__prop_interface, in_signature="ssv", out_signature="")
def Set(self, interface, prop, value):
getter, setter=self.__prop_mapping[interface][prop]
def Set(self, interface_name, prop, value):
getter, setter=self._prop_mapping[interface_name][prop]
if setter is not None:
setter(self, value)
setter(value)
@dbus.service.method(__prop_interface, in_signature="s", out_signature="a{sv}")
def GetAll(self, interface):
def GetAll(self, interface_name):
read_props={}
props=self.__prop_mapping[interface]
for key, (getter, setter) in props.items():
if callable(getter):
getter=getter(self)
read_props[key]=getter
try:
props=self._prop_mapping[interface_name]
for key, (getter, setter) in props.items():
if callable(getter):
getter=getter()
read_props[key]=getter
except KeyError: # interface has no properties
pass
return read_props
# Root methods
@dbus.service.method(__root_interface, in_signature="", out_signature="")
def PropertiesChanged(self, interface_name, changed_properties, invalidated_properties):
self._bus.emit_signal(
None, self._MPRIS_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged",
GLib.Variant.new_tuple(
GLib.Variant("s", interface_name),
GLib.Variant("a{sv}", changed_properties),
GLib.Variant("as", invalidated_properties)
)
)
# root methods
def Raise(self):
self._window.present()
return
@dbus.service.method(__root_interface, in_signature="", out_signature="")
def Quit(self):
return
app_action_group=self._window.get_action_group("app")
quit_action=app_action_group.lookup_action("quit")
quit_action.activate()
# Player methods
@dbus.service.signal(__player_interface, signature="x")
def Seeked(self, position):
return float(position)
@dbus.service.method(__player_interface, in_signature="", out_signature="")
# player methods
def Next(self):
self._client.wrapped_call("next")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Previous(self):
self._client.wrapped_call("previous")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Pause(self):
self._client.wrapped_call("pause", 1)
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def PlayPause(self):
status=self._client.wrapped_call("status")
if status["state"] == "play":
self._client.wrapped_call("pause", 1)
else:
self._client.wrapped_call("play")
return
self._client.wrapped_call("toggle_play")
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Stop(self):
self._client.wrapped_call("stop")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Play(self):
self._client.wrapped_call("play")
return
@dbus.service.method(__player_interface, in_signature="x", out_signature="")
def Seek(self, offset):
if offset > 0:
offset="+"+str(offset/1000000)
else:
offset=str(offset/1000000)
self._client.wrapped_call("seekcur", offset)
return
@dbus.service.method(__player_interface, in_signature="ox", out_signature="")
def SetPosition(self, trackid, position):
song=self._client.wrapped_call("currentsong")
if str(trackid).split("/")[-1] != song["id"]:
@@ -284,29 +351,17 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
mpd_pos=position/1000000
if mpd_pos >= 0 and mpd_pos <= float(song["duration"]):
self._client.wrapped_call("seekcur", str(mpd_pos))
return
@dbus.service.method(__player_interface, in_signature="s", out_signature="")
def OpenUri(self, uri):
return
pass
# MPRIS implemented metadata tags (all: http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata)
implemented_tags={
"mpris:trackid": dbus.ObjectPath,
"mpris:length": dbus.Int64,
"mpris:artUrl": str,
"xesam:album": str,
"xesam:albumArtist": list,
"xesam:artist": list,
"xesam:composer": list,
"xesam:contentCreated": str,
"xesam:discNumber": int,
"xesam:genre": list,
"xesam:title": str,
"xesam:trackNumber": int,
"xesam:url": str,
}
def Seeked(self, position):
self._bus.emit_signal(
None, self._MPRIS_PATH, self._MPRIS_PLAYER_IFACE, "Seeked",
GLib.Variant.new_tuple(GLib.Variant("x", position))
)
# other methods
def _update_metadata(self):
"""
Translate metadata returned by MPD to the MPRIS v2 syntax.
@@ -315,67 +370,64 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
mpd_meta=self._client.wrapped_call("currentsong") # raw values needed for cover
song=ClientHelper.song_to_list_dict(mpd_meta)
self._metadata={}
for tag, xesam_tag in (("album","album"),("title","title"),("track","trackNumber"),("disc","discNumber"),("date","contentCreated")):
for tag, xesam_tag in (("album","album"),("title","title"),("date","contentCreated")):
if tag in song:
self._metadata["xesam:{}".format(xesam_tag)]=song[tag][0]
self._metadata["xesam:{}".format(xesam_tag)]=GLib.Variant("s", song[tag][0])
for tag, xesam_tag in (("track","trackNumber"),("disc","discNumber")):
if tag in song:
self._metadata["xesam:{}".format(xesam_tag)]=GLib.Variant("i", int(song[tag][0]))
for tag, xesam_tag in (("albumartist","albumArtist"),("artist","artist"),("composer","composer"),("genre","genre")):
if tag in song:
self._metadata["xesam:{}".format(xesam_tag)]=song[tag]
self._metadata["xesam:{}".format(xesam_tag)]=GLib.Variant("as", song[tag])
if "id" in song:
self._metadata["mpris:trackid"]="/org/mpris/MediaPlayer2/Track/{}".format(song["id"][0])
self._metadata["mpris:trackid"]=GLib.Variant("o", self._MPRIS_PATH+"/Track/{}".format(song["id"][0]))
if "time" in song:
self._metadata["mpris:length"]=float(song["duration"][0]) * 1000000
self._metadata["mpris:length"]=GLib.Variant("x", float(song["duration"][0])*1000000)
if "file" in song:
song_file=song["file"][0]
lib_path=self._settings.get_value("paths")[self._settings.get_int("active-profile")]
self._metadata["xesam:url"]="file://{}".format(os.path.join(lib_path, song_file))
self._metadata["xesam:url"]=GLib.Variant("s", "file://{}".format(os.path.join(lib_path, song_file)))
cover=Cover(self._settings, mpd_meta)
if cover.path is not None:
self._metadata["mpris:artUrl"]="file://{}".format(cover.path)
# Cast self._metadata to the correct type, or discard it
for key, value in self._metadata.items():
try:
self._metadata[key]=self.implemented_tags[key](value)
except:
self._metadata[key]=""
self._metadata["mpris:artUrl"]=GLib.Variant("s", "file://{}".format(cover.path))
def _update_property(self, interface, prop):
getter, setter=self.__prop_mapping[interface][prop]
def _update_property(self, interface_name, prop):
getter, setter=self._prop_mapping[interface_name][prop]
if callable(getter):
value=getter(self)
value=getter()
else:
value=getter
self.PropertiesChanged(interface, {prop: value}, [])
self.PropertiesChanged(interface_name, {prop: value}, [])
return value
def _on_state_changed(self, *args):
self._update_property("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
self._update_property("org.mpris.MediaPlayer2.Player", "CanGoNext")
self._update_property("org.mpris.MediaPlayer2.Player", "CanGoPrevious")
self._update_property(self._MPRIS_PLAYER_IFACE, "PlaybackStatus")
self._update_property(self._MPRIS_PLAYER_IFACE, "CanGoNext")
self._update_property(self._MPRIS_PLAYER_IFACE, "CanGoPrevious")
def _on_song_changed(self, *args):
self._update_metadata()
self._update_property("org.mpris.MediaPlayer2.Player", "Metadata")
self._update_property(self._MPRIS_PLAYER_IFACE, "Metadata")
def _on_volume_changed(self, *args):
self._update_property("org.mpris.MediaPlayer2.Player", "Volume")
self._update_property(self._MPRIS_PLAYER_IFACE, "Volume")
def _on_loop_changed(self, *args):
self._update_property("org.mpris.MediaPlayer2.Player", "LoopStatus")
self._update_property(self._MPRIS_PLAYER_IFACE, "LoopStatus")
def _on_random_changed(self, *args):
self._update_property("org.mpris.MediaPlayer2.Player", "Shuffle")
self._update_property(self._MPRIS_PLAYER_IFACE, "Shuffle")
def _on_reconnected(self, *args):
properties=("CanPlay","CanPause","CanSeek")
for p in properties:
self._update_property("org.mpris.MediaPlayer2.Player", p)
self._update_property(self._MPRIS_PLAYER_IFACE, p)
def _on_connection_error(self, *args):
self._metadata={}
properties=("PlaybackStatus","CanGoNext","CanGoPrevious","Metadata","Volume","LoopStatus","Shuffle","CanPlay","CanPause","CanSeek")
for p in properties:
self._update_property("org.mpris.MediaPlayer2.Player", p)
self._update_property(self._MPRIS_PLAYER_IFACE, p)
######################
# MPD client wrapper #
@@ -497,9 +549,15 @@ class Client(MPDClient):
self.emitter.emit("connection_error")
return False
# connect successful
self._main_timeout_id=GLib.timeout_add(self._refresh_interval, self._main_loop)
self.emitter.emit("reconnected")
return True
if "status" in self.commands():
self._main_timeout_id=GLib.timeout_add(self._refresh_interval, self._main_loop)
self.emitter.emit("reconnected")
return True
else:
self.disconnect()
self.emitter.emit("connection_error")
print("No read permission, check your mpd config.")
return False
def reconnect(self):
if self._main_timeout_id is not None:
@@ -650,7 +708,7 @@ class Client(MPDClient):
########################
class Settings(Gio.Settings):
BASE_KEY="org.mpdevil"
BASE_KEY="org.mpdevil.mpdevil"
# temp settings
mini_player=GObject.Property(type=bool, default=False)
cursor_watch=GObject.Property(type=bool, default=False)
@@ -1286,7 +1344,7 @@ class AboutDialog(Gtk.AboutDialog):
self.set_translator_credits("Martin de Reuver\nMartin Wagner")
self.set_website("https://github.com/SoongNoonien/mpdevil")
self.set_copyright("\xa9 2020 Martin Wagner")
self.set_logo_icon_name("mpdevil")
self.set_logo_icon_name("org.mpdevil.mpdevil")
###########################
# general purpose widgets #
@@ -1770,11 +1828,15 @@ class SearchWindow(Gtk.Box):
songs=self._client.wrapped_call("search", self._tag_combo_box.get_active_text(), self.search_entry.get_text())
for s in songs:
song=ClientHelper.song_to_str_dict(ClientHelper.pepare_song_for_display(s))
try:
int_track=int(song["track"])
except:
int_track=0
self._store.append([
song["track"], song["title"],
song["artist"], song["album"],
song["human_duration"], song["file"],
int(song.get("track", 0))
int_track
])
self._hits_label.set_text(_("{num} hits").format(num=self._songs_view.count()))
if self._songs_view.count() == 0:
@@ -2788,17 +2850,22 @@ class PlaylistWindow(Gtk.Box):
pop.popup()
def _on_row_deleted(self, model, path): # sync treeview to mpd
if self._inserted_path is not None: # move
path=int(path.to_string())
if path > self._inserted_path:
path=path-1
if path < self._inserted_path:
self._inserted_path=self._inserted_path-1
self._client.wrapped_call("move", path, self._inserted_path)
self._inserted_path=None
else: # delete
self._client.wrapped_call("delete", path) # bad song index possible
self._playlist_version=self._client.wrapped_call("status")["playlist"]
try:
if self._inserted_path is not None: # move
path=int(path.to_string())
if path > self._inserted_path:
path=path-1
if path < self._inserted_path:
self._inserted_path=self._inserted_path-1
self._client.wrapped_call("move", path, self._inserted_path)
self._inserted_path=None
else: # delete
self._client.wrapped_call("delete", path) # bad song index possible
self._playlist_version=int(self._client.wrapped_call("status")["playlist"])
except MPDBase.CommandError as e:
self._playlist_version=None
self._client.emitter.emit("playlist_changed", int(self._client.wrapped_call("status")["playlist"]))
raise e # propagate exception
def _on_row_inserted(self, model, path, treeiter):
self._inserted_path=int(path.to_string())
@@ -2998,6 +3065,8 @@ class PlaybackControl(Gtk.ButtonBox):
self._settings.connect("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::show-stop", self._on_show_stop_changed)
self._client.emitter.connect("state", self._on_state)
self._client.emitter.connect("playlist_changed", self._refresh_tooltips)
self._client.emitter.connect("current_song_changed", self._refresh_tooltips)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
@@ -3007,6 +3076,18 @@ class PlaybackControl(Gtk.ButtonBox):
self.pack_start(self._stop_button, True, True, 0)
self.pack_start(self._next_button, True, True, 0)
def _refresh_tooltips(self, *args):
try:
songs=self._client.wrapped_call("playlistinfo")
song=int(self._client.wrapped_call("status")["song"])
elapsed=ClientHelper.calc_display_length(songs[:song])
rest=ClientHelper.calc_display_length(songs[song+1:])
self._prev_button.set_tooltip_text(_("{titles} titles ({length})").format(titles=song, length=elapsed))
self._next_button.set_tooltip_text(_("{titles} titles ({length})").format(titles=(len(songs)-(song+1)), length=rest))
except:
self._prev_button.set_tooltip_text("")
self._next_button.set_tooltip_text("")
def _on_play_clicked(self, widget):
self._client.wrapped_call("toggle_play")
@@ -3035,6 +3116,8 @@ class PlaybackControl(Gtk.ButtonBox):
def _on_disconnected(self, *args):
self.set_sensitive(False)
self._prev_button.set_tooltip_text("")
self._next_button.set_tooltip_text("")
def _on_reconnected(self, *args):
self.set_sensitive(True)
@@ -3516,7 +3599,8 @@ class ConnectionNotify(Gtk.Revealer):
class MainWindow(Gtk.ApplicationWindow):
def __init__(self, app, client, settings):
super().__init__(title=("mpdevil"), icon_name="mpdevil", application=app)
super().__init__(title=("mpdevil"), icon_name="org.mpdevil.mpdevil", application=app)
self.set_default_icon_name("org.mpdevil.mpdevil")
Notify.init("mpdevil")
self.set_default_size(settings.get_int("width"), settings.get_int("height"))
if settings.get_boolean("maximize"):
@@ -3781,7 +3865,7 @@ class MainWindow(Gtk.ApplicationWindow):
class mpdevil(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.mpdevil", flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs)
super().__init__(*args, application_id="org.mpdevil.mpdevil", flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs)
self._settings=Settings()
self._client=Client(self._settings)
self._window=None

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2020 Martin Wagner <martin.wagner.dev@gmail.com> -->
<component type="desktop-application">
<id>org.mpdevil.mpdevil</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<name>mpdevil</name>
<summary>A simple music browser for MPD</summary>
<description>
<p>Mpdevil is a simple music browser for the Music Player Daemon (MPD) which is focused on playing local music without the need of managing playlists. Instead of maintaining a client side database of your music library mpdevil loads all tags and covers on demand. So you'll never see any outdated information in the browser. Mpdevil strongly relies on tags.</p>
<ul>
<li>display large covers</li>
<li>play songs without doubleclick</li>
<li>fetch lyrics</li>
<li>MPRIS interface</li>
<li>notifications on title change</li>
<li>basic queue manipulation (move and delete single tracks)</li>
<li>search songs</li>
<li>filter by genre</li>
<li>media keys support</li>
<li>many shortcuts</li>
<li>manage multiple mpd servers</li>
</ul>
</description>
<launchable type="desktop-id">org.mpdevil.mpdevil.desktop</launchable>
<screenshots>
<screenshot type="default">
<image type="source" width="1016" height="1024">https://raw.githubusercontent.com/SoongNoonien/mpdevil/v0.9.7/screenshots/mainwindow_0.9.5.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/SoongNoonien/mpdevil</url>
<url type="bugtracker">https://github.com/SoongNoonien/mpdevil/issues</url>
<provides>
<binary>mpdevil</binary>
</provides>
<update_contact>martin.wagner.dev@gmail.com</update_contact>
</component>

View File

@@ -3,7 +3,7 @@ Name=mpdevil
GenericName=MPD Client
_Comment=A simple music browser for MPD
Exec=mpdevil
Icon=mpdevil
Icon=org.mpdevil.mpdevil
Terminal=false
Type=Application
StartupNotify=true

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.mpdevil" path="/org/mpdevil/">
<schema id="org.mpdevil.mpdevil" path="/org/mpdevil/mpdevil/">
<key type="b" name="maximize">
<default>false</default>
<summary>Maximize mpdevil on startup</summary>

View File

@@ -1,4 +1,4 @@
[encoding: UTF-8]
bin/mpdevil
data/mpdevil.desktop.in
data/org.mpdevil.mpdevil.desktop.in

View File

@@ -4,22 +4,23 @@ import DistUtilsExtra.auto
DistUtilsExtra.auto.setup(
name='mpdevil',
version='0.9.7', # sync with bin/mpdevil
version='0.9.8', # sync with bin/mpdevil
author="Martin Wagner",
author_email="martin.wagner.dev@gmail.com",
description=('A simple music browser for MPD'),
url="https://github.com/SoongNoonien/mpdevil",
license='GPL-3.0',
data_files=[
('share/icons/hicolor/16x16/apps/', ['data/icons/16x16/mpdevil.png']),
('share/icons/hicolor/22x22/apps/', ['data/icons/22x22/mpdevil.png']),
('share/icons/hicolor/24x24/apps/', ['data/icons/24x24/mpdevil.png']),
('share/icons/hicolor/32x32/apps/', ['data/icons/32x32/mpdevil.png']),
('share/icons/hicolor/48x48/apps/', ['data/icons/48x48/mpdevil.png']),
('share/icons/hicolor/64x64/apps/', ['data/icons/64x64/mpdevil.png']),
('share/icons/hicolor/128x128/apps/', ['data/icons/128x128/mpdevil.png']),
('share/icons/hicolor/256x256/apps/', ['data/icons/256x256/mpdevil.png']),
('share/icons/hicolor/scalable/apps/', ['data/icons/scalable/mpdevil.svg'])
('share/metainfo/', ['data/org.mpdevil.mpdevil.appdata.xml']),
('share/icons/hicolor/16x16/apps/', ['data/icons/16x16/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/22x22/apps/', ['data/icons/22x22/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/24x24/apps/', ['data/icons/24x24/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/32x32/apps/', ['data/icons/32x32/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/48x48/apps/', ['data/icons/48x48/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/64x64/apps/', ['data/icons/64x64/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/128x128/apps/', ['data/icons/128x128/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/256x256/apps/', ['data/icons/256x256/org.mpdevil.mpdevil.png']),
('share/icons/hicolor/scalable/apps/', ['data/icons/scalable/org.mpdevil.mpdevil.svg'])
],
)