16 Commits

Author SHA1 Message Date
Martin Wagner
36c25b54c4 added missing tags to appdata 2020-12-27 22:51:16 +01:00
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 349 additions and 221 deletions

View File

@@ -1,6 +1,6 @@
README for mpdevil 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) ![ScreenShot](screenshots/mainwindow_0.9.5.png)
@@ -61,7 +61,6 @@ Python modules:
- gi (Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify) - gi (Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify)
- requests - requests
- bs4 (beautifulsoup) - bs4 (beautifulsoup)
- dbus
Run: Run:
```bash ```bash

View File

@@ -26,21 +26,17 @@ from mpd import MPDClient, base as MPDBase
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import threading import threading
import gettext
gettext.textdomain("mpdevil")
_=gettext.gettext
import datetime import datetime
import os import os
import sys import sys
import re 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 VERSION="0.9.8" # sync with setup.py
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
VERSION="0.9.7" # sync with setup.py
COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$" 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 # # 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 # adding vars
self._window=window self._window=window
self._client=client self._client=client
self._settings=settings self._settings=settings
self._metadata={} 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 # 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 # connect
self._client.emitter.connect("state", self._on_state_changed) 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("connection_error", self._on_connection_error)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
# Interfaces def _handle_method_call(self, connection, sender, object_path, interface_name, method_name, parameters, invocation):
__prop_interface=dbus.PROPERTIES_IFACE args=list(parameters.unpack())
__root_interface="org.mpris.MediaPlayer2" result=getattr(self, method_name)(*args)
__root_props={ out_args=self._node_info.lookup_interface(interface_name).lookup_method(method_name).out_args
"CanQuit": (False, None), if out_args == []:
"CanRaise": (True, None), invocation.return_value(None)
"DesktopEntry": ("mpdevil", None), else:
"HasTrackList": (False, None), signature="("+"".join([arg.signature for arg in out_args])+")"
"Identity": ("mpdevil", None), variant=GLib.Variant(signature, (result,))
"SupportedUriSchemes": (dbus.Array(signature="s"), None), invocation.return_value(variant)
"SupportedMimeTypes": (dbus.Array(signature="s"), None)
}
def __get_playback_status(self): # setter and getter
def _get_playback_status(self):
if self._client.connected(): if self._client.connected():
status=self._client.wrapped_call("status") status=self._client.wrapped_call("status")
return {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]] return GLib.Variant("s", {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]])
return "Stopped" return GLib.Variant("s", "Stopped")
def __set_loop_status(self, value): def _set_loop_status(self, value):
if self._client.connected(): if self._client.connected():
if value == "Playlist": if value == "Playlist":
self._client.wrapped_call("repeat", 1) self._client.wrapped_call("repeat", 1)
@@ -106,177 +212,138 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
elif value == "None": elif value == "None":
self._client.wrapped_call("repeat", 0) self._client.wrapped_call("repeat", 0)
self._client.wrapped_call("single", 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(): if self._client.connected():
status=self._client.wrapped_call("status") status=self._client.wrapped_call("status")
if int(status["repeat"]) == 1: if status["repeat"] == "1":
if int(status.get("single", 0)) == 1: if status.get("single", "0") == "0":
return "Track" return GLib.Variant("s", "Playlist")
else: else:
return "Playlist" return GLib.Variant("s", "Track")
else: else:
return "None" return GLib.Variant("s", "None")
return "None" return GLib.Variant("s", "None")
def __set_shuffle(self, value): def _set_shuffle(self, value):
if self._client.connected(): if self._client.connected():
self._client.wrapped_call("random", value) if value:
return self._client.wrapped_call("random", "1")
def __get_shuffle(self):
if self._client.connected():
if int(self._client.wrapped_call("status")["random"]) == 1:
return True
else: else:
return False self._client.wrapped_call("random", "0")
return False
def __get_metadata(self): def _get_shuffle(self):
return dbus.Dictionary(self._metadata, signature="sv")
def __get_volume(self):
if self._client.connected(): if self._client.connected():
return float(self._client.wrapped_call("status").get("volume", 0))/100 if self._client.wrapped_call("status")["random"] == "1":
return 0.0 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 self._client.connected():
if value >= 0 and value <= 1: if value >= 0 and value <= 1:
self._client.wrapped_call("setvol", int(value * 100)) self._client.wrapped_call("setvol", int(value * 100))
return
def __get_position(self): def _get_position(self):
if self._client.connected(): if self._client.connected():
status=self._client.wrapped_call("status") status=self._client.wrapped_call("status")
return dbus.Int64(float(status.get("elapsed", 0))*1000000) return GLib.Variant("x", float(status.get("elapsed", 0))*1000000)
return dbus.Int64(0) return GLib.Variant("x", 0)
def __get_can_next_prev(self): def _get_can_next_prev(self):
if self._client.connected(): if self._client.connected():
status=self._client.wrapped_call("status") status=self._client.wrapped_call("status")
if status["state"] == "stop": if status["state"] == "stop":
return False return GLib.Variant("b", False)
else: else:
return True return GLib.Variant("b", True)
return False return GLib.Variant("b", False)
def __get_can_play_pause_seek(self): def _get_can_play_pause_seek(self):
return self._client.connected() return GLib.Variant("b", self._client.connected())
__player_interface="org.mpris.MediaPlayer2.Player" # introspect methods
__player_props={ def Introspect(self):
"PlaybackStatus": (__get_playback_status, None), return self._INTERFACES_XML
"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,
}
# Prop methods # property methods
@dbus.service.signal(__prop_interface, signature="sa{sv}as") def Get(self, interface_name, prop):
def PropertiesChanged(self, interface, changed_properties, invalidated_properties): getter, setter=self._prop_mapping[interface_name][prop]
pass
@dbus.service.method(__prop_interface, in_signature="ss", out_signature="v")
def Get(self, interface, prop):
getter, setter=self.__prop_mapping[interface][prop]
if callable(getter): if callable(getter):
return getter(self) return getter()
return getter return getter
@dbus.service.method(__prop_interface, in_signature="ssv", out_signature="") def Set(self, interface_name, prop, value):
def Set(self, interface, prop, value): getter, setter=self._prop_mapping[interface_name][prop]
getter, setter=self.__prop_mapping[interface][prop]
if setter is not None: 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_name):
def GetAll(self, interface):
read_props={} read_props={}
props=self.__prop_mapping[interface] try:
for key, (getter, setter) in props.items(): props=self._prop_mapping[interface_name]
if callable(getter): for key, (getter, setter) in props.items():
getter=getter(self) if callable(getter):
read_props[key]=getter getter=getter()
read_props[key]=getter
except KeyError: # interface has no properties
pass
return read_props return read_props
# Root methods def PropertiesChanged(self, interface_name, changed_properties, invalidated_properties):
@dbus.service.method(__root_interface, in_signature="", out_signature="") 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): def Raise(self):
self._window.present() self._window.present()
return
@dbus.service.method(__root_interface, in_signature="", out_signature="")
def Quit(self): 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 # 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="")
def Next(self): def Next(self):
self._client.wrapped_call("next") self._client.wrapped_call("next")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Previous(self): def Previous(self):
self._client.wrapped_call("previous") self._client.wrapped_call("previous")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Pause(self): def Pause(self):
self._client.wrapped_call("pause", 1) self._client.wrapped_call("pause", 1)
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def PlayPause(self): def PlayPause(self):
status=self._client.wrapped_call("status") self._client.wrapped_call("toggle_play")
if status["state"] == "play":
self._client.wrapped_call("pause", 1)
else:
self._client.wrapped_call("play")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Stop(self): def Stop(self):
self._client.wrapped_call("stop") self._client.wrapped_call("stop")
return
@dbus.service.method(__player_interface, in_signature="", out_signature="")
def Play(self): def Play(self):
self._client.wrapped_call("play") self._client.wrapped_call("play")
return
@dbus.service.method(__player_interface, in_signature="x", out_signature="")
def Seek(self, offset): def Seek(self, offset):
if offset > 0: if offset > 0:
offset="+"+str(offset/1000000) offset="+"+str(offset/1000000)
else: else:
offset=str(offset/1000000) offset=str(offset/1000000)
self._client.wrapped_call("seekcur", offset) self._client.wrapped_call("seekcur", offset)
return
@dbus.service.method(__player_interface, in_signature="ox", out_signature="")
def SetPosition(self, trackid, position): def SetPosition(self, trackid, position):
song=self._client.wrapped_call("currentsong") song=self._client.wrapped_call("currentsong")
if str(trackid).split("/")[-1] != song["id"]: 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 mpd_pos=position/1000000
if mpd_pos >= 0 and mpd_pos <= float(song["duration"]): if mpd_pos >= 0 and mpd_pos <= float(song["duration"]):
self._client.wrapped_call("seekcur", str(mpd_pos)) self._client.wrapped_call("seekcur", str(mpd_pos))
return
@dbus.service.method(__player_interface, in_signature="s", out_signature="")
def OpenUri(self, uri): def OpenUri(self, uri):
return pass
# MPRIS implemented metadata tags (all: http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata) def Seeked(self, position):
implemented_tags={ self._bus.emit_signal(
"mpris:trackid": dbus.ObjectPath, None, self._MPRIS_PATH, self._MPRIS_PLAYER_IFACE, "Seeked",
"mpris:length": dbus.Int64, GLib.Variant.new_tuple(GLib.Variant("x", position))
"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,
}
# other methods
def _update_metadata(self): def _update_metadata(self):
""" """
Translate metadata returned by MPD to the MPRIS v2 syntax. 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 mpd_meta=self._client.wrapped_call("currentsong") # raw values needed for cover
song=ClientHelper.song_to_list_dict(mpd_meta) song=ClientHelper.song_to_list_dict(mpd_meta)
self._metadata={} 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: 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")): for tag, xesam_tag in (("albumartist","albumArtist"),("artist","artist"),("composer","composer"),("genre","genre")):
if tag in song: 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: 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: 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: if "file" in song:
song_file=song["file"][0] song_file=song["file"][0]
lib_path=self._settings.get_value("paths")[self._settings.get_int("active-profile")] 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) cover=Cover(self._settings, mpd_meta)
if cover.path is not None: if cover.path is not None:
self._metadata["mpris:artUrl"]="file://{}".format(cover.path) self._metadata["mpris:artUrl"]=GLib.Variant("s", "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]=""
def _update_property(self, interface, prop): def _update_property(self, interface_name, prop):
getter, setter=self.__prop_mapping[interface][prop] getter, setter=self._prop_mapping[interface_name][prop]
if callable(getter): if callable(getter):
value=getter(self) value=getter()
else: else:
value=getter value=getter
self.PropertiesChanged(interface, {prop: value}, []) self.PropertiesChanged(interface_name, {prop: value}, [])
return value return value
def _on_state_changed(self, *args): def _on_state_changed(self, *args):
self._update_property("org.mpris.MediaPlayer2.Player", "PlaybackStatus") self._update_property(self._MPRIS_PLAYER_IFACE, "PlaybackStatus")
self._update_property("org.mpris.MediaPlayer2.Player", "CanGoNext") self._update_property(self._MPRIS_PLAYER_IFACE, "CanGoNext")
self._update_property("org.mpris.MediaPlayer2.Player", "CanGoPrevious") self._update_property(self._MPRIS_PLAYER_IFACE, "CanGoPrevious")
def _on_song_changed(self, *args): def _on_song_changed(self, *args):
self._update_metadata() 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): 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): 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): 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): def _on_reconnected(self, *args):
properties=("CanPlay","CanPause","CanSeek") properties=("CanPlay","CanPause","CanSeek")
for p in properties: 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): def _on_connection_error(self, *args):
self._metadata={} self._metadata={}
properties=("PlaybackStatus","CanGoNext","CanGoPrevious","Metadata","Volume","LoopStatus","Shuffle","CanPlay","CanPause","CanSeek") properties=("PlaybackStatus","CanGoNext","CanGoPrevious","Metadata","Volume","LoopStatus","Shuffle","CanPlay","CanPause","CanSeek")
for p in properties: for p in properties:
self._update_property("org.mpris.MediaPlayer2.Player", p) self._update_property(self._MPRIS_PLAYER_IFACE, p)
###################### ######################
# MPD client wrapper # # MPD client wrapper #
@@ -497,9 +549,15 @@ class Client(MPDClient):
self.emitter.emit("connection_error") self.emitter.emit("connection_error")
return False return False
# connect successful # connect successful
self._main_timeout_id=GLib.timeout_add(self._refresh_interval, self._main_loop) if "status" in self.commands():
self.emitter.emit("reconnected") self._main_timeout_id=GLib.timeout_add(self._refresh_interval, self._main_loop)
return True 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): def reconnect(self):
if self._main_timeout_id is not None: if self._main_timeout_id is not None:
@@ -650,7 +708,7 @@ class Client(MPDClient):
######################## ########################
class Settings(Gio.Settings): class Settings(Gio.Settings):
BASE_KEY="org.mpdevil" BASE_KEY="org.mpdevil.mpdevil"
# temp settings # temp settings
mini_player=GObject.Property(type=bool, default=False) mini_player=GObject.Property(type=bool, default=False)
cursor_watch=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_translator_credits("Martin de Reuver\nMartin Wagner")
self.set_website("https://github.com/SoongNoonien/mpdevil") self.set_website("https://github.com/SoongNoonien/mpdevil")
self.set_copyright("\xa9 2020 Martin Wagner") self.set_copyright("\xa9 2020 Martin Wagner")
self.set_logo_icon_name("mpdevil") self.set_logo_icon_name("org.mpdevil.mpdevil")
########################### ###########################
# general purpose widgets # # 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()) songs=self._client.wrapped_call("search", self._tag_combo_box.get_active_text(), self.search_entry.get_text())
for s in songs: for s in songs:
song=ClientHelper.song_to_str_dict(ClientHelper.pepare_song_for_display(s)) 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([ self._store.append([
song["track"], song["title"], song["track"], song["title"],
song["artist"], song["album"], song["artist"], song["album"],
song["human_duration"], song["file"], 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())) self._hits_label.set_text(_("{num} hits").format(num=self._songs_view.count()))
if self._songs_view.count() == 0: if self._songs_view.count() == 0:
@@ -2788,17 +2850,22 @@ class PlaylistWindow(Gtk.Box):
pop.popup() pop.popup()
def _on_row_deleted(self, model, path): # sync treeview to mpd def _on_row_deleted(self, model, path): # sync treeview to mpd
if self._inserted_path is not None: # move try:
path=int(path.to_string()) if self._inserted_path is not None: # move
if path > self._inserted_path: path=int(path.to_string())
path=path-1 if path > self._inserted_path:
if path < self._inserted_path: path=path-1
self._inserted_path=self._inserted_path-1 if path < self._inserted_path:
self._client.wrapped_call("move", path, self._inserted_path) self._inserted_path=self._inserted_path-1
self._inserted_path=None self._client.wrapped_call("move", path, self._inserted_path)
else: # delete self._inserted_path=None
self._client.wrapped_call("delete", path) # bad song index possible else: # delete
self._playlist_version=self._client.wrapped_call("status")["playlist"] 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): def _on_row_inserted(self, model, path, treeiter):
self._inserted_path=int(path.to_string()) 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("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::show-stop", self._on_show_stop_changed) self._settings.connect("changed::show-stop", self._on_show_stop_changed)
self._client.emitter.connect("state", self._on_state) 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("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) 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._stop_button, True, True, 0)
self.pack_start(self._next_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): def _on_play_clicked(self, widget):
self._client.wrapped_call("toggle_play") self._client.wrapped_call("toggle_play")
@@ -3035,6 +3116,8 @@ class PlaybackControl(Gtk.ButtonBox):
def _on_disconnected(self, *args): def _on_disconnected(self, *args):
self.set_sensitive(False) self.set_sensitive(False)
self._prev_button.set_tooltip_text("")
self._next_button.set_tooltip_text("")
def _on_reconnected(self, *args): def _on_reconnected(self, *args):
self.set_sensitive(True) self.set_sensitive(True)
@@ -3516,7 +3599,8 @@ class ConnectionNotify(Gtk.Revealer):
class MainWindow(Gtk.ApplicationWindow): class MainWindow(Gtk.ApplicationWindow):
def __init__(self, app, client, settings): 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") Notify.init("mpdevil")
self.set_default_size(settings.get_int("width"), settings.get_int("height")) self.set_default_size(settings.get_int("width"), settings.get_int("height"))
if settings.get_boolean("maximize"): if settings.get_boolean("maximize"):
@@ -3781,7 +3865,7 @@ class MainWindow(Gtk.ApplicationWindow):
class mpdevil(Gtk.Application): class mpdevil(Gtk.Application):
def __init__(self, *args, **kwargs): 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._settings=Settings()
self._client=Client(self._settings) self._client=Client(self._settings)
self._window=None 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,44 @@
<?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>
<releases>
<release version="0.9.8" date="2020-12-27">
<description>
</description>
</release>
</releases>
<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>
<content_rating type="oars-1.1" />
<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 GenericName=MPD Client
_Comment=A simple music browser for MPD _Comment=A simple music browser for MPD
Exec=mpdevil Exec=mpdevil
Icon=mpdevil Icon=org.mpdevil.mpdevil
Terminal=false Terminal=false
Type=Application Type=Application
StartupNotify=true StartupNotify=true

View File

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

View File

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

View File

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