mpdevil/bin/mpdevil

3793 lines
135 KiB
Plaintext
Raw Normal View History

2020-01-19 22:48:49 +03:00
#!/usr/bin/python3
2020-01-11 13:25:15 +03:00
# -*- coding: utf-8 -*-
#
# mpdevil - MPD Client.
2020-07-04 00:11:33 +03:00
# Copyright 2020 Martin Wagner <martin.wagner.dev@gmail.com>
2020-01-11 13:25:15 +03:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
2020-03-22 19:05:51 +03:00
import gi
2020-09-24 22:17:10 +03:00
gi.require_version("Gtk", "3.0")
gi.require_version("Notify", "0.7")
2020-03-03 17:59:18 +03:00
from gi.repository import Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify
from mpd import MPDClient, base as MPDBase
2020-09-24 17:50:19 +03:00
import requests
from bs4 import BeautifulSoup
import threading
2020-01-11 13:25:15 +03:00
import locale
import gettext
2020-09-24 22:17:10 +03:00
gettext.textdomain("mpdevil")
_=gettext.gettext
2020-01-11 13:25:15 +03:00
import datetime
import os
import sys
2020-04-07 19:02:43 +03:00
import re
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# MPRIS modules
2020-03-21 00:09:13 +03:00
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
2020-03-21 00:09:13 +03:00
2020-10-04 20:28:04 +03:00
VERSION="0.9.4" # sync with setup.py
2020-09-24 22:17:10 +03:00
COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$"
2020-03-22 19:05:51 +03:00
2020-07-04 14:16:17 +03:00
#########
# MPRIS #
#########
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
class MPRISInterface(dbus.service.Object): # 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>
"""
2020-07-04 14:16:17 +03:00
def __init__(self, window, client, settings):
2020-08-31 11:44:23 +03:00
super().__init__(dbus.SessionBus(), "/org/mpris/MediaPlayer2")
2020-07-04 14:16:17 +03:00
self._name="org.mpris.MediaPlayer2.mpdevil"
2020-09-25 13:24:35 +03:00
# adding vars
self._window=window
self._client=client
self._settings=settings
self._metadata={}
2020-09-25 13:24:35 +03:00
self._bus=dbus.SessionBus()
2020-03-30 18:08:59 +03:00
2020-09-25 18:13:39 +03:00
# start
self._bus_name=dbus.service.BusName(self._name, bus=self._bus, allow_replacement=True, replace_existing=True) # TODO
2020-07-04 14:16:17 +03:00
# connect
self._client.emitter.connect("state", self._on_state_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed)
self._client.emitter.connect("volume_changed", self._on_volume_changed)
self._client.emitter.connect("repeat", self._on_loop_changed)
self._client.emitter.connect("single", self._on_loop_changed)
self._client.emitter.connect("random", self._on_random_changed)
2020-09-25 18:13:39 +03:00
self._client.emitter.connect("connection_error", self._on_connection_error)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-09-25 13:24:35 +03:00
# Interfaces
__prop_interface=dbus.PROPERTIES_IFACE
2020-07-04 14:16:17 +03:00
__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)
}
2020-03-30 12:54:04 +03:00
2020-07-04 14:16:17 +03:00
def __get_playback_status(self):
2020-09-25 18:13:39 +03:00
if self._client.connected():
status=self._client.wrapped_call("status")
return {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]]
return "Stopped"
2020-07-04 14:16:17 +03:00
def __set_loop_status(self, value):
2020-09-25 18:13:39 +03:00
if self._client.connected():
if value == "Playlist":
self._client.wrapped_call("repeat", 1)
self._client.wrapped_call("single", 0)
elif value == "Track":
self._client.wrapped_call("repeat", 1)
self._client.wrapped_call("single", 1)
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))
2020-07-04 14:16:17 +03:00
return
2020-07-04 14:16:17 +03:00
def __get_loop_status(self):
2020-09-25 18:13:39 +03:00
if self._client.connected():
status=self._client.wrapped_call("status")
if int(status["repeat"]) == 1:
if int(status.get("single", 0)) == 1:
return "Track"
else:
return "Playlist"
2020-07-04 14:16:17 +03:00
else:
2020-09-25 18:13:39 +03:00
return "None"
return "None"
2020-07-04 14:16:17 +03:00
def __set_shuffle(self, value):
2020-09-25 18:13:39 +03:00
if self._client.connected():
self._client.wrapped_call("random", value)
2020-07-04 14:16:17 +03:00
return
def __get_shuffle(self):
2020-09-25 18:13:39 +03:00
if self._client.connected():
if int(self._client.wrapped_call("status")["random"]) == 1:
return True
else:
return False
return False
2020-07-04 14:16:17 +03:00
def __get_metadata(self):
2020-09-24 22:17:10 +03:00
return dbus.Dictionary(self._metadata, signature="sv")
2020-03-30 12:54:04 +03:00
2020-07-04 14:16:17 +03:00
def __get_volume(self):
2020-09-25 18:13:39 +03:00
if self._client.connected():
return float(self._client.wrapped_call("status").get("volume", 0))/100
return 0.0
2020-07-04 14:16:17 +03:00
def __set_volume(self, value):
2020-09-25 18:13:39 +03:00
if self._client.connected():
if value >= 0 and value <= 1:
self._client.wrapped_call("setvol", int(value * 100))
2020-07-04 14:16:17 +03:00
return
2020-03-30 12:54:04 +03:00
2020-07-04 14:16:17 +03:00
def __get_position(self):
2020-09-25 18:13:39 +03:00
if self._client.connected():
status=self._client.wrapped_call("status")
return dbus.Int64(float(status.get("elapsed", 0))*1000000)
return dbus.Int64(0)
2020-07-04 14:16:17 +03:00
def __get_can_next_prev(self):
2020-09-25 18:13:39 +03:00
if self._client.connected():
status=self._client.wrapped_call("status")
if status["state"] == "stop":
return False
else:
return True
return False
def __get_can_play_pause_seek(self):
return self._client.connected()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
__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),
2020-09-25 18:13:39 +03:00
"CanPlay": (__get_can_play_pause_seek, None),
"CanPause": (__get_can_play_pause_seek, None),
"CanSeek": (__get_can_play_pause_seek, None),
2020-07-04 14:16:17 +03:00
"CanControl": (True, None),
}
__prop_mapping={
__player_interface: __player_props,
__root_interface: __root_props,
2020-04-07 19:02:43 +03:00
}
2020-09-25 13:24:35 +03:00
# Prop methods
2020-07-04 14:16:17 +03:00
@dbus.service.signal(__prop_interface, signature="sa{sv}as")
def PropertiesChanged(self, interface, changed_properties, invalidated_properties):
pass
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
@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):
return getter(self)
return getter
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
@dbus.service.method(__prop_interface, in_signature="ssv", out_signature="")
def Set(self, interface, prop, value):
getter, setter=self.__prop_mapping[interface][prop]
if setter is not None:
setter(self, value)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
@dbus.service.method(__prop_interface, in_signature="s", out_signature="a{sv}")
def GetAll(self, interface):
read_props={}
props=self.__prop_mapping[interface]
for key, (getter, setter) in props.items():
if callable(getter):
getter=getter(self)
read_props[key]=getter
return read_props
# Root methods
2020-09-24 22:17:10 +03:00
@dbus.service.method(__root_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Raise(self):
self._window.present()
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__root_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Quit(self):
return
# Player methods
2020-09-25 13:24:35 +03:00
@dbus.service.signal(__player_interface, signature="x")
def Seeked(self, position):
return float(position)
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Next(self):
self._client.wrapped_call("next")
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Previous(self):
self._client.wrapped_call("previous")
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Pause(self):
self._client.wrapped_call("pause", 1)
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def PlayPause(self):
status=self._client.wrapped_call("status")
2020-09-24 22:17:10 +03:00
if status["state"] == "play":
self._client.wrapped_call("pause", 1)
2020-07-04 14:16:17 +03:00
else:
self._client.wrapped_call("play")
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Stop(self):
self._client.wrapped_call("stop")
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="", out_signature="")
2020-07-04 14:16:17 +03:00
def Play(self):
self._client.wrapped_call("play")
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="x", out_signature="")
2020-09-25 13:24:35 +03:00
def Seek(self, offset):
if offset > 0:
offset="+"+str(offset/1000000)
else:
offset=str(offset/1000000)
self._client.wrapped_call("seekcur", offset)
2020-07-04 14:16:17 +03:00
return
2020-09-24 22:17:10 +03:00
@dbus.service.method(__player_interface, in_signature="ox", out_signature="")
2020-07-04 14:16:17 +03:00
def SetPosition(self, trackid, position):
song=self._client.wrapped_call("currentsong")
2020-09-25 13:24:35 +03:00
if str(trackid).split("/")[-1] != song["id"]:
2020-07-04 14:16:17 +03:00
return
2020-09-25 13:24:35 +03:00
mpd_pos=position/1000000
if mpd_pos >= 0 and mpd_pos <= float(song["duration"]):
self._client.wrapped_call("seekcur", str(mpd_pos))
2020-07-04 14:16:17 +03:00
return
2020-09-25 13:24:35 +03:00
@dbus.service.method(__player_interface, in_signature="s", out_signature="")
def OpenUri(self, uri):
2020-07-04 14:16:17 +03:00
return
2020-09-25 13:24:35 +03:00
# 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 _update_metadata(self):
"""
Translate metadata returned by MPD to the MPRIS v2 syntax.
http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
"""
mpd_meta=self._client.wrapped_call("currentsong")
2020-09-26 14:29:31 +03:00
self._metadata={}
2020-09-25 13:24:35 +03:00
for tag, xesam_tag in (("album","album"),("title","title"),("track","trackNumber"),("disc","discNumber"),("date","contentCreated")):
if tag in mpd_meta:
self._metadata["xesam:{}".format(xesam_tag)]=mpd_meta[tag]
for tag, xesam_tag in (("albumartist","albumArtist"),("artist","artist"),("composer","composer"),("genre","genre")):
if tag in mpd_meta:
if type(mpd_meta[tag]) == list:
self._metadata["xesam:{}".format(xesam_tag)]=mpd_meta[tag]
else:
self._metadata["xesam:{}".format(xesam_tag)]=[mpd_meta[tag]]
if "id" in mpd_meta:
self._metadata["mpris:trackid"]="/org/mpris/MediaPlayer2/Track/{}".format(mpd_meta["id"])
if "time" in mpd_meta:
self._metadata["mpris:length"]=float(mpd_meta["duration"]) * 1000000
if "file" in mpd_meta:
song_file=mpd_meta["file"]
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))
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 ValueError:
del self._metadata[key]
def _update_property(self, interface, prop):
getter, setter=self.__prop_mapping[interface][prop]
if callable(getter):
value=getter(self)
else:
value=getter
self.PropertiesChanged(interface, {prop: value}, [])
return value
2020-08-21 19:03:36 +03:00
def _on_state_changed(self, *args):
2020-09-25 13:24:35 +03:00
self._update_property("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
self._update_property("org.mpris.MediaPlayer2.Player", "CanGoNext")
self._update_property("org.mpris.MediaPlayer2.Player", "CanGoPrevious")
2020-08-21 19:03:36 +03:00
def _on_song_changed(self, *args):
2020-09-25 13:24:35 +03:00
self._update_metadata()
self._update_property("org.mpris.MediaPlayer2.Player", "Metadata")
2020-08-21 19:03:36 +03:00
def _on_volume_changed(self, *args):
2020-09-25 13:24:35 +03:00
self._update_property("org.mpris.MediaPlayer2.Player", "Volume")
2020-08-21 19:03:36 +03:00
def _on_loop_changed(self, *args):
2020-09-25 13:24:35 +03:00
self._update_property("org.mpris.MediaPlayer2.Player", "LoopStatus")
2020-08-21 19:03:36 +03:00
def _on_random_changed(self, *args):
2020-09-25 13:24:35 +03:00
self._update_property("org.mpris.MediaPlayer2.Player", "Shuffle")
2020-08-21 19:03:36 +03:00
def _on_reconnected(self, *args):
2020-09-25 18:13:39 +03:00
properties=("CanPlay","CanPause","CanSeek")
for p in properties:
self._update_property("org.mpris.MediaPlayer2.Player", p)
2020-09-25 18:13:39 +03:00
def _on_connection_error(self, *args):
self._metadata={}
2020-09-25 18:13:39 +03:00
properties=("PlaybackStatus","CanGoNext","CanGoPrevious","Metadata","Volume","LoopStatus","Shuffle","CanPlay","CanPause","CanSeek")
for p in properties:
self._update_property("org.mpris.MediaPlayer2.Player", p)
2020-07-04 14:16:17 +03:00
######################
# MPD client wrapper #
######################
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class ClientHelper():
2020-09-30 12:06:00 +03:00
def seconds_to_display_time(seconds):
raw_time_string=str(datetime.timedelta(seconds=seconds))
stript_time_string=raw_time_string.lstrip("0").lstrip(":")
return stript_time_string.replace(":", "") # use 'ratio' as delimiter
2020-07-04 14:16:17 +03:00
def song_to_str_dict(song): # converts tags with multiple values to comma separated strings
return_song=song
for tag, value in return_song.items():
if type(value) == list:
2020-09-24 22:17:10 +03:00
return_song[tag]=(", ".join(value))
2020-07-04 14:16:17 +03:00
return return_song
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def song_to_first_str_dict(song): # extracts the first value of multiple value tags
return_song=song
for tag, value in return_song.items():
if type(value) == list:
return_song[tag]=value[0]
return return_song
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def extend_song_for_display(song):
2020-09-12 15:31:17 +03:00
base_song={
"title": _("Unknown Title"),
"track": "",
2020-09-12 15:31:17 +03:00
"disc": "",
"artist": "",
"album": "",
2020-09-12 15:31:17 +03:00
"duration": "0.0",
"date": "",
"genre": ""
}
2020-10-02 09:15:34 +03:00
if "range" in song: # translate .cue 'range' to 'duration' if needed
start, end=song["range"].split("-")
if start != "" and end != "":
base_song["duration"]=str((float(end)-float(start)))
2020-07-04 14:16:17 +03:00
base_song.update(song)
2020-09-30 12:06:00 +03:00
base_song["human_duration"]=ClientHelper.seconds_to_display_time(int(float(base_song["duration"])))
2020-07-04 14:16:17 +03:00
return base_song
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def calc_display_length(songs):
length=float(0)
for song in songs:
2020-09-23 17:21:24 +03:00
length=length+float(song.get("duration", 0.0))
2020-09-30 12:06:00 +03:00
return ClientHelper.seconds_to_display_time(int(length))
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class MpdEventEmitter(GObject.Object):
__gsignals__={
2020-09-24 22:17:10 +03:00
"update": (GObject.SignalFlags.RUN_FIRST, None, ()),
"disconnected": (GObject.SignalFlags.RUN_FIRST, None, ()),
"reconnected": (GObject.SignalFlags.RUN_FIRST, None, ()),
"connection_error": (GObject.SignalFlags.RUN_FIRST, None, ()),
"current_song_changed": (GObject.SignalFlags.RUN_FIRST, None, ()),
"state": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
"elapsed_changed": (GObject.SignalFlags.RUN_FIRST, None, (float,float,)),
"volume_changed": (GObject.SignalFlags.RUN_FIRST, None, (float,)),
"playlist_changed": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
"repeat": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"random": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"single": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"consume": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"audio": (GObject.SignalFlags.RUN_FIRST, None, (str,str,str,)),
"bitrate": (GObject.SignalFlags.RUN_FIRST, None, (float,))
2020-03-21 00:09:13 +03:00
}
2020-07-04 14:16:17 +03:00
def __init__(self):
super().__init__()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class Client(MPDClient):
def __init__(self, settings):
2020-08-31 11:44:23 +03:00
super().__init__()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self._settings=settings
2020-07-04 14:16:17 +03:00
self.emitter=MpdEventEmitter()
self._last_status={}
self._refresh_interval=self._settings.get_int("refresh-interval")
self._main_timeout_id=None
2020-03-21 00:09:13 +03:00
2020-08-15 12:56:44 +03:00
#connect
self._settings.connect("changed::active-profile", self._on_active_profile_changed)
2020-08-15 12:56:44 +03:00
def wrapped_call(self, name, *args):
try:
func=getattr(self, name)
except:
raise ValueError
return func(*args)
2020-07-04 14:16:17 +03:00
def start(self):
self.emitter.emit("disconnected") # bring player in defined state
active=self._settings.get_int("active-profile")
try:
self.connect(self._settings.get_value("hosts")[active], self._settings.get_value("ports")[active])
if self._settings.get_value("passwords")[active] != "":
self.password(self._settings.get_value("passwords")[active])
except:
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
def reconnect(self):
if self._main_timeout_id is not None:
GLib.source_remove(self._main_timeout_id)
self._main_timeout_id=None
self._last_status={}
self.disconnect()
self.start()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def connected(self):
try:
self.wrapped_call("ping")
2020-07-04 14:16:17 +03:00
return True
except:
return False
2020-03-21 00:09:13 +03:00
2020-08-16 18:07:41 +03:00
def files_to_playlist(self, files, mode="default"): # modes: default, play, append, enqueue
def append(files):
2020-07-04 14:16:17 +03:00
for f in files:
self.add(f)
2020-08-16 18:07:41 +03:00
def play(files):
2020-08-21 13:03:42 +03:00
if files != []:
2020-08-16 18:07:41 +03:00
self.clear()
for f in files:
self.add(f)
self.play()
def enqueue(files):
status=self.status()
if status["state"] == "stop":
play(files)
2020-07-04 14:16:17 +03:00
else:
self.moveid(status["songid"], 0)
current_song_file=self.playlistinfo()[0]["file"]
try:
self.delete((1,)) # delete all songs, but the first. bad song index possible
except:
pass
for f in files:
2020-08-21 13:03:42 +03:00
if f == current_song_file:
2020-07-04 14:16:17 +03:00
self.move(0, (len(self.playlistinfo())-1))
2020-08-21 13:03:42 +03:00
else:
self.add(f)
2020-08-16 18:07:41 +03:00
if mode == "append":
append(files)
elif mode == "enqueue":
enqueue(files)
elif mode == "play":
play(files)
elif mode == "default":
if self._settings.get_boolean("force-mode"):
2020-08-16 18:07:41 +03:00
play(files)
else:
enqueue(files)
2020-03-21 00:09:13 +03:00
2020-08-16 18:07:41 +03:00
def album_to_playlist(self, album, artist, year, mode="default"):
songs=self.find("album", album, "date", year, self._settings.get_artist_type(), artist)
2020-09-24 22:17:10 +03:00
self.files_to_playlist([song["file"] for song in songs], mode)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def comp_list(self, *args): # simulates listing behavior of python-mpd2 1.0
native_list=self.list(*args)
if len(native_list) > 0:
if type(native_list[0]) == dict:
return ([l[args[0]] for l in native_list])
else:
return native_list
else:
return([])
def get_metadata(self, uri):
meta_base=self.lsinfo(uri)[0]
2020-10-02 09:15:34 +03:00
try: # .cue files produce an error here
meta_extra=self.readcomments(uri) # contains comment tag
meta_base.update(meta_extra)
except:
pass
return meta_base
2020-09-29 13:39:21 +03:00
def toggle_play(self):
status=self.status()
if status["state"] == "play":
self.pause(1)
elif status["state"] == "pause":
self.pause(0)
else:
try:
self.play()
except:
pass
2020-09-29 14:02:51 +03:00
def toggle_option(self, option): # repeat, random, single, consume
2020-09-29 13:39:21 +03:00
new_state=(int(self.status()[option])+1)%2 # toggle 0,1
func=getattr(self, option)
func(new_state)
def _main_loop(self, *args):
2020-07-04 14:16:17 +03:00
try:
status=self.status()
diff=set(status.items())-set(self._last_status.items())
2020-07-12 16:20:29 +03:00
for key, val in diff:
if key == "elapsed":
self.emitter.emit("elapsed_changed", float(val), float(status["duration"]))
elif key == "bitrate":
self.emitter.emit("bitrate", float(val))
2020-07-12 16:20:29 +03:00
elif key == "songid":
self.emitter.emit("current_song_changed")
elif key == "state":
self.emitter.emit("state", val)
elif key == "audio":
# see: https://www.musicpd.org/doc/html/user.html#audio-output-format
2020-09-24 22:17:10 +03:00
samplerate, bits, channels=val.split(":")
2020-09-11 14:57:39 +03:00
if bits == "f":
bits="32fp"
self.emitter.emit("audio", samplerate, bits, channels)
2020-07-12 16:20:29 +03:00
elif key == "volume":
self.emitter.emit("volume_changed", float(val))
2020-07-12 16:20:29 +03:00
elif key == "playlist":
self.emitter.emit("playlist_changed", int(val))
elif key in ["repeat", "random", "single", "consume"]:
if val == "1":
self.emitter.emit(key, True)
else:
self.emitter.emit(key, False)
diff=set(self._last_status)-set(status)
if "songid" in diff:
self.emitter.emit("current_song_changed")
if "volume" in diff:
self.emitter.emit("volume_changed", -1)
if "updating_db" in diff:
self.emitter.emit("update")
self._last_status=status
2020-08-15 12:56:44 +03:00
except (MPDBase.ConnectionError, ConnectionResetError) as e:
2020-08-14 23:08:56 +03:00
self.disconnect()
self._last_status={}
2020-07-04 14:16:17 +03:00
self.emitter.emit("disconnected")
self.emitter.emit("connection_error")
self._main_timeout_id=None
2020-07-04 14:16:17 +03:00
return False
return True
2020-03-21 00:09:13 +03:00
def _on_active_profile_changed(self, *args):
self.reconnect()
2020-08-21 19:03:36 +03:00
2020-07-04 14:16:17 +03:00
########################
# gio settings wrapper #
########################
2020-03-21 00:09:13 +03:00
2020-01-28 20:39:18 +03:00
class Settings(Gio.Settings):
2020-04-09 01:26:21 +03:00
BASE_KEY="org.mpdevil"
2020-09-15 19:45:30 +03:00
# temp settings
mini_player=GObject.Property(type=bool, default=False)
2020-01-28 20:39:18 +03:00
def __init__(self):
2020-01-28 21:59:14 +03:00
super().__init__(schema=self.BASE_KEY)
2020-08-04 20:37:54 +03:00
# fix profile settings
2020-02-07 22:13:38 +03:00
if len(self.get_value("profiles")) < (self.get_int("active-profile")+1):
self.set_int("active-profile", 0)
2020-09-12 15:31:17 +03:00
profile_keys=[
2020-09-24 22:17:10 +03:00
("as", "profiles", "new profile"),
("as", "hosts", "localhost"),
("ai", "ports", 6600),
("as", "passwords", ""),
("as", "paths", ""),
("as", "regex", "")
2020-09-12 15:31:17 +03:00
]
2020-08-04 20:37:54 +03:00
profile_arrays=[]
for vtype, key, default in profile_keys:
profile_arrays.append(self.get_value(key).unpack())
max_len=max(len(x) for x in profile_arrays)
for index, (vtype, key, default) in enumerate(profile_keys):
profile_arrays[index]=(profile_arrays[index]+max_len*[default])[:max_len]
self.set_value(key, GLib.Variant(vtype, profile_arrays[index]))
2020-01-28 20:39:18 +03:00
def array_append(self, vtype, key, value): # append to Gio.Settings (self._settings) array
2020-01-28 20:39:18 +03:00
array=self.get_value(key).unpack()
array.append(value)
self.set_value(key, GLib.Variant(vtype, array))
def array_delete(self, vtype, key, pos): # delete entry of Gio.Settings (self._settings) array
2020-01-28 20:39:18 +03:00
array=self.get_value(key).unpack()
array.pop(pos)
self.set_value(key, GLib.Variant(vtype, array))
def array_modify(self, vtype, key, pos, value): # modify entry of Gio.Settings (self._settings) array
2020-01-28 20:39:18 +03:00
array=self.get_value(key).unpack()
array[pos]=value
self.set_value(key, GLib.Variant(vtype, array))
2020-01-28 21:59:14 +03:00
def get_gtk_icon_size(self, key):
icon_size=self.get_int(key)
2020-06-27 17:11:41 +03:00
sizes=[(48, Gtk.IconSize.DIALOG), (32, Gtk.IconSize.DND), (24, Gtk.IconSize.LARGE_TOOLBAR), (16, Gtk.IconSize.BUTTON)]
for pixel_size, gtk_size in sizes:
if icon_size >= pixel_size:
return gtk_size
return Gtk.IconSize.INVALID
2020-01-28 21:59:14 +03:00
2020-02-26 01:37:27 +03:00
def get_artist_type(self):
if self.get_boolean("use-album-artist"):
2020-02-26 01:37:27 +03:00
return ("albumartist")
else:
return ("artist")
2020-02-26 01:37:27 +03:00
2020-09-16 16:08:56 +03:00
###################
# settings dialog #
###################
2020-05-26 16:04:16 +03:00
2020-09-16 16:08:56 +03:00
class GeneralSettings(Gtk.Box):
def __init__(self, settings):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
2020-09-16 16:08:56 +03:00
self._settings=settings
self._settings_handlers=[]
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# int_settings
int_settings={}
int_settings_data=[
(_("Main cover size:"), (100, 1200, 10), "track-cover"),
(_("Album view cover size:"), (50, 600, 10), "album-cover"),
(_("Action bar icon size:"), (16, 64, 2), "icon-size"),
(_("Secondary icon size:"), (16, 64, 2), "icon-size-sec")
]
for label, (vmin, vmax, step), key in int_settings_data:
int_settings[key]=(Gtk.Label(label=label, xalign=0), Gtk.SpinButton.new_with_range(vmin, vmax, step))
int_settings[key][1].set_value(self._settings.get_int(key))
int_settings[key][1].connect("value-changed", self._on_int_changed, key)
self._settings_handlers.append(
2020-09-24 22:17:10 +03:00
self._settings.connect("changed::{}".format(key), self._on_int_settings_changed, int_settings[key][1])
2020-09-16 16:08:56 +03:00
)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# combo_settings
combo_settings={}
combo_settings_data=[
(_("Sort albums by:"), _("name"), _("year"), "sort-albums-by-year"),
(_("Position of playlist:"), _("bottom"), _("right"), "playlist-right")
]
for label, vfalse, vtrue, key in combo_settings_data:
combo_settings[key]=(Gtk.Label(label=label, xalign=0), Gtk.ComboBoxText(entry_text_column=0))
combo_settings[key][1].append_text(vfalse)
combo_settings[key][1].append_text(vtrue)
if self._settings.get_boolean(key):
combo_settings[key][1].set_active(1)
else:
combo_settings[key][1].set_active(0)
combo_settings[key][1].connect("changed", self._on_combo_changed, key)
self._settings_handlers.append(
2020-09-24 22:17:10 +03:00
self._settings.connect("changed::{}".format(key), self._on_combo_settings_changed, combo_settings[key][1])
2020-09-16 16:08:56 +03:00
)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# check buttons
check_buttons={}
check_buttons_data=[
(_("Use Client-side decoration"), "use-csd"),
(_("Show stop button"), "show-stop"),
(_("Show lyrics button"), "show-lyrics-button"),
(_("Show initials in artist view"), "show-initials"),
(_("Show tooltips in album view"), "show-album-view-tooltips"),
2020-09-30 12:06:00 +03:00
(_("Use “Album Artist” tag"), "use-album-artist"),
2020-09-16 16:08:56 +03:00
(_("Send notification on title change"), "send-notify"),
(_("Stop playback on quit"), "stop-on-quit"),
(_("Play selected albums and titles immediately"), "force-mode")
]
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
for label, key in check_buttons_data:
check_buttons[key]=Gtk.CheckButton(label=label)
check_buttons[key].set_active(self._settings.get_boolean(key))
check_buttons[key].set_margin_start(12)
check_buttons[key].connect("toggled", self._on_toggled, key)
self._settings_handlers.append(
2020-09-24 22:17:10 +03:00
self._settings.connect("changed::{}".format(key), self._on_check_settings_changed, check_buttons[key])
2020-09-16 16:08:56 +03:00
)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
# headings
view_heading=Gtk.Label(label=_("<b>View</b>"), use_markup=True, xalign=0)
behavior_heading=Gtk.Label(label=_("<b>Behavior</b>"), use_markup=True, xalign=0)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
# view grid
view_grid=Gtk.Grid(row_spacing=6, column_spacing=12)
view_grid.set_margin_start(12)
view_grid.add(int_settings["track-cover"][0])
view_grid.attach_next_to(int_settings["album-cover"][0], int_settings["track-cover"][0], Gtk.PositionType.BOTTOM, 1, 1)
view_grid.attach_next_to(int_settings["icon-size"][0], int_settings["album-cover"][0], Gtk.PositionType.BOTTOM, 1, 1)
view_grid.attach_next_to(int_settings["icon-size-sec"][0], int_settings["icon-size"][0], Gtk.PositionType.BOTTOM, 1, 1)
view_grid.attach_next_to(combo_settings["playlist-right"][0], int_settings["icon-size-sec"][0], Gtk.PositionType.BOTTOM, 1, 1)
view_grid.attach_next_to(int_settings["track-cover"][1], int_settings["track-cover"][0], Gtk.PositionType.RIGHT, 1, 1)
view_grid.attach_next_to(int_settings["album-cover"][1], int_settings["album-cover"][0], Gtk.PositionType.RIGHT, 1, 1)
view_grid.attach_next_to(int_settings["icon-size"][1], int_settings["icon-size"][0], Gtk.PositionType.RIGHT, 1, 1)
view_grid.attach_next_to(int_settings["icon-size-sec"][1], int_settings["icon-size-sec"][0], Gtk.PositionType.RIGHT, 1, 1)
view_grid.attach_next_to(combo_settings["playlist-right"][1], combo_settings["playlist-right"][0], Gtk.PositionType.RIGHT, 1, 1)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# behavior grid
behavior_grid=Gtk.Grid(row_spacing=6, column_spacing=12)
behavior_grid.set_margin_start(12)
behavior_grid.add(combo_settings["sort-albums-by-year"][0])
behavior_grid.attach_next_to(
combo_settings["sort-albums-by-year"][1],
combo_settings["sort-albums-by-year"][0],
Gtk.PositionType.RIGHT, 1, 1
)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# connect
self.connect("destroy", self._remove_handlers)
2020-09-16 16:08:56 +03:00
# packing
box=Gtk.Box(spacing=12)
box.pack_start(check_buttons["use-csd"], False, False, 0)
box.pack_start(Gtk.Label(label=_("(restart required)"), sensitive=False), False, False, 0)
self.pack_start(view_heading, False, False, 0)
self.pack_start(box, False, False, 0)
self.pack_start(check_buttons["show-stop"], False, False, 0)
self.pack_start(check_buttons["show-lyrics-button"], False, False, 0)
self.pack_start(check_buttons["show-initials"], False, False, 0)
self.pack_start(check_buttons["show-album-view-tooltips"], False, False, 0)
self.pack_start(view_grid, False, False, 0)
self.pack_start(behavior_heading, False, False, 0)
self.pack_start(check_buttons["use-album-artist"], False, False, 0)
self.pack_start(check_buttons["send-notify"], False, False, 0)
self.pack_start(check_buttons["stop-on-quit"], False, False, 0)
self.pack_start(check_buttons["force-mode"], False, False, 0)
self.pack_start(behavior_grid, False, False, 0)
2020-09-16 16:08:56 +03:00
def _remove_handlers(self, *args):
for handler in self._settings_handlers:
self._settings.disconnect(handler)
2020-09-16 16:08:56 +03:00
def _on_int_settings_changed(self, settings, key, entry):
entry.set_value(settings.get_int(key))
2020-09-16 16:08:56 +03:00
def _on_combo_settings_changed(self, settings, key, combo):
if settings.get_boolean(key):
combo.set_active(1)
else:
combo.set_active(0)
2020-09-16 16:08:56 +03:00
def _on_check_settings_changed(self, settings, key, button):
button.set_active(settings.get_boolean(key))
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_int_changed(self, widget, key):
self._settings.set_int(key, int(widget.get_value()))
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_combo_changed(self, box, key):
active=box.get_active()
if active == 0:
self._settings.set_boolean(key, False)
else:
self._settings.set_boolean(key, True)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_toggled(self, widget, key):
self._settings.set_boolean(key, widget.get_active())
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
class ProfileSettings(Gtk.Grid):
def __init__(self, parent, client, settings):
super().__init__(row_spacing=6, column_spacing=12, border_width=18)
2020-08-09 23:58:48 +03:00
2020-09-16 16:08:56 +03:00
# adding vars
self._client=client
self._settings=settings
self._gui_modification=False # indicates whether the settings were changed from the settings dialog
2020-09-16 16:08:56 +03:00
# widgets
self._profiles_combo=Gtk.ComboBoxText(entry_text_column=0, hexpand=True)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
add_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-add", Gtk.IconSize.BUTTON))
delete_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-remove", Gtk.IconSize.BUTTON))
add_delete_buttons=Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.EXPAND)
add_delete_buttons.pack_start(add_button, True, True, 0)
add_delete_buttons.pack_start(delete_button, True, True, 0)
2020-01-18 00:13:58 +03:00
2020-09-16 16:08:56 +03:00
connect_button=Gtk.Button(label=_("Connect"), image=Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.BUTTON))
2020-01-18 00:13:58 +03:00
2020-09-16 16:08:56 +03:00
self._profile_entry=Gtk.Entry(hexpand=True)
self._host_entry=Gtk.Entry(hexpand=True)
self._port_entry=Gtk.SpinButton.new_with_range(0, 65535, 1)
address_entry=Gtk.Box(spacing=6)
address_entry.pack_start(self._host_entry, True, True, 0)
address_entry.pack_start(self._port_entry, False, False, 0)
self._password_entry=Gtk.Entry(hexpand=True, visibility=False)
self._path_entry=Gtk.Entry(hexpand=True)
self._path_select_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("folder-open", Gtk.IconSize.BUTTON))
path_box=Gtk.Box(spacing=6)
path_box.pack_start(self._path_entry, True, True, 0)
path_box.pack_start(self._path_select_button, False, False, 0)
self._regex_entry=Gtk.Entry(hexpand=True, placeholder_text=COVER_REGEX)
self._regex_entry.set_tooltip_text(
_("The first image in the same directory as the song file "\
"matching this regex will be displayed. %AlbumArtist% and "\
"%Album% will be replaced by the corresponding tags of the song.")
)
profiles_label=Gtk.Label(label=_("Profile:"), xalign=1)
profile_label=Gtk.Label(label=_("Name:"), xalign=1)
host_label=Gtk.Label(label=_("Host:"), xalign=1)
password_label=Gtk.Label(label=_("Password:"), xalign=1)
path_label=Gtk.Label(label=_("Music lib:"), xalign=1)
regex_label=Gtk.Label(label=_("Cover regex:"), xalign=1)
2020-01-18 00:13:58 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-09-16 16:08:56 +03:00
add_button.connect("clicked", self._on_add_button_clicked)
delete_button.connect("clicked", self._on_delete_button_clicked)
connect_button.connect("clicked", self._on_connect_button_clicked)
self._path_select_button.connect("clicked", self._on_path_select_button_clicked, parent)
self._profiles_combo.connect("changed", self._on_profiles_changed)
self.entry_changed_handlers=[]
self.entry_changed_handlers.append((self._profile_entry, self._profile_entry.connect("changed", self._on_profile_entry_changed)))
self.entry_changed_handlers.append((self._host_entry, self._host_entry.connect("changed", self._on_host_entry_changed)))
self.entry_changed_handlers.append((self._port_entry, self._port_entry.connect("value-changed", self._on_port_entry_changed)))
self.entry_changed_handlers.append((self._password_entry, self._password_entry.connect("changed", self._on_password_entry_changed)))
self.entry_changed_handlers.append((self._path_entry, self._path_entry.connect("changed", self._on_path_entry_changed)))
self.entry_changed_handlers.append((self._regex_entry, self._regex_entry.connect("changed", self._on_regex_entry_changed)))
self._settings_handlers=[]
self._settings_handlers.append(self._settings.connect("changed::profiles", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::hosts", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::ports", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::passwords", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::paths", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::regex", self._on_settings_changed))
self.connect("destroy", self._remove_handlers)
2020-01-18 00:13:58 +03:00
2020-09-16 16:08:56 +03:00
self._profiles_combo_reload()
self._profiles_combo.set_active(0)
2020-09-16 16:08:56 +03:00
# packing
self.add(profiles_label)
self.attach_next_to(profile_label, profiles_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(host_label, profile_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(password_label, host_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(path_label, password_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(regex_label, path_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(self._profiles_combo, profiles_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(add_delete_buttons, self._profiles_combo, Gtk.PositionType.RIGHT, 1, 1)
self.attach_next_to(self._profile_entry, profile_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(address_entry, host_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(self._password_entry, password_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(path_box, path_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(self._regex_entry, regex_label, Gtk.PositionType.RIGHT, 2, 1)
connect_button.set_margin_top(12)
self.attach_next_to(connect_button, self._regex_entry, Gtk.PositionType.BOTTOM, 2, 1)
2020-01-18 00:13:58 +03:00
2020-09-16 16:08:56 +03:00
def _block_entry_changed_handlers(self, *args):
for obj, handler in self.entry_changed_handlers:
obj.handler_block(handler)
2020-09-16 16:08:56 +03:00
def _unblock_entry_changed_handlers(self, *args):
for obj, handler in self.entry_changed_handlers:
obj.handler_unblock(handler)
2020-09-16 16:08:56 +03:00
def _profiles_combo_reload(self, *args):
self._block_entry_changed_handlers()
2020-01-18 00:13:58 +03:00
2020-09-16 16:08:56 +03:00
self._profiles_combo.remove_all()
for profile in self._settings.get_value("profiles"):
self._profiles_combo.append_text(profile)
2020-09-16 16:08:56 +03:00
self._unblock_entry_changed_handlers()
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _remove_handlers(self, *args):
for handler in self._settings_handlers:
self._settings.disconnect(handler)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_settings_changed(self, *args):
if self._gui_modification:
self._gui_modification=False
else:
self._profiles_combo_reload()
self._profiles_combo.set_active(0)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_add_button_clicked(self, *args):
model=self._profiles_combo.get_model()
2020-09-24 22:17:10 +03:00
self._settings.array_append("as", "profiles", "new profile ({})".format(len(model)))
self._settings.array_append("as", "hosts", "localhost")
self._settings.array_append("ai", "ports", 6600)
self._settings.array_append("as", "passwords", "")
self._settings.array_append("as", "paths", "")
self._settings.array_append("as", "regex", "")
2020-09-16 16:08:56 +03:00
self._profiles_combo_reload()
new_pos=len(model)-1
self._profiles_combo.set_active(new_pos)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_delete_button_clicked(self, *args):
pos=self._profiles_combo.get_active()
2020-09-24 22:17:10 +03:00
self._settings.array_delete("as", "profiles", pos)
self._settings.array_delete("as", "hosts", pos)
self._settings.array_delete("ai", "ports", pos)
self._settings.array_delete("as", "passwords", pos)
self._settings.array_delete("as", "paths", pos)
self._settings.array_delete("as", "regex", pos)
2020-09-16 16:08:56 +03:00
if len(self._settings.get_value("profiles")) == 0:
self._on_add_button_clicked()
else:
self._profiles_combo_reload()
new_pos=max(pos-1,0)
self._profiles_combo.set_active(new_pos)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_connect_button_clicked(self, *args):
self._settings.set_int("active-profile", self._profiles_combo.get_active())
self._client.reconnect()
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_profile_entry_changed(self, *args):
self._gui_modification=True
pos=self._profiles_combo.get_active()
2020-09-24 22:17:10 +03:00
self._settings.array_modify("as", "profiles", pos, self._profile_entry.get_text())
2020-09-16 16:08:56 +03:00
self._profiles_combo_reload()
self._profiles_combo.set_active(pos)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_host_entry_changed(self, *args):
self._gui_modification=True
2020-09-24 22:17:10 +03:00
self._settings.array_modify("as", "hosts", self._profiles_combo.get_active(), self._host_entry.get_text())
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_port_entry_changed(self, *args):
self._gui_modification=True
2020-09-24 22:17:10 +03:00
self._settings.array_modify("ai", "ports", self._profiles_combo.get_active(), int(self._port_entry.get_value()))
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_password_entry_changed(self, *args):
self._gui_modification=True
2020-09-24 22:17:10 +03:00
self._settings.array_modify("as", "passwords", self._profiles_combo.get_active(), self._password_entry.get_text())
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_path_entry_changed(self, *args):
self._gui_modification=True
2020-09-24 22:17:10 +03:00
self._settings.array_modify("as", "paths", self._profiles_combo.get_active(), self._path_entry.get_text())
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_regex_entry_changed(self, *args):
self._gui_modification=True
2020-09-24 22:17:10 +03:00
self._settings.array_modify("as", "regex", self._profiles_combo.get_active(), self._regex_entry.get_text())
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_path_select_button_clicked(self, widget, parent):
dialog=Gtk.FileChooserDialog(title=_("Choose directory"), transient_for=parent, action=Gtk.FileChooserAction.SELECT_FOLDER)
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
dialog.set_default_size(800, 400)
dialog.set_current_folder(self._settings.get_value("paths")[self._profiles_combo.get_active()])
response=dialog.run()
if response == Gtk.ResponseType.OK:
self._gui_modification=True
2020-09-24 22:17:10 +03:00
self._settings.array_modify("as", "paths", self._profiles_combo.get_active(), dialog.get_filename())
2020-09-16 16:08:56 +03:00
self._path_entry.set_text(dialog.get_filename())
dialog.destroy()
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_profiles_changed(self, *args):
active=self._profiles_combo.get_active()
self._block_entry_changed_handlers()
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
self._profile_entry.set_text(self._settings.get_value("profiles")[active])
self._host_entry.set_text(self._settings.get_value("hosts")[active])
self._port_entry.set_value(self._settings.get_value("ports")[active])
self._password_entry.set_text(self._settings.get_value("passwords")[active])
self._path_entry.set_text(self._settings.get_value("paths")[active])
self._regex_entry.set_text(self._settings.get_value("regex")[active])
2020-05-26 20:49:55 +03:00
2020-09-16 16:08:56 +03:00
self._unblock_entry_changed_handlers()
class PlaylistSettings(Gtk.Box):
def __init__(self, settings):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
2020-08-18 23:07:23 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._settings=settings
2020-09-16 16:08:56 +03:00
# label
label=Gtk.Label(label=_("Choose the order of information to appear in the playlist:"), wrap=True, xalign=0)
2020-09-16 16:08:56 +03:00
# Store
# (toggle, header, actual_index)
self._store=Gtk.ListStore(bool, str, int)
2020-08-21 13:03:42 +03:00
2020-09-16 16:08:56 +03:00
# TreeView
treeview=Gtk.TreeView(model=self._store, reorderable=True, headers_visible=False)
treeview.set_search_column(-1)
2020-09-16 16:08:56 +03:00
# selection
self._selection=treeview.get_selection()
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
# Column
renderer_text=Gtk.CellRendererText()
renderer_toggle=Gtk.CellRendererToggle()
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
column_toggle=Gtk.TreeViewColumn("", renderer_toggle, active=0)
treeview.append_column(column_toggle)
2020-09-16 16:08:56 +03:00
column_text=Gtk.TreeViewColumn("", renderer_text, text=1)
treeview.append_column(column_text)
2020-09-16 16:08:56 +03:00
# fill store
self._headers=[_("No"), _("Disc"), _("Title"), _("Artist"), _("Album"), _("Length"), _("Year"), _("Genre")]
self._fill()
2020-09-16 16:08:56 +03:00
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(treeview)
frame=Gtk.Frame()
frame.add(scroll)
2020-09-16 16:08:56 +03:00
# Toolbar
toolbar=Gtk.Toolbar()
style_context=toolbar.get_style_context()
style_context.add_class("inline-toolbar")
self._up_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-up-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
self._up_button.set_sensitive(False)
self._down_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
self._down_button.set_sensitive(False)
toolbar.insert(self._up_button, 0)
toolbar.insert(self._down_button, 1)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
# column chooser
column_chooser=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
column_chooser.pack_start(frame, True, True, 0)
column_chooser.pack_start(toolbar, False, False, 0)
2020-08-16 18:07:41 +03:00
# connect
2020-09-16 16:08:56 +03:00
self._row_deleted=self._store.connect("row-deleted", self._save_permutation)
renderer_toggle.connect("toggled", self._on_cell_toggled)
self._up_button.connect("clicked", self._on_up_button_clicked)
self._down_button.connect("clicked", self._on_down_button_clicked)
self._selection.connect("changed", self._set_button_sensitivity)
self._settings_handlers=[]
self._settings_handlers.append(self._settings.connect("changed::column-visibilities", self._on_visibilities_changed))
self._settings_handlers.append(self._settings.connect("changed::column-permutation", self._on_permutation_changed))
self.connect("destroy", self._remove_handlers)
2020-07-04 13:35:39 +03:00
# packing
2020-09-16 16:08:56 +03:00
self.pack_start(label, False, False, 0)
self.pack_start(column_chooser, True, True, 0)
2020-08-16 18:07:41 +03:00
2020-09-16 16:08:56 +03:00
def _fill(self, *args):
visibilities=self._settings.get_value("column-visibilities").unpack()
for actual_index in self._settings.get_value("column-permutation"):
self._store.append([visibilities[actual_index], self._headers[actual_index], actual_index])
2020-03-30 23:20:14 +03:00
2020-09-16 16:08:56 +03:00
def _save_permutation(self, *args):
permutation=[]
for row in self._store:
permutation.append(row[2])
self._settings.set_value("column-permutation", GLib.Variant("ai", permutation))
2020-09-16 15:51:28 +03:00
2020-09-16 16:08:56 +03:00
def _set_button_sensitivity(self, *args):
treeiter=self._selection.get_selected()[1]
if treeiter is None:
self._up_button.set_sensitive(False)
self._down_button.set_sensitive(False)
else:
path=self._store.get_path(treeiter)
if self._store.iter_next(treeiter) is None:
self._up_button.set_sensitive(True)
self._down_button.set_sensitive(False)
elif not path.prev():
self._up_button.set_sensitive(False)
self._down_button.set_sensitive(True)
else:
self._up_button.set_sensitive(True)
self._down_button.set_sensitive(True)
2020-02-27 22:05:48 +03:00
2020-09-16 16:08:56 +03:00
def _remove_handlers(self, *args):
for handler in self._settings_handlers:
self._settings.disconnect(handler)
2020-02-27 22:05:48 +03:00
2020-09-16 16:08:56 +03:00
def _on_cell_toggled(self, widget, path):
self._store[path][0]=not self._store[path][0]
2020-09-24 22:17:10 +03:00
self._settings.array_modify("ab", "column-visibilities", self._store[path][2], self._store[path][0])
2020-02-27 22:05:48 +03:00
2020-09-16 16:08:56 +03:00
def _on_up_button_clicked(self, *args):
treeiter=self._selection.get_selected()[1]
path=self._store.get_path(treeiter)
path.prev()
prev=self._store.get_iter(path)
self._store.move_before(treeiter, prev)
self._set_button_sensitivity()
self._save_permutation()
2020-02-29 01:23:53 +03:00
2020-09-16 16:08:56 +03:00
def _on_down_button_clicked(self, *args):
treeiter=self._selection.get_selected()[1]
path=self._store.get_path(treeiter)
next=self._store.iter_next(treeiter)
self._store.move_after(treeiter, next)
self._set_button_sensitivity()
self._save_permutation()
2020-09-16 16:08:56 +03:00
def _on_visibilities_changed(self, *args):
visibilities=self._settings.get_value("column-visibilities").unpack()
for i, actual_index in enumerate(self._settings.get_value("column-permutation")):
self._store[i][0]=visibilities[actual_index]
2020-02-27 22:05:48 +03:00
2020-09-16 16:08:56 +03:00
def _on_permutation_changed(self, *args):
equal=True
perm=self._settings.get_value("column-permutation")
for i, e in enumerate(self._store):
if e[2] != perm[i]:
equal=False
break
if not equal:
self._store.handler_block(self._row_deleted)
self._store.clear()
self._fill()
self._store.handler_unblock(self._row_deleted)
2020-09-16 16:08:56 +03:00
class SettingsDialog(Gtk.Dialog):
def __init__(self, parent, client, settings):
use_csd=settings.get_boolean("use-csd")
if use_csd:
super().__init__(title=_("Settings"), transient_for=parent, use_header_bar=True)
# css
style_context=self.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {-GtkDialog-content-area-border: 0px;}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
else:
super().__init__(title=_("Settings"), transient_for=parent)
self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
self.set_default_size(500, 400)
2020-02-27 22:05:48 +03:00
2020-09-16 16:08:56 +03:00
# widgets
general=GeneralSettings(settings)
profiles=ProfileSettings(parent, client, settings)
playlist=PlaylistSettings(settings)
2020-08-09 23:58:48 +03:00
2020-09-16 16:08:56 +03:00
# packing
tabs=Gtk.Notebook()
tabs.append_page(general, Gtk.Label(label=_("General")))
tabs.append_page(profiles, Gtk.Label(label=_("Profiles")))
tabs.append_page(playlist, Gtk.Label(label=_("Playlist")))
vbox=self.get_content_area()
vbox.set_spacing(6)
vbox.pack_start(tabs, True, True, 0)
2020-08-09 23:58:48 +03:00
2020-09-16 16:08:56 +03:00
self.show_all()
2020-09-16 15:51:28 +03:00
2020-09-16 16:08:56 +03:00
#################
# other dialogs #
#################
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
class ServerStats(Gtk.Dialog):
def __init__(self, parent, client, settings):
use_csd=settings.get_boolean("use-csd")
if use_csd:
super().__init__(title=_("Stats"), transient_for=parent, use_header_bar=True)
# css
style_context=self.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {-GtkDialog-content-area-border: 0px;}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
else:
super().__init__(title=_("Stats"), transient_for=parent)
self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
self.set_resizable(False)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# Store
# (tag, value)
store=Gtk.ListStore(str, str)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# TreeView
2020-09-29 14:02:51 +03:00
treeview=Gtk.TreeView(model=store, headers_visible=False)
treeview.set_can_focus(False)
2020-09-16 16:08:56 +03:00
treeview.set_search_column(-1)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# selection
sel=treeview.get_selection()
sel.set_mode(Gtk.SelectionMode.NONE)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# Column
renderer_text=Gtk.CellRendererText()
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
2020-03-12 19:09:24 +03:00
2020-09-16 16:08:56 +03:00
column_tag=Gtk.TreeViewColumn("", renderer_text_ralign, text=0)
treeview.append_column(column_tag)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
column_value=Gtk.TreeViewColumn("", renderer_text, text=1)
treeview.append_column(column_value)
2020-03-31 18:36:41 +03:00
2020-09-16 16:08:56 +03:00
store.append(["protocol:", str(client.mpd_version)])
2020-09-16 16:08:56 +03:00
stats=client.wrapped_call("stats")
for key in stats:
print_key=key+":"
if key == "uptime" or key == "playtime" or key == "db_playtime":
2020-09-30 12:06:00 +03:00
store.append([print_key, ClientHelper.seconds_to_display_time(int(stats[key]))])
2020-09-16 16:08:56 +03:00
elif key == "db_update":
2020-09-30 12:06:00 +03:00
store.append([print_key, str(datetime.datetime.fromtimestamp(int(stats[key]))).replace(":", "")])
2020-09-16 16:08:56 +03:00
else:
store.append([print_key, stats[key]])
frame=Gtk.Frame()
frame.add(treeview)
self.vbox.pack_start(frame, True, True, 0)
self.vbox.set_spacing(6)
self.show_all()
self.run()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
class AboutDialog(Gtk.AboutDialog):
def __init__(self, window):
super().__init__(transient_for=window, modal=True)
self.set_program_name("mpdevil")
self.set_version(VERSION)
self.set_comments(_("A simple music browser for MPD"))
self.set_authors(["Martin Wagner"])
self.set_website("https://github.com/SoongNoonien/mpdevil")
self.set_copyright("\xa9 2020 Martin Wagner")
self.set_logo_icon_name("mpdevil")
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
#################################
# small general purpose widgets #
#################################
2020-09-16 16:08:56 +03:00
class PixelSizedIcon(Gtk.Image):
def __init__(self, icon_name, pixel_size):
super().__init__()
self.set_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
if pixel_size > 0:
self.set_pixel_size(pixel_size)
2020-02-01 16:36:57 +03:00
2020-09-16 16:08:56 +03:00
class FocusFrame(Gtk.Overlay):
def __init__(self):
super().__init__()
2020-05-19 18:23:15 +03:00
2020-09-16 16:08:56 +03:00
self._frame=Gtk.Frame()
self._frame.set_no_show_all(True)
2020-09-16 16:08:56 +03:00
# css
style_context=self._frame.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {border-color: @theme_selected_bg_color; border-width: 2px;}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
2020-09-16 16:08:56 +03:00
self.add_overlay(self._frame)
self.set_overlay_pass_through(self._frame, True)
2020-09-11 00:07:00 +03:00
2020-09-16 16:08:56 +03:00
def set_widget(self, widget):
widget.connect("focus-in-event", self._on_focus_in_event)
widget.connect("focus-out-event", self._on_focus_out_event)
2020-09-11 00:07:00 +03:00
2020-09-16 16:08:56 +03:00
def _on_focus_in_event(self, *args):
self._frame.show()
2020-03-12 19:09:24 +03:00
2020-09-16 16:08:56 +03:00
def _on_focus_out_event(self, *args):
self._frame.hide()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
class SongPopover(Gtk.Popover):
def __init__(self, song, relative, x, y):
super().__init__()
rect=Gdk.Rectangle()
rect.x=x
# Gtk places popovers 26px above the given position for no obvious reasons, so I move them 26px
rect.y=y+26
rect.width = 1
rect.height = 1
self.set_pointing_to(rect)
self.set_relative_to(relative)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# Store
# (tag, display-value, tooltip)
store=Gtk.ListStore(str, str, str)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# TreeView
2020-09-29 14:02:51 +03:00
treeview=Gtk.TreeView(model=store, headers_visible=False, search_column=-1, tooltip_column=2)
treeview.set_can_focus(False)
2020-09-16 16:08:56 +03:00
sel=treeview.get_selection()
sel.set_mode(Gtk.SelectionMode.NONE)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
frame=Gtk.Frame(border_width=3)
frame.add(treeview)
2020-08-23 11:59:03 +03:00
2020-09-16 16:08:56 +03:00
# Column
renderer_text=Gtk.CellRendererText(width_chars=50, ellipsize=Pango.EllipsizeMode.MIDDLE, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
column_tag=Gtk.TreeViewColumn(_("MPD-Tag"), renderer_text_ralign, text=0)
column_tag.set_property("resizable", False)
treeview.append_column(column_tag)
2020-08-23 11:59:03 +03:00
2020-09-16 16:08:56 +03:00
column_value=Gtk.TreeViewColumn(_("Value"), renderer_text, text=1)
column_value.set_property("resizable", False)
treeview.append_column(column_value)
2020-09-16 16:08:56 +03:00
# packing
self.add(frame)
2020-08-13 18:40:27 +03:00
2020-09-16 16:08:56 +03:00
song=ClientHelper.song_to_str_dict(song)
for tag, value in song.items():
tooltip=value.replace("&", "&amp;")
if tag == "time":
2020-09-30 12:06:00 +03:00
store.append([tag+":", ClientHelper.seconds_to_display_time(int(value)), tooltip])
2020-09-16 16:08:56 +03:00
elif tag == "last-modified":
2020-09-24 22:17:10 +03:00
time=datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
2020-09-30 12:06:00 +03:00
store.append([tag+":", time.strftime("%a %d %B %Y, %H%M UTC"), tooltip])
2020-09-16 16:08:56 +03:00
else:
store.append([tag+":", value, tooltip])
frame.show_all()
2020-08-21 19:03:36 +03:00
2020-09-16 16:08:56 +03:00
class Cover(object):
def __init__(self, settings, song):
self.path=None
if song != {}:
song_file=song["file"]
2020-09-16 16:08:56 +03:00
active_profile=settings.get_int("active-profile")
2020-02-28 18:03:45 +03:00
2020-09-16 16:08:56 +03:00
lib_path=settings.get_value("paths")[active_profile]
regex_str=settings.get_value("regex")[active_profile]
2020-03-26 19:01:15 +03:00
2020-09-16 16:08:56 +03:00
if regex_str == "":
2020-09-24 22:17:10 +03:00
regex=re.compile(COVER_REGEX, flags=re.IGNORECASE)
2020-08-23 11:59:03 +03:00
else:
2020-09-23 17:21:24 +03:00
regex_str=regex_str.replace("%AlbumArtist%", song.get("albumartist", ""))
regex_str=regex_str.replace("%Album%", song.get("album", ""))
2020-09-16 16:08:56 +03:00
try:
2020-09-24 22:17:10 +03:00
regex=re.compile(regex_str, flags=re.IGNORECASE)
2020-09-16 16:08:56 +03:00
except:
print("illegal regex:", regex_str)
2020-09-24 22:17:10 +03:00
return
2020-09-16 16:08:56 +03:00
if song_file is not None:
2020-10-02 09:15:34 +03:00
song_dir=os.path.join(lib_path, os.path.dirname(song_file))
if song_dir.endswith(".cue"):
song_dir=os.path.dirname(song_dir) # get actual directory of .cue file
2020-09-16 16:08:56 +03:00
if os.path.exists(song_dir):
for f in os.listdir(song_dir):
if regex.match(f):
self.path=os.path.join(song_dir, f)
break
2020-09-16 16:08:56 +03:00
def get_pixbuf(self, size):
if self.path is None: # fallback needed
self.path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size)
2020-03-30 23:20:14 +03:00
2020-09-16 16:08:56 +03:00
###########
# browser #
###########
class SongsView(Gtk.TreeView):
def __init__(self, client, store, file_column_id):
super().__init__(model=store)
self.set_search_column(-1)
self.columns_autosize()
# add vars
self._client=client
self._store=store
self._file_column_id=file_column_id
# selection
self._selection=self.get_selection()
self._selection.set_mode(Gtk.SelectionMode.SINGLE)
# connect
self.connect("row-activated", self._on_row_activated)
self.connect("button-press-event", self._on_button_press_event)
self._key_press_event=self.connect("key-press-event", self._on_key_press_event)
def clear(self):
self._store.clear()
def count(self):
return len(self._store)
def get_files(self):
return_list=[]
for row in self._store:
return_list.append(row[self._file_column_id])
return return_list
def _on_row_activated(self, widget, path, view_column):
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]], "play")
def _on_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
2020-08-23 11:59:03 +03:00
try:
2020-09-16 16:08:56 +03:00
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]])
except:
pass
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]], "append")
except:
pass
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
file_name=self._store[path][self._file_column_id]
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, int(event.x), int(event.y))
pop.popup()
pop.show_all()
2020-08-23 11:59:03 +03:00
except:
pass
2020-03-30 23:20:14 +03:00
def _on_key_press_event(self, widget, event):
self.handler_block(self._key_press_event)
2020-09-17 13:52:07 +03:00
if event.keyval == Gdk.keyval_from_name("p"):
2020-09-16 16:08:56 +03:00
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
self._client.wrapped_call("files_to_playlist", [self._store.get_value(treeiter, self._file_column_id)])
2020-09-17 13:52:07 +03:00
elif event.keyval == Gdk.keyval_from_name("a"):
2020-09-16 16:08:56 +03:00
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
self._client.wrapped_call("files_to_playlist", [self._store.get_value(treeiter, self._file_column_id)], "append")
2020-09-17 13:52:07 +03:00
elif event.keyval == Gdk.keyval_from_name("Menu"):
2020-09-16 16:08:56 +03:00
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
path=self._store.get_path(treeiter)
cell=self.get_cell_area(path, None)
file_name=self._store[path][self._file_column_id]
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, int(cell.x), int(cell.y))
pop.popup()
pop.show_all()
self.handler_unblock(self._key_press_event)
2020-03-30 23:20:14 +03:00
2020-09-16 16:08:56 +03:00
class SongsWindow(Gtk.Box):
def __init__(self, client, store, file_column_id):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
2020-09-11 00:07:00 +03:00
2020-09-16 16:08:56 +03:00
# adding vars
self._client=client
2020-09-11 00:07:00 +03:00
2020-09-16 16:08:56 +03:00
# treeview
self._songs_view=SongsView(client, store, file_column_id)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self._songs_view)
2020-09-16 15:38:58 +03:00
2020-09-16 16:08:56 +03:00
# buttons
2020-10-01 13:22:08 +03:00
append_button=Gtk.Button.new_with_mnemonic(_("_Append"))
append_button.set_image(Gtk.Image.new_from_icon_name("list-add", Gtk.IconSize.BUTTON))
2020-09-16 16:08:56 +03:00
append_button.set_tooltip_text(_("Add all titles to playlist"))
2020-10-01 13:22:08 +03:00
play_button=Gtk.Button.new_with_mnemonic(_("_Play"))
play_button.set_image(Gtk.Image.new_from_icon_name("media-playback-start", Gtk.IconSize.BUTTON))
2020-09-16 16:08:56 +03:00
play_button.set_tooltip_text(_("Directly play all titles"))
2020-10-01 13:22:08 +03:00
enqueue_button=Gtk.Button.new_with_mnemonic(_("_Enqueue"))
enqueue_button.set_image(Gtk.Image.new_from_icon_name("insert-object", Gtk.IconSize.BUTTON))
2020-09-16 16:08:56 +03:00
enqueue_button.set_tooltip_text(_("Append all titles after the currently playing track and clear the playlist from all other songs"))
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# button box
button_box=Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.EXPAND)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# action bar
self._action_bar=Gtk.ActionBar()
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# connect
append_button.connect("clicked", self._on_append_button_clicked)
play_button.connect("clicked", self._on_play_button_clicked)
enqueue_button.connect("clicked", self._on_enqueue_button_clicked)
2020-09-16 16:08:56 +03:00
# packing
frame=FocusFrame()
frame.set_widget(self._songs_view)
frame.add(scroll)
self.pack_start(frame, True, True, 0)
button_box.pack_start(append_button, True, True, 0)
button_box.pack_start(play_button, True, True, 0)
button_box.pack_start(enqueue_button, True, True, 0)
self._action_bar.pack_start(button_box)
self.pack_start(self._action_bar, False, False, 0)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def get_treeview(self):
return self._songs_view
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def get_action_bar(self):
return self._action_bar
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_append_button_clicked(self, *args):
self._client.wrapped_call("files_to_playlist", self._songs_view.get_files(), "append")
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_play_button_clicked(self, *args):
self._client.wrapped_call("files_to_playlist", self._songs_view.get_files(), "play")
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_enqueue_button_clicked(self, *args):
self._client.wrapped_call("files_to_playlist", self._songs_view.get_files(), "enqueue")
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
class SearchWindow(Gtk.Box):
def __init__(self, client):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# adding vars
self._client=client
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# tag switcher
self._tags=Gtk.ComboBoxText()
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# search entry
self.search_entry=Gtk.SearchEntry()
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# label
self._hits_label=Gtk.Label(xalign=1)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# store
# (track, title, artist, album, duration, file, sort track)
self._store=Gtk.ListStore(str, str, str, str, str, str, int)
self._store.set_default_sort_func(lambda *args: 0)
2020-09-15 19:45:30 +03:00
2020-09-16 16:08:56 +03:00
# songs window
self._songs_window=SongsWindow(self._client, self._store, 5)
2020-09-16 16:08:56 +03:00
# action bar
self._action_bar=self._songs_window.get_action_bar()
self._action_bar.set_sensitive(False)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
# songs view
self._songs_view=self._songs_window.get_treeview()
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
# columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_track.set_property("resizable", False)
self._songs_view.append_column(column_track)
2020-08-26 21:30:42 +03:00
2020-09-16 16:08:56 +03:00
column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, text=1)
column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_title.set_property("resizable", False)
column_title.set_property("expand", True)
self._songs_view.append_column(column_title)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
column_artist=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=2)
column_artist.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_artist.set_property("resizable", False)
column_artist.set_property("expand", True)
self._songs_view.append_column(column_artist)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
column_album=Gtk.TreeViewColumn(_("Album"), renderer_text, text=3)
column_album.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_album.set_property("resizable", False)
column_album.set_property("expand", True)
self._songs_view.append_column(column_album)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=4)
column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_time.set_property("resizable", False)
self._songs_view.append_column(column_time)
column_track.set_sort_column_id(6)
2020-09-16 16:08:56 +03:00
column_title.set_sort_column_id(1)
column_artist.set_sort_column_id(2)
column_album.set_sort_column_id(3)
column_time.set_sort_column_id(4)
2020-08-08 14:31:33 +03:00
# connect
2020-09-16 16:08:56 +03:00
self.search_entry.connect("search-changed", self._on_search_changed)
self._tags.connect("changed", self._on_search_changed)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._client.emitter.connect("disconnected", self._on_disconnected)
2020-08-08 14:31:33 +03:00
# packing
2020-09-16 16:08:56 +03:00
hbox=Gtk.Box(spacing=6, border_width=6)
hbox.pack_start(self.search_entry, True, True, 0)
hbox.pack_end(self._tags, False, False, 0)
self._hits_label.set_margin_end(6)
self._action_bar.pack_end(self._hits_label)
self.pack_start(hbox, False, False, 0)
self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
self.pack_start(self._songs_window, True, True, 0)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
def clear(self, *args):
self._songs_view.clear()
self.search_entry.set_text("")
self._tags.remove_all()
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
def _on_disconnected(self, *args):
self._tags.set_sensitive(False)
self.search_entry.set_sensitive(False)
self.clear()
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
def _on_reconnected(self, *args):
self._tags.append_text("any")
for tag in self._client.wrapped_call("tagtypes"):
if not tag.startswith("MUSICBRAINZ"):
self._tags.append_text(tag)
self._tags.set_active(0)
self._tags.set_sensitive(True)
self.search_entry.set_sensitive(True)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
def _on_search_changed(self, widget):
self._songs_view.clear()
self._hits_label.set_text("")
if len(self.search_entry.get_text()) > 1:
songs=self._client.wrapped_call("search", self._tags.get_active_text(), self.search_entry.get_text())
for s in songs:
song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s))
self._store.append([
song["track"], song["title"],
2020-09-16 16:08:56 +03:00
song["artist"], song["album"],
song["human_duration"], song["file"],
2020-09-23 17:21:24 +03:00
int(song.get("track", 0))
2020-09-16 16:08:56 +03:00
])
2020-09-24 22:17:10 +03:00
self._hits_label.set_text(_("{num} hits").format(num=self._songs_view.count()))
2020-09-16 16:08:56 +03:00
if self._songs_view.count() == 0:
self._action_bar.set_sensitive(False)
else:
self._action_bar.set_sensitive(True)
2020-08-08 14:31:33 +03:00
2020-09-16 16:08:56 +03:00
class GenreSelect(Gtk.ComboBoxText):
2020-09-24 22:17:10 +03:00
__gsignals__={"genre_changed": (GObject.SignalFlags.RUN_FIRST, None, ())}
2020-08-21 19:03:36 +03:00
2020-07-04 14:16:17 +03:00
def __init__(self, client):
super().__init__(wrap_width=3)
2020-07-04 14:16:17 +03:00
# adding vars
self._client=client
2020-07-04 14:16:17 +03:00
# connect
2020-09-16 16:08:56 +03:00
self._changed=self.connect("changed", self._on_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._client.emitter.connect("update", self._refresh)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def deactivate(self):
self.set_active(0)
2020-08-21 19:03:36 +03:00
2020-09-16 16:08:56 +03:00
def _clear(self, *args):
self.handler_block(self._changed)
self.remove_all()
self.handler_unblock(self._changed)
def get_selected_genre(self):
if self.get_active() == 0:
return None
else:
return self.get_active_text()
2020-07-04 14:16:17 +03:00
def _refresh(self, *args):
2020-09-16 16:08:56 +03:00
self.handler_block(self._changed)
self.remove_all()
self.append_text(_("all genres"))
for genre in self._client.wrapped_call("comp_list", "genre"):
self.append_text(genre)
self.set_active(0)
self.handler_unblock(self._changed)
2020-09-16 16:08:56 +03:00
def _on_changed(self, *args):
self.emit("genre_changed")
2020-09-16 16:08:56 +03:00
def _on_disconnected(self, *args):
self.set_sensitive(False)
self._clear()
2020-09-16 16:08:56 +03:00
def _on_reconnected(self, *args):
self._refresh()
self.set_sensitive(True)
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
class ArtistWindow(FocusFrame):
2020-09-24 22:17:10 +03:00
__gsignals__={"artists_changed": (GObject.SignalFlags.RUN_FIRST, None, ())}
2020-03-10 15:50:36 +03:00
2020-09-16 16:08:56 +03:00
def __init__(self, client, settings, genre_select):
2020-08-31 11:44:23 +03:00
super().__init__()
2020-08-25 13:18:35 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._client=client
self._settings=settings
2020-09-16 16:08:56 +03:00
self._genre_select=genre_select
2020-03-03 15:41:46 +03:00
2020-09-16 16:08:56 +03:00
# artistStore
# (name, weight, initial-letter, weight-initials)
self._store=Gtk.ListStore(str, Pango.Weight, str, Pango.Weight)
2020-03-30 18:08:59 +03:00
2020-09-16 16:08:56 +03:00
# TreeView
self._treeview=Gtk.TreeView(model=self._store, activate_on_single_click=True, search_column=0, headers_visible=False)
2020-09-16 16:08:56 +03:00
self._treeview.columns_autosize()
# Selection
self._selection=self._treeview.get_selection()
self._selection.set_mode(Gtk.SelectionMode.SINGLE)
# Columns
renderer_text_malign=Gtk.CellRendererText(xalign=0.5)
self._column_initials=Gtk.TreeViewColumn("", renderer_text_malign, text=2, weight=3)
self._column_initials.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self._column_initials.set_property("resizable", False)
self._column_initials.set_visible(self._settings.get_boolean("show-initials"))
self._treeview.append_column(self._column_initials)
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
self._column_name=Gtk.TreeViewColumn("", renderer_text, text=0, weight=1)
self._column_name.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self._column_name.set_property("resizable", False)
self._treeview.append_column(self._column_name)
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self._treeview)
2020-07-04 13:35:39 +03:00
# connect
2020-09-16 16:08:56 +03:00
self._treeview.connect("row-activated", self._on_row_activated)
self._settings.connect("changed::use-album-artist", self._refresh)
self._settings.connect("changed::show-initials", self._on_show_initials_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-09-16 16:08:56 +03:00
self._client.emitter.connect("update", self._refresh)
self._genre_select.connect("genre_changed", self._refresh)
2020-03-03 15:41:46 +03:00
2020-09-16 16:08:56 +03:00
self.set_widget(self._treeview)
self.add(scroll)
def _clear(self, *args):
self._store.clear()
def select(self, artist):
row_num=len(self._store)
for i in range(0, row_num):
path=Gtk.TreePath(i)
if self._store[path][0] == artist:
self._treeview.set_cursor(path, None, False)
if self.get_selected_artists()[1] != [artist]:
self._treeview.row_activated(path, self._column_name)
break
def get_selected_artists(self):
artists=[]
genre=self._genre_select.get_selected_genre()
if self._store[Gtk.TreePath(0)][1] == Pango.Weight.BOLD:
for row in self._store:
artists.append(row[0])
return (genre, artists[1:])
else:
for row in self._store:
if row[1] == Pango.Weight.BOLD:
artists.append(row[0])
break
return (genre, artists)
def highlight_selected(self):
for path, row in enumerate(self._store):
if row[1] == Pango.Weight.BOLD:
self._treeview.set_cursor(path, None, False)
break
def _refresh(self, *args):
self._selection.set_mode(Gtk.SelectionMode.NONE)
self._clear()
self._store.append([_("all artists"), Pango.Weight.BOOK, "", Pango.Weight.BOOK])
genre=self._genre_select.get_selected_genre()
if genre is None:
artists=self._client.wrapped_call("comp_list", self._settings.get_artist_type())
else:
artists=self._client.wrapped_call("comp_list", self._settings.get_artist_type(), "genre", genre)
current_char=""
for artist in artists:
try:
if current_char == artist[0]:
self._store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK])
else:
self._store.append([artist, Pango.Weight.BOOK, artist[0], Pango.Weight.BOLD])
current_char=artist[0]
except:
self._store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK])
self._selection.set_mode(Gtk.SelectionMode.SINGLE)
self.emit("artists_changed")
def _on_row_activated(self, widget, path, view_column):
for row in self._store: # reset bold text
row[1]=Pango.Weight.BOOK
self._store[path][1]=Pango.Weight.BOLD
self.emit("artists_changed")
def _on_disconnected(self, *args):
self.set_sensitive(False)
self._clear()
def _on_reconnected(self, *args):
self._refresh()
self.set_sensitive(True)
def _on_show_initials_changed(self, *args):
self._column_initials.set_visible(self._settings.get_boolean("show-initials"))
class AlbumDialog(Gtk.Dialog): # also used by 'MainCover'
def __init__(self, parent, client, settings, album, album_artist, year):
use_csd=settings.get_boolean("use-csd")
if use_csd:
super().__init__(transient_for=parent, use_header_bar=True)
else:
super().__init__(transient_for=parent)
# css
style_context=self.get_style_context()
provider=Gtk.CssProvider()
if use_csd:
css=b"""* {-GtkDialog-content-area-border: 0px;}"""
else:
css=b"""* {-GtkDialog-action-area-border: 0px;}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
# adding vars
self._client=client
self._settings=settings
songs=self._client.wrapped_call("find", "album", album, "date", year, self._settings.get_artist_type(), album_artist)
# determine size
size=parent.get_size()
diagonal=(size[0]**2+size[1]**2)**(0.5)
h=diagonal//4
w=h*5//4
self.set_default_size(w, h)
# title
2020-09-24 22:17:10 +03:00
duration=ClientHelper.calc_display_length(songs)
2020-09-16 16:08:56 +03:00
if year == "":
2020-09-24 22:17:10 +03:00
self.set_title("{} - {} ({})".format(album_artist, album, duration))
2020-09-16 16:08:56 +03:00
else:
2020-09-24 22:17:10 +03:00
self.set_title("{} - {} ({}) ({})".format(album_artist, album, year, duration))
2020-09-16 16:08:56 +03:00
# store
# (track, title (artist), duration, file)
store=Gtk.ListStore(str, str, str, str)
2020-09-16 16:08:56 +03:00
for s in songs:
song=ClientHelper.extend_song_for_display(s)
if type(song["title"]) == list: # could be impossible
2020-09-24 22:17:10 +03:00
title=(", ".join(song["title"]))
2020-09-16 16:08:56 +03:00
else:
title=song["title"]
if type(song["artist"]) == list:
try:
song["artist"].remove(album_artist)
except:
pass
2020-09-24 22:17:10 +03:00
artist=(", ".join(song["artist"]))
2020-09-16 16:08:56 +03:00
else:
artist=song["artist"]
if artist == album_artist:
2020-09-24 22:17:10 +03:00
title_artist="<b>{}</b>".format(title)
2020-09-16 16:08:56 +03:00
else:
2020-09-24 22:17:10 +03:00
title_artist="<b>{}</b> - {}".format(title, artist)
2020-09-16 16:08:56 +03:00
title_artist=title_artist.replace("&", "&amp;")
store.append([song["track"], title_artist, song["human_duration"], song["file"]])
2020-09-16 16:08:56 +03:00
# songs window
songs_window=SongsWindow(self._client, store, 3)
# songs view
songs_view=songs_window.get_treeview()
# columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
column_track.set_property("resizable", False)
songs_view.append_column(column_track)
column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1)
column_title.set_property("resizable", False)
column_title.set_property("expand", True)
songs_view.append_column(column_title)
2020-10-02 09:31:48 +03:00
column_time=Gtk.TreeViewColumn(_("Length"), renderer_text_ralign, text=2)
2020-09-16 16:08:56 +03:00
column_time.set_property("resizable", False)
songs_view.append_column(column_time)
# close button
close_button=Gtk.ToggleButton(image=Gtk.Image.new_from_icon_name("window-close", Gtk.IconSize.BUTTON), label=_("Close"))
# action bar
action_bar=songs_window.get_action_bar()
action_bar.pack_end(close_button)
# connect
close_button.connect("clicked", self._on_close_button_clicked)
# packing
self.vbox.pack_start(songs_window, True, True, 0) # vbox default widget of dialogs
self.show_all()
def open(self):
response=self.run()
def _on_close_button_clicked(self, *args):
self.destroy()
class AlbumWindow(FocusFrame):
def __init__(self, client, settings, artist_window, window):
super().__init__()
# adding vars
self._settings=settings
self._client=client
self._artist_window=artist_window
self._window=window
self._button_event=(None, None)
self.stop_flag=False
self._done=True
self._pending=[]
# cover, display_label, display_label_artist, tooltip(titles), album, year, artist
self._store=Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str, str, str, str)
self._sort_settings()
# iconview
self._iconview=Gtk.IconView(model=self._store, item_width=0, pixbuf_column=0, markup_column=1)
self._tooltip_settings()
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self._iconview)
# connect
self._iconview.connect("item-activated", self._on_item_activated)
self._iconview.connect("button-release-event", self._on_button_release_event)
self._iconview.connect("button-press-event", self._on_button_press_event)
self._key_press_event=self.connect("key-press-event", self._on_key_press_event)
self._client.emitter.connect("update", self._clear)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._settings.connect("changed::show-album-view-tooltips", self._tooltip_settings)
self._settings.connect("changed::sort-albums-by-year", self._sort_settings)
self._settings.connect("changed::album-cover", self._on_cover_size_changed)
self._settings.connect("changed::use-album-artist", self._clear)
self._artist_window.connect("artists_changed", self._refresh)
self.set_widget(self._iconview)
self.add(scroll)
def _workaround_clear(self):
self._store.clear()
# workaround (scrollbar still visible after clear)
self._iconview.set_model(None)
self._iconview.set_model(self._store)
def _clear(self, *args):
if self._done:
self._workaround_clear()
elif not self._clear in self._pending:
self.stop_flag=True
self._pending.append(self._clear)
def scroll_to_current_album(self):
def callback():
song=ClientHelper.song_to_first_str_dict(self._client.wrapped_call("currentsong"))
2020-09-23 17:21:24 +03:00
album=song.get("album", "")
2020-09-16 16:08:56 +03:00
self._iconview.unselect_all()
row_num=len(self._store)
for i in range(0, row_num):
path=Gtk.TreePath(i)
treeiter=self._store.get_iter(path)
if self._store.get_value(treeiter, 4) == album:
self._iconview.set_cursor(path, None, False)
self._iconview.select_path(path)
self._iconview.scroll_to_path(path, True, 0, 0)
break
if self._done:
callback()
elif not self.scroll_to_current_album in self._pending:
self._pending.append(self.scroll_to_current_album)
def _tooltip_settings(self, *args):
if self._settings.get_boolean("show-album-view-tooltips"):
self._iconview.set_tooltip_column(3)
else:
self._iconview.set_tooltip_column(-1)
def _sort_settings(self, *args):
if self._settings.get_boolean("sort-albums-by-year"):
self._store.set_sort_column_id(5, Gtk.SortType.ASCENDING)
else:
self._store.set_sort_column_id(1, Gtk.SortType.ASCENDING)
def _add_row(self, row): # needed for GLib.idle
self._store.append(row)
return False # stop after one run
def _refresh(self, *args):
def callback():
GLib.idle_add(self._workaround_clear)
try: # self._artist_window could still be empty
genre, artists=self._artist_window.get_selected_artists()
except:
GLib.idle_add(self._done_callback)
# show artist names if all albums are shown
if len(artists) > 1:
self._iconview.set_markup_column(2)
else:
self._iconview.set_markup_column(1)
# prepare albmus list (run all mpd related commands)
albums=[]
artist_type=self._settings.get_artist_type()
for artist in artists:
try: # client cloud meanwhile disconnect
if self.stop_flag:
GLib.idle_add(self._done_callback)
return
else:
if genre is None:
album_candidates=self._client.wrapped_call("comp_list", "album", artist_type, artist)
else:
album_candidates=self._client.wrapped_call(
"comp_list", "album", artist_type, artist, "genre", genre
)
for album in album_candidates:
years=self._client.wrapped_call("comp_list", "date", "album", album, artist_type, artist)
for year in years:
songs=self._client.wrapped_call(
"find", "album", album, "date", year, artist_type, artist
)
albums.append({"artist": artist, "album": album, "year": year, "songs": songs})
while Gtk.events_pending():
Gtk.main_iteration_do(True)
except MPDBase.ConnectionError:
GLib.idle_add(self._done_callback)
return
# display albums
if self._settings.get_boolean("sort-albums-by-year"):
2020-09-24 22:17:10 +03:00
albums=sorted(albums, key=lambda k: k["year"])
2020-09-16 16:08:56 +03:00
else:
2020-09-24 22:17:10 +03:00
albums=sorted(albums, key=lambda k: k["album"])
2020-09-16 16:08:56 +03:00
size=self._settings.get_int("album-cover")
for i, album in enumerate(albums):
if self.stop_flag:
break
else:
cover=Cover(self._settings, album["songs"][0]).get_pixbuf(size)
# tooltip
length_human_readable=ClientHelper.calc_display_length(album["songs"])
2020-09-23 17:21:24 +03:00
discs=int(album["songs"][-1].get("disc", 1))
2020-09-16 16:08:56 +03:00
if discs > 1:
2020-09-24 22:17:10 +03:00
tooltip=_("{titles} titles on {discs} discs ({length})").format(
titles=len(album["songs"]), discs=discs, length=length_human_readable)
2020-09-16 16:08:56 +03:00
else:
2020-09-24 22:17:10 +03:00
tooltip=_("{titles} titles ({length})").format(
titles=len(album["songs"]), length=length_human_readable)
2020-09-16 16:08:56 +03:00
# album label
2020-09-24 22:17:10 +03:00
if album["year"] == "":
display_label="<b>{}</b>".format(album["album"])
else:
display_label="<b>{}</b> ({})".format(album["album"], album["year"])
2020-09-16 16:08:56 +03:00
display_label_artist=display_label+"\n"+album["artist"]
display_label=display_label.replace("&", "&amp;")
display_label_artist=display_label_artist.replace("&", "&amp;")
# add album
GLib.idle_add(self._add_row,
[cover, display_label, display_label_artist, tooltip, album["album"], album["year"], album["artist"]]
)
# execute pending events
if i%16 == 0:
while Gtk.events_pending():
Gtk.main_iteration_do(True)
GLib.idle_add(self._done_callback)
if self._done:
self._done=False
callback()
elif not self._refresh in self._pending:
self.stop_flag=True
self._pending.append(self._refresh)
def _path_to_playlist(self, path, mode="default"):
album=self._store[path][4]
year=self._store[path][5]
artist=self._store[path][6]
self._client.wrapped_call("album_to_playlist", album, artist, year, mode)
def _open_album_dialog(self, path):
if self._client.connected():
album=self._store[path][4]
year=self._store[path][5]
artist=self._store[path][6]
album_dialog=AlbumDialog(self._window, self._client, self._settings, album, artist, year)
album_dialog.open()
album_dialog.destroy()
def _done_callback(self, *args):
self.stop_flag=False
self._done=True
pending=self._pending
self._pending=[]
for p in pending:
try:
p()
except:
pass
return False
def _on_button_press_event(self, widget, event):
path=widget.get_path_at_pos(int(event.x), int(event.y))
if event.type == Gdk.EventType.BUTTON_PRESS:
self._button_event=(event.button, path)
def _on_button_release_event(self, widget, event):
path=widget.get_path_at_pos(int(event.x), int(event.y))
if path is not None:
if self._button_event == (event.button, path):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._path_to_playlist(path)
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._path_to_playlist(path, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._open_album_dialog(path)
2020-03-03 15:41:46 +03:00
2020-09-16 16:08:56 +03:00
def _on_key_press_event(self, widget, event):
self.handler_block(self._key_press_event)
2020-09-17 13:52:07 +03:00
if event.keyval == Gdk.keyval_from_name("p"):
2020-09-16 16:08:56 +03:00
paths=self._iconview.get_selected_items()
if len(paths) != 0:
self._path_to_playlist(paths[0])
2020-09-17 13:52:07 +03:00
elif event.keyval == Gdk.keyval_from_name("a"):
2020-09-16 16:08:56 +03:00
paths=self._iconview.get_selected_items()
if len(paths) != 0:
self._path_to_playlist(paths[0], "append")
2020-09-17 13:52:07 +03:00
elif event.keyval == Gdk.keyval_from_name("Menu"):
2020-09-16 16:08:56 +03:00
paths=self._iconview.get_selected_items()
if len(paths) != 0:
self._open_album_dialog(paths[0])
self.handler_unblock(self._key_press_event)
2020-08-21 19:03:36 +03:00
2020-09-16 16:08:56 +03:00
def _on_item_activated(self, widget, path):
treeiter=self._store.get_iter(path)
selected_album=self._store.get_value(treeiter, 4)
selected_album_year=self._store.get_value(treeiter, 5)
selected_artist=self._store.get_value(treeiter, 6)
self._client.wrapped_call("album_to_playlist", selected_album, selected_artist, selected_album_year, "play")
2020-03-03 15:41:46 +03:00
def _on_disconnected(self, *args):
2020-09-16 16:08:56 +03:00
self._iconview.set_sensitive(False)
self._clear()
def _on_reconnected(self, *args):
2020-09-16 16:08:56 +03:00
self._iconview.set_sensitive(True)
2020-09-16 16:08:56 +03:00
def _on_cover_size_changed(self, *args):
def callback():
self._refresh()
return False
GLib.idle_add(callback)
2020-09-15 19:45:30 +03:00
2020-09-16 16:08:56 +03:00
class Browser(Gtk.Paned):
2020-09-24 22:17:10 +03:00
__gsignals__={"search_focus_changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,))}
2020-03-10 15:50:36 +03:00
2020-09-16 16:08:56 +03:00
def __init__(self, client, settings, window):
super().__init__(orientation=Gtk.Orientation.HORIZONTAL) # paned1
2020-03-03 15:41:46 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._client=client
self._settings=settings
2020-09-16 16:08:56 +03:00
self._use_csd=self._settings.get_boolean("use-csd")
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
if self._use_csd:
self._icon_size=0
else:
self._icon_size=self._settings.get_int("icon-size-sec")
# widgets
self._icons={}
2020-09-29 14:02:51 +03:00
icons_data=("go-previous-symbolic", "system-search-symbolic")
2020-08-18 16:31:41 +03:00
for data in icons_data:
self._icons[data]=PixelSizedIcon(data, self._icon_size)
2020-08-18 16:31:41 +03:00
2020-09-29 14:02:51 +03:00
self.back_to_current_album_button=Gtk.Button(image=self._icons["go-previous-symbolic"], tooltip_text=_("Back to current album"))
self.back_to_current_album_button.set_can_focus(False)
self.search_button=Gtk.ToggleButton(image=self._icons["system-search-symbolic"], tooltip_text=_("Search"))
self.search_button.set_can_focus(False)
self._genre_select=GenreSelect(self._client)
self._artist_window=ArtistWindow(self._client, self._settings, self._genre_select)
2020-09-16 16:08:56 +03:00
self._search_window=SearchWindow(self._client)
self._album_window=AlbumWindow(self._client, self._settings, self._artist_window, window)
2020-01-27 22:27:35 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-09-17 13:44:10 +03:00
self.back_to_current_album_button.connect("clicked", self._back_to_current_album)
2020-09-16 16:08:56 +03:00
self.search_button.connect("toggled", self._on_search_toggled)
self._search_window.search_entry.connect("focus_in_event", lambda *args: self.emit("search_focus_changed", True))
self._search_window.search_entry.connect("focus_out_event", lambda *args: self.emit("search_focus_changed", False))
self._artist_window.connect("artists_changed", self._on_artists_changed)
self._settings.connect("notify::mini-player", self._on_mini_player)
if not self._use_csd:
self._settings.connect("changed::icon-size-sec", self._on_icon_size_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-07-04 13:35:39 +03:00
# packing
2020-09-16 16:08:56 +03:00
self._stack=Gtk.Stack(transition_type=Gtk.StackTransitionType.CROSSFADE)
self._stack.add_named(self._album_window, "albums")
self._stack.add_named(self._search_window, "search")
2020-01-11 13:25:15 +03:00
hbox=Gtk.Box(spacing=6, border_width=6)
2020-09-16 16:08:56 +03:00
if self._use_csd:
hbox.pack_start(self._genre_select, True, True, 0)
2020-09-16 16:08:56 +03:00
else:
hbox.pack_start(self.back_to_current_album_button, False, False, 0)
hbox.pack_start(self._genre_select, True, True, 0)
2020-09-16 16:08:56 +03:00
hbox.pack_start(self.search_button, False, False, 0)
box1=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box1.pack_start(hbox, False, False, 0)
box1.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
box1.pack_start(self._artist_window, True, True, 0)
self.pack1(box1, False, False)
2020-09-16 16:08:56 +03:00
self.pack2(self._stack, True, False)
2020-05-17 23:52:10 +03:00
2020-09-16 16:08:56 +03:00
self.set_position(self._settings.get_int("paned1"))
2020-07-04 14:16:17 +03:00
2020-09-16 16:08:56 +03:00
def save_settings(self):
self._settings.set_int("paned1", self.get_position())
2020-09-17 13:44:10 +03:00
def _back_to_current_album(self, *args):
2020-09-16 16:08:56 +03:00
def callback():
try:
2020-09-16 16:08:56 +03:00
song=ClientHelper.song_to_first_str_dict(self._client.wrapped_call("currentsong"))
if song == {}:
return False
except MPDBase.ConnectionError:
return False
self.search_button.set_active(False)
# get artist name
2020-07-04 14:16:17 +03:00
try:
2020-09-16 16:08:56 +03:00
artist=song[self._settings.get_artist_type()]
2020-07-04 14:16:17 +03:00
except:
2020-09-23 17:21:24 +03:00
artist=song.get("artist", "")
2020-09-16 16:08:56 +03:00
# deactivate genre filter to show all artists (if needed)
try:
if song["genre"] != self._genre_select.get_selected_genre():
self._genre_select.deactivate()
2020-09-16 16:08:56 +03:00
except:
self._genre_select.deactivate()
2020-09-16 16:08:56 +03:00
# select artist
if len(self._artist_window.get_selected_artists()[1]) <= 1: # one artist selected
self._artist_window.select(artist)
else: # all artists selected
self.search_button.set_active(False)
self._artist_window.highlight_selected()
self._album_window.scroll_to_current_album()
return False
GLib.idle_add(callback) # ensure it will be executed even when albums are still loading
2020-08-21 19:47:17 +03:00
2020-09-16 16:08:56 +03:00
def _on_search_toggled(self, widget):
if widget.get_active():
self._stack.set_visible_child_name("search")
self._search_window.search_entry.grab_focus()
else:
self._stack.set_visible_child_name("albums")
2020-09-16 16:08:56 +03:00
def _on_reconnected(self, *args):
2020-09-17 13:44:10 +03:00
self._back_to_current_album()
2020-09-16 16:08:56 +03:00
self.back_to_current_album_button.set_sensitive(True)
self.search_button.set_sensitive(True)
2020-08-18 12:54:15 +03:00
def _on_disconnected(self, *args):
2020-09-16 16:08:56 +03:00
self.back_to_current_album_button.set_sensitive(False)
self.search_button.set_active(False)
self.search_button.set_sensitive(False)
2020-08-09 23:58:48 +03:00
2020-09-16 16:08:56 +03:00
def _on_artists_changed(self, *args):
self.search_button.set_active(False)
2020-06-27 17:11:41 +03:00
2020-09-15 19:45:30 +03:00
def _on_mini_player(self, obj, typestring):
visibility=not(obj.get_property("mini-player"))
self.set_property("visible", visibility)
2020-09-16 16:08:56 +03:00
self.back_to_current_album_button.set_property("visible", visibility)
self.search_button.set_property("visible", visibility)
2020-09-15 19:45:30 +03:00
def _on_icon_size_changed(self, *args):
pixel_size=self._settings.get_int("icon-size-sec")
for icon in self._icons.values():
2020-08-18 16:31:41 +03:00
icon.set_pixel_size(pixel_size)
2020-09-16 16:08:56 +03:00
######################
# playlist and cover #
######################
2020-09-16 17:57:58 +03:00
class LyricsWindow(FocusFrame):
2020-09-16 16:08:56 +03:00
def __init__(self, client, settings):
2020-08-31 11:44:23 +03:00
super().__init__()
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._settings=settings
2020-09-16 16:08:56 +03:00
self._client=client
2020-09-16 17:57:58 +03:00
self._displayed_song_file=None
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# text view
2020-09-16 17:57:58 +03:00
self._text_view=Gtk.TextView(
2020-09-16 16:08:56 +03:00
editable=False,
cursor_visible=False,
wrap_mode=Gtk.WrapMode.WORD,
justification=Gtk.Justification.CENTER,
opacity=0.9
2020-09-12 15:31:17 +03:00
)
2020-09-16 17:57:58 +03:00
self._text_view.set_left_margin(5)
2020-09-24 17:50:19 +03:00
self._text_view.set_right_margin(5)
2020-09-16 17:57:58 +03:00
self._text_view.set_bottom_margin(5)
self._text_view.set_top_margin(3)
self.set_widget(self._text_view)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# text buffer
2020-09-16 17:57:58 +03:00
self._text_buffer=self._text_view.get_buffer()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
2020-09-16 17:57:58 +03:00
scroll.add(self._text_view)
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
# connect
2020-09-16 17:57:58 +03:00
self._client.emitter.connect("disconnected", self._on_disconnected)
2020-09-16 16:08:56 +03:00
self._song_changed=self._client.emitter.connect("current_song_changed", self._refresh)
2020-09-16 17:57:58 +03:00
self._client.emitter.handler_block(self._song_changed)
2020-06-26 21:24:06 +03:00
2020-09-16 16:08:56 +03:00
# packing
2020-09-16 17:57:58 +03:00
self.add(scroll)
2020-06-26 21:24:06 +03:00
2020-09-16 17:57:58 +03:00
def enable(self, *args):
current_song=self._client.wrapped_call("currentsong")
if current_song == {}:
if self._displayed_song_file is not None:
self._refresh()
else:
if current_song["file"] != self._displayed_song_file:
self._refresh()
self._client.emitter.handler_unblock(self._song_changed)
GLib.idle_add(self._text_view.grab_focus) # focus textview
def disable(self, *args):
self._client.emitter.handler_block(self._song_changed)
2020-01-11 13:25:15 +03:00
2020-09-24 17:50:19 +03:00
def _get_lyrics(self, title, artist):
2020-09-24 22:17:10 +03:00
replaces=((" ", "+"),(".", "_"),("@", "_"),(",", "_"),(";", "_"),("&", "_"),("\\", "_"),("/", "_"),('"', "_"),("(", "_"),(")", "_"))
2020-09-24 17:50:19 +03:00
for char1, char2 in replaces:
title=title.replace(char1, char2)
artist=artist.replace(char1, char2)
2020-09-24 22:17:10 +03:00
req=requests.get("https://www.letras.mus.br/winamp.php?musica={0}&artista={1}".format(title,artist))
soup=BeautifulSoup(req.text, "html.parser")
2020-09-24 17:50:19 +03:00
soup=soup.find(id="letra-cnt")
if soup is None:
raise ValueError("Not found")
paragraphs=[i for i in soup.children][1] # remove unneded paragraphs (NavigableString)
lyrics=""
for paragraph in paragraphs:
for line in paragraph.stripped_strings:
2020-09-24 22:17:10 +03:00
lyrics+=line+"\n"
lyrics+="\n"
2020-09-24 17:50:19 +03:00
output=lyrics[:-2] # omit last two newlines
if output == "": # assume song is instrumental when lyrics are empty
return "Instrumental"
else:
return output
2020-09-16 16:08:56 +03:00
def _display_lyrics(self, current_song):
GLib.idle_add(self._text_buffer.set_text, _("searching..."), -1)
2020-09-24 17:50:19 +03:00
try:
text=self._get_lyrics(current_song.get("title", ""), current_song.get("artist", ""))
except requests.exceptions.ConnectionError:
self._displayed_song_file=None
text=_("connection error")
except ValueError:
2020-09-23 13:01:10 +03:00
text=_("lyrics not found")
2020-09-16 16:08:56 +03:00
GLib.idle_add(self._text_buffer.set_text, text, -1)
def _refresh(self, *args):
2020-09-16 17:57:58 +03:00
current_song=self._client.wrapped_call("currentsong")
if current_song == {}:
self._displayed_song_file=None
self._text_buffer.set_text("", -1)
2020-09-16 17:57:58 +03:00
else:
self._displayed_song_file=current_song["file"]
update_thread=threading.Thread(
target=self._display_lyrics,
kwargs={"current_song": ClientHelper.song_to_first_str_dict(current_song)},
daemon=True
)
update_thread.start()
2020-01-11 13:25:15 +03:00
2020-09-16 17:57:58 +03:00
def _on_disconnected(self, *args):
self._displayed_song_file=None
self._text_buffer.set_text("", -1)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
class AudioType(Gtk.Label):
def __init__(self, client):
super().__init__()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self._client=client
2020-09-16 16:08:56 +03:00
self._init_vars()
2020-09-16 16:08:56 +03:00
# connect
self._client.emitter.connect("audio", self._on_audio)
self._client.emitter.connect("bitrate", self._on_bitrate)
self._client.emitter.connect("current_song_changed", self._on_song_changed)
self._client.emitter.connect("disconnected", self.clear)
self._client.emitter.connect("state", self._on_state)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def clear(self, *args):
self.set_text("")
self._init_vars()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _init_vars(self):
self.freq=0
self.res=0
self.chan=0
self.brate=0
self.file_type=""
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _refresh(self, *args):
2020-09-24 22:17:10 +03:00
string=_("{bitrate} kb/s, {frequency} kHz, {resolution} bit, {channels} channels, {file_type}").format(
bitrate=self.brate, frequency=self.freq, resolution=self.res, channels=self.chan, file_type=self.file_type)
2020-09-16 16:08:56 +03:00
self.set_text(string)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_audio(self, emitter, freq, res, chan):
try:
self.freq=str(int(freq)/1000)
except:
self.freq=freq
self.res=res
self.chan=chan
self._refresh()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_bitrate(self, emitter, brate):
self.brate=brate
self._refresh()
2020-06-26 21:24:06 +03:00
2020-09-16 16:08:56 +03:00
def _on_song_changed(self, *args):
try:
2020-10-03 14:49:32 +03:00
self.file_type=self._client.wrapped_call("currentsong")["file"].split(".")[-1].split("/")[0]
2020-09-16 16:08:56 +03:00
self._refresh()
except:
pass
2020-06-26 21:24:06 +03:00
2020-09-16 16:08:56 +03:00
def _on_state(self, emitter, state):
if state == "stop":
self.clear()
2020-01-11 13:25:15 +03:00
2020-09-16 21:56:43 +03:00
class CoverEventBox(Gtk.EventBox):
2020-09-16 16:08:56 +03:00
def __init__(self, client, settings, window):
super().__init__()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# adding vars
self._client=client
self._settings=settings
self._window=window
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-09-16 21:56:43 +03:00
self._button_press_event=self.connect("button-press-event", self._on_button_press_event)
2020-09-16 16:08:56 +03:00
self._settings.connect("notify::mini-player", self._on_mini_player)
2020-06-26 21:24:06 +03:00
2020-09-16 16:08:56 +03:00
def _on_button_press_event(self, widget, event):
if self._client.connected():
song=ClientHelper.song_to_first_str_dict(self._client.wrapped_call("currentsong"))
if song != {}:
try:
artist=song[self._settings.get_artist_type()]
except:
2020-09-23 17:21:24 +03:00
artist=song.get("artist", "")
album=song.get("album", "")
album_year=song.get("date", "")
2020-09-16 16:08:56 +03:00
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
self._client.wrapped_call("album_to_playlist", album, artist, album_year)
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
self._client.wrapped_call("album_to_playlist", album, artist, album_year, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
album_dialog=AlbumDialog(self._window, self._client, self._settings, album, artist, album_year)
album_dialog.open()
album_dialog.destroy()
2020-06-26 21:24:06 +03:00
2020-09-16 21:56:43 +03:00
def _on_mini_player(self, obj, typestring):
if obj.get_property("mini-player"):
self.handler_block(self._button_press_event)
else:
self.handler_unblock(self._button_press_event)
class MainCover(Gtk.Frame):
def __init__(self, client, settings):
super().__init__()
# css
style_context=self.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {background-color: @theme_base_color; border-width: 0px}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
# adding vars
self._client=client
self._settings=settings
# cover
self._cover=Gtk.Image.new()
size=self._settings.get_int("track-cover")
self._cover.set_from_pixbuf(Cover(self._settings, {}).get_pixbuf(size)) # set to fallback cover
# set default size
self._cover.set_size_request(size, size)
# connect
self._client.emitter.connect("current_song_changed", self._refresh)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._settings.connect("changed::track-cover", self._on_settings_changed)
self.add(self._cover)
def _refresh(self, *args):
current_song=self._client.wrapped_call("currentsong")
self._cover.set_from_pixbuf(Cover(self._settings, current_song).get_pixbuf(self._settings.get_int("track-cover")))
2020-09-16 16:08:56 +03:00
def _on_disconnected(self, *args):
size=self._settings.get_int("track-cover")
self._cover.set_from_pixbuf(Cover(self._settings, {}).get_pixbuf(size))
self._cover.set_sensitive(False)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_reconnected(self, *args):
self._cover.set_sensitive(True)
def _on_settings_changed(self, *args):
size=self._settings.get_int("track-cover")
self._cover.set_size_request(size, size)
self._refresh()
2020-03-24 18:14:01 +03:00
2020-09-16 16:08:56 +03:00
class PlaylistWindow(Gtk.Box):
def __init__(self, client, settings):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
2020-03-04 18:39:59 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._client=client
self._settings=settings
2020-09-16 16:08:56 +03:00
self._playlist_version=None
self._icon_size=self._settings.get_int("icon-size-sec")
2020-03-04 18:39:59 +03:00
2020-09-16 16:08:56 +03:00
# buttons
self._icons={}
2020-09-29 14:02:51 +03:00
icons_data=("go-previous-symbolic", "edit-clear-symbolic")
2020-09-16 16:08:56 +03:00
for data in icons_data:
self._icons[data]=PixelSizedIcon(data, self._icon_size)
2020-03-24 18:14:01 +03:00
2020-09-16 16:08:56 +03:00
provider=Gtk.CssProvider()
css=b"""* {min-height: 8px;}""" # allow further shrinking
provider.load_from_data(css)
2020-09-16 16:08:56 +03:00
self._back_to_current_song_button=Gtk.Button(
image=self._icons["go-previous-symbolic"],
tooltip_text=_("Scroll to current song"),
2020-09-29 14:02:51 +03:00
relief=Gtk.ReliefStyle.NONE
2020-09-12 15:31:17 +03:00
)
2020-09-29 14:02:51 +03:00
self._back_to_current_song_button.set_can_focus(False)
2020-09-16 16:08:56 +03:00
style_context=self._back_to_current_song_button.get_style_context()
style_context.add_provider(provider, 800)
self._clear_button=Gtk.Button(
image=self._icons["edit-clear-symbolic"],
tooltip_text=_("Clear playlist"),
2020-09-29 14:02:51 +03:00
relief=Gtk.ReliefStyle.NONE
2020-09-16 16:08:56 +03:00
)
2020-09-29 14:02:51 +03:00
self._clear_button.set_can_focus(False)
2020-09-16 16:08:56 +03:00
style_context=self._clear_button.get_style_context()
style_context.add_class("destructive-action")
style_context.add_provider(provider, 800)
2020-03-24 18:14:01 +03:00
2020-09-16 16:08:56 +03:00
# Store
# (track, disc, title, artist, album, duration, date, genre, file, weight)
self._store=Gtk.ListStore(str, str, str, str, str, str, str, str, str, Pango.Weight)
2020-06-26 21:24:06 +03:00
2020-09-16 16:08:56 +03:00
# TreeView
self._treeview=Gtk.TreeView(model=self._store, activate_on_single_click=True)
self._treeview.set_search_column(2)
2020-03-24 18:14:01 +03:00
2020-09-16 16:08:56 +03:00
# selection
self._selection=self._treeview.get_selection()
self._selection.set_mode(Gtk.SelectionMode.SINGLE)
2020-03-24 18:14:01 +03:00
2020-09-16 16:08:56 +03:00
# Columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
self._columns=[None, None, None, None, None, None, None, None]
2020-06-26 21:24:06 +03:00
2020-09-16 16:08:56 +03:00
self._columns[0]=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0, weight=9)
self._columns[1]=Gtk.TreeViewColumn(_("Disc"), renderer_text_ralign, text=1, weight=9)
self._columns[2]=Gtk.TreeViewColumn(_("Title"), renderer_text, text=2, weight=9)
self._columns[3]=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=3, weight=9)
self._columns[4]=Gtk.TreeViewColumn(_("Album"), renderer_text, text=4, weight=9)
self._columns[5]=Gtk.TreeViewColumn(_("Length"), renderer_text, text=5, weight=9)
self._columns[6]=Gtk.TreeViewColumn(_("Year"), renderer_text, text=6, weight=9)
self._columns[7]=Gtk.TreeViewColumn(_("Genre"), renderer_text, text=7, weight=9)
2020-03-24 18:14:01 +03:00
2020-09-16 16:08:56 +03:00
for column in self._columns:
column.set_property("resizable", True)
column.set_min_width(30)
2020-08-21 19:03:36 +03:00
2020-09-16 16:08:56 +03:00
self._load_settings()
2020-08-21 19:03:36 +03:00
2020-09-16 16:08:56 +03:00
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self._treeview)
2020-09-16 16:08:56 +03:00
# frame
frame=FocusFrame()
frame.set_widget(self._treeview)
frame.add(scroll)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# audio infos
audio=AudioType(self._client)
audio.set_xalign(1)
audio.set_ellipsize(Pango.EllipsizeMode.END)
2020-09-16 16:08:56 +03:00
# playlist info
self._playlist_info=Gtk.Label(xalign=0, ellipsize=Pango.EllipsizeMode.END)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# action bar
action_bar=Gtk.ActionBar()
action_bar.pack_start(self._back_to_current_song_button)
self._playlist_info.set_margin_start(3)
action_bar.pack_start(self._playlist_info)
audio.set_margin_end(3)
audio.set_margin_start(12)
action_bar.pack_end(self._clear_button)
action_bar.pack_end(audio)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# connect
self._treeview.connect("row-activated", self._on_row_activated)
self._key_press_event=self._treeview.connect("key-press-event", self._on_key_press_event)
self._treeview.connect("button-press-event", self._on_button_press_event)
self._back_to_current_song_button.connect("clicked", self._on_back_to_current_song_button_clicked)
self._clear_button.connect("clicked", self._on_clear_button_clicked)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
self._client.emitter.connect("playlist_changed", self._on_playlist_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
self._settings.connect("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::column-visibilities", self._load_settings)
self._settings.connect("changed::column-permutation", self._load_settings)
self._settings.connect("changed::icon-size-sec", self._on_icon_size_changed)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# packing
self.pack_start(frame, True, True, 0)
self.pack_end(action_bar, False, False, 0)
2020-08-04 20:37:54 +03:00
2020-09-16 16:08:56 +03:00
def save_settings(self): # only saves the column sizes
columns=self._treeview.get_columns()
permutation=self._settings.get_value("column-permutation").unpack()
sizes=[0] * len(permutation)
for i in range(len(permutation)):
sizes[permutation[i]]=columns[i].get_width()
self._settings.set_value("column-sizes", GLib.Variant("ai", sizes))
2020-06-27 17:11:41 +03:00
2020-09-16 16:08:56 +03:00
def _load_settings(self, *args):
columns=self._treeview.get_columns()
for column in columns:
self._treeview.remove_column(column)
sizes=self._settings.get_value("column-sizes").unpack()
visibilities=self._settings.get_value("column-visibilities").unpack()
for i in self._settings.get_value("column-permutation"):
if sizes[i] > 0:
self._columns[i].set_fixed_width(sizes[i])
self._columns[i].set_visible(visibilities[i])
self._treeview.append_column(self._columns[i])
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _clear(self, *args):
self._playlist_info.set_text("")
self._store.clear()
self._playlist_version=None
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _refresh_playlist_info(self):
songs=self._client.wrapped_call("playlistinfo")
if songs == []:
self._playlist_info.set_text("")
else:
2020-09-24 22:17:10 +03:00
length_human_readable=ClientHelper.calc_display_length(songs)
self._playlist_info.set_text(_("{titles} titles ({length})").format(titles=len(songs), length=length_human_readable))
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _scroll_to_selected_title(self, *args):
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
path=treeview.get_path(treeiter)
self._treeview.scroll_to_cell(path, None, True, 0.25)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _refresh_selection(self): # Gtk.TreePath(len(self._store) is used to generate an invalid TreePath (needed to unset cursor)
self._treeview.set_cursor(Gtk.TreePath(len(self._store)), None, False)
for row in self._store: # reset bold text
row[9]=Pango.Weight.BOOK
try:
song=self._client.wrapped_call("status")["song"]
path=Gtk.TreePath(int(song))
self._selection.select_path(path)
self._store[path][9]=Pango.Weight.BOLD
except:
self._selection.unselect_all()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _remove_song(self, path):
self._client.wrapped_call("delete", path) # bad song index possible
self._store.remove(self._store.get_iter(path))
self._playlist_version=self._client.wrapped_call("status")["playlist"]
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_key_press_event(self, widget, event):
self._treeview.handler_block(self._key_press_event)
2020-09-17 13:52:07 +03:00
if event.keyval == Gdk.keyval_from_name("Delete"):
2020-09-16 16:08:56 +03:00
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
path=self._store.get_path(treeiter)
try:
self._remove_song(path)
except:
pass
2020-09-17 13:52:07 +03:00
elif event.keyval == Gdk.keyval_from_name("Menu"):
2020-09-16 16:08:56 +03:00
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
path=self._store.get_path(treeiter)
cell=self._treeview.get_cell_area(path, None)
file_name=self._store[path][8]
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, int(cell.x), int(cell.y))
pop.popup()
pop.show_all()
self._treeview.handler_unblock(self._key_press_event)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_button_press_event(self, widget, event):
if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self._remove_song(path)
except:
pass
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
pop=SongPopover(self._client.wrapped_call("get_metadata", self._store[path][8]), widget, int(event.x), int(event.y))
pop.popup()
except:
pass
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_row_activated(self, widget, path, view_column):
self._client.wrapped_call("play", path)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_playlist_changed(self, emitter, version):
songs=[]
if self._playlist_version is not None:
songs=self._client.wrapped_call("plchanges", self._playlist_version)
else:
songs=self._client.wrapped_call("playlistinfo")
if songs != []:
self._playlist_info.set_text("")
for s in songs:
song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s))
try:
treeiter=self._store.get_iter(song["pos"])
self._store.set(treeiter,
0, song["track"],
1, song["disc"],
2, song["title"],
3, song["artist"],
4, song["album"],
5, song["human_duration"],
6, song["date"],
7, song["genre"],
8, song["file"],
9, Pango.Weight.BOOK
)
except:
self._store.append([
song["track"], song["disc"],
song["title"], song["artist"],
song["album"], song["human_duration"],
song["date"], song["genre"],
song["file"], Pango.Weight.BOOK
])
for i in reversed(range(int(self._client.wrapped_call("status")["playlistlength"]), len(self._store))):
treeiter=self._store.get_iter(i)
self._store.remove(treeiter)
self._refresh_playlist_info()
if self._playlist_version is None or songs != []:
self._refresh_selection()
self._scroll_to_selected_title()
self._playlist_version=version
2020-06-27 17:11:41 +03:00
2020-09-16 16:08:56 +03:00
def _on_song_changed(self, *args):
self._refresh_selection()
if self._client.wrapped_call("status")["state"] == "play":
self._scroll_to_selected_title()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_back_to_current_song_button_clicked(self, *args):
self._treeview.set_cursor(Gtk.TreePath(len(self._store)), None, False) # set to invalid TreePath (needed to unset cursor)
2020-09-16 16:08:56 +03:00
for path, row in enumerate(self._store):
if row[9] == Pango.Weight.BOLD:
self._selection.select_path(path)
break
self._scroll_to_selected_title()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_clear_button_clicked(self, *args):
self._client.clear()
2020-02-16 14:20:38 +03:00
2020-09-16 16:08:56 +03:00
def _on_disconnected(self, *args):
self._treeview.set_sensitive(False)
self._clear()
self._back_to_current_song_button.set_sensitive(False)
self._clear_button.set_sensitive(False)
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
def _on_reconnected(self, *args):
self._back_to_current_song_button.set_sensitive(True)
self._clear_button.set_sensitive(True)
self._treeview.set_sensitive(True)
def _on_mini_player(self, obj, typestring):
visibility=not(obj.get_property("mini-player"))
self.set_property("visible", visibility)
2020-03-28 15:23:56 +03:00
2020-09-16 16:08:56 +03:00
def _on_icon_size_changed(self, *args):
pixel_size=self._settings.get_int("icon-size-sec")
for icon in self._icons.values():
icon.set_pixel_size(pixel_size)
2020-02-16 14:20:38 +03:00
class CoverPlaylistWindow(Gtk.Paned):
2020-09-16 16:08:56 +03:00
def __init__(self, client, settings, window):
super().__init__()
2020-01-11 13:25:15 +03:00
2020-09-16 16:08:56 +03:00
# adding vars
self._client=client
self._settings=settings
self._window=window
2020-09-16 16:08:56 +03:00
# cover
2020-09-16 21:56:43 +03:00
main_cover=MainCover(self._client, self._settings)
self._cover_event_box=CoverEventBox(self._client, self._settings, self._window)
2020-03-28 15:23:56 +03:00
# playlist
self._playlist_window=PlaylistWindow(self._client, self._settings)
2020-09-16 16:08:56 +03:00
# lyrics button
self.lyrics_button=Gtk.ToggleButton(
2020-09-16 16:08:56 +03:00
image=Gtk.Image.new_from_icon_name("media-view-subtitles-symbolic", Gtk.IconSize.BUTTON),
2020-09-29 14:02:51 +03:00
tooltip_text=_("Show lyrics")
2020-09-16 16:08:56 +03:00
)
2020-09-29 14:02:51 +03:00
self.lyrics_button.set_can_focus(False)
self.lyrics_button.set_margin_top(6)
self.lyrics_button.set_margin_end(6)
style_context=self.lyrics_button.get_style_context()
2020-09-16 16:08:56 +03:00
style_context.add_class("circular")
2020-03-28 15:23:56 +03:00
2020-09-16 17:57:58 +03:00
# lyrics window
self._lyrics_window=LyricsWindow(self._client, self._settings)
2020-09-16 16:08:56 +03:00
# revealer
2020-09-16 17:57:58 +03:00
self._lyrics_button_revealer=Gtk.Revealer()
self._lyrics_button_revealer.set_halign(Gtk.Align.END)
self._lyrics_button_revealer.set_valign(Gtk.Align.START)
self._lyrics_button_revealer.add(self.lyrics_button)
2020-01-11 13:25:15 +03:00
2020-09-16 21:56:43 +03:00
# stack
self._stack=Gtk.Stack(transition_type=Gtk.StackTransitionType.CROSSFADE)
self._stack.add_named(self._cover_event_box, "cover")
self._stack.add_named(self._lyrics_window, "lyrics")
self._stack.set_visible_child(self._cover_event_box)
2020-09-16 16:08:56 +03:00
# connect
self.lyrics_button.connect("toggled", self._on_lyrics_toggled)
2020-09-16 16:08:56 +03:00
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._settings.connect("changed::show-lyrics-button", self._on_settings_changed)
2020-03-03 18:11:30 +03:00
# packing
overlay=Gtk.Overlay()
overlay.add(main_cover)
overlay.add_overlay(self._stack)
overlay.add_overlay(self._lyrics_button_revealer)
self.pack1(overlay, False, False)
self.pack2(self._playlist_window, True, False)
self.set_position(self._settings.get_int("paned0"))
2020-09-16 16:08:56 +03:00
self._on_settings_changed() # hide lyrics button
def save_settings(self):
self._settings.set_int("paned0", self.get_position())
self._playlist_window.save_settings()
2020-09-16 16:08:56 +03:00
def _on_reconnected(self, *args):
self.lyrics_button.set_sensitive(True)
2020-09-16 16:08:56 +03:00
def _on_disconnected(self, *args):
self.lyrics_button.set_active(False)
self.lyrics_button.set_sensitive(False)
2020-02-16 14:20:38 +03:00
2020-09-16 17:57:58 +03:00
def _on_lyrics_toggled(self, widget):
if widget.get_active():
2020-09-16 21:56:43 +03:00
self._stack.set_visible_child(self._lyrics_window)
2020-09-16 17:57:58 +03:00
self._lyrics_window.enable()
else:
2020-09-16 21:56:43 +03:00
self._stack.set_visible_child(self._cover_event_box)
2020-09-16 17:57:58 +03:00
self._lyrics_window.disable()
2020-09-16 16:08:56 +03:00
def _on_settings_changed(self, *args):
if self._settings.get_boolean("show-lyrics-button"):
2020-09-16 17:57:58 +03:00
self._lyrics_button_revealer.set_reveal_child(True)
2020-08-15 14:51:05 +03:00
else:
2020-09-16 17:57:58 +03:00
self._lyrics_button_revealer.set_reveal_child(False)
2020-09-16 16:08:56 +03:00
2020-07-04 14:16:17 +03:00
###################
# control widgets #
###################
2020-08-21 13:40:48 +03:00
class PlaybackControl(Gtk.ButtonBox):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings):
2020-08-31 16:49:06 +03:00
super().__init__(layout_style=Gtk.ButtonBoxStyle.EXPAND)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._client=client
self._settings=settings
self._icon_size=self._settings.get_int("icon-size")
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# widgets
self._icons={}
2020-09-29 14:02:51 +03:00
icons_data=(
2020-09-12 16:00:36 +03:00
"media-playback-start-symbolic", "media-playback-stop-symbolic",
"media-playback-pause-symbolic", "media-skip-backward-symbolic",
"media-skip-forward-symbolic"
2020-09-29 14:02:51 +03:00
)
2020-06-27 17:11:41 +03:00
for data in icons_data:
self._icons[data]=PixelSizedIcon(data, self._icon_size)
2020-06-27 17:11:41 +03:00
2020-09-29 14:02:51 +03:00
self._play_button=Gtk.Button(image=self._icons["media-playback-start-symbolic"])
self._play_button.set_can_focus(False)
self._stop_button=Gtk.Button(image=self._icons["media-playback-stop-symbolic"])
self._stop_button.set_can_focus(False)
self._prev_button=Gtk.Button(image=self._icons["media-skip-backward-symbolic"])
self._prev_button.set_can_focus(False)
self._next_button=Gtk.Button(image=self._icons["media-skip-forward-symbolic"])
self._next_button.set_can_focus(False)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-09-29 13:39:21 +03:00
self._play_button.connect("clicked", self._on_play_clicked)
self._stop_button.connect("clicked", self._on_stop_clicked)
self._stop_button.set_property("no-show-all", not(self._settings.get_boolean("show-stop")))
self._prev_button.connect("clicked", self._on_prev_clicked)
self._next_button.connect("clicked", self._on_next_clicked)
2020-09-15 19:45:30 +03:00
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::icon-size", self._on_icon_size_changed)
self._client.emitter.connect("state", self._on_state)
2020-09-29 13:39:21 +03:00
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# packing
2020-09-29 13:39:21 +03:00
self.pack_start(self._prev_button, True, True, 0)
self.pack_start(self._play_button, True, True, 0)
self.pack_start(self._stop_button, True, True, 0)
self.pack_start(self._next_button, True, True, 0)
2020-01-11 13:25:15 +03:00
def _on_play_clicked(self, widget):
2020-09-29 14:02:51 +03:00
self._client.wrapped_call("toggle_play")
2020-01-11 13:25:15 +03:00
def _on_stop_clicked(self, widget):
2020-09-29 14:02:51 +03:00
self._client.wrapped_call("stop")
2020-01-11 13:25:15 +03:00
def _on_prev_clicked(self, widget):
2020-09-29 14:02:51 +03:00
self._client.wrapped_call("previous")
2020-01-11 13:25:15 +03:00
def _on_next_clicked(self, widget):
2020-09-29 14:02:51 +03:00
self._client.wrapped_call("next")
2020-01-11 13:25:15 +03:00
2020-09-29 13:39:21 +03:00
def _on_state(self, emitter, state):
if state == "play":
self._play_button.set_image(self._icons["media-playback-pause-symbolic"])
self._prev_button.set_sensitive(True)
self._next_button.set_sensitive(True)
elif state == "pause":
self._play_button.set_image(self._icons["media-playback-start-symbolic"])
self._prev_button.set_sensitive(True)
self._next_button.set_sensitive(True)
else:
self._play_button.set_image(self._icons["media-playback-start-symbolic"])
self._prev_button.set_sensitive(False)
self._next_button.set_sensitive(False)
def _on_disconnected(self, *args):
self.set_sensitive(False)
def _on_reconnected(self, *args):
self.set_sensitive(True)
2020-09-15 19:45:30 +03:00
def _on_mini_player(self, obj, typestring):
self._on_show_stop_changed()
def _on_show_stop_changed(self, *args):
2020-09-15 19:45:30 +03:00
visibility=(self._settings.get_boolean("show-stop") and not self._settings.get_property("mini-player"))
2020-09-29 13:39:21 +03:00
self._stop_button.set_property("visible", visibility)
self._stop_button.set_property("no-show-all", not(visibility))
2020-01-11 13:25:15 +03:00
def _on_icon_size_changed(self, *args):
pixel_size=self._settings.get_int("icon-size")
for icon in self._icons.values():
2020-06-27 17:11:41 +03:00
icon.set_pixel_size(pixel_size)
2020-07-04 14:16:17 +03:00
class SeekBar(Gtk.Box):
2020-01-11 13:25:15 +03:00
def __init__(self, client):
super().__init__(hexpand=True)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._client=client
self._update=True
self._jumped=False
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# labels
self._elapsed=Gtk.Label(width_chars=5)
self._rest=Gtk.Label(width_chars=6)
2020-01-11 13:25:15 +03:00
2020-09-18 23:28:51 +03:00
# event boxes
self._elapsed_event_box=Gtk.EventBox()
self._rest_event_box=Gtk.EventBox()
2020-07-04 14:16:17 +03:00
# progress bar
2020-09-29 14:02:51 +03:00
self._scale=Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
self._scale.set_can_focus(False)
2020-09-29 13:39:21 +03:00
self._scale.set_show_fill_level(True)
self._scale.set_restrict_to_fill_level(False)
self._scale.set_draw_value(False)
self._scale.set_increments(10, 60)
self._adjustment=self._scale.get_adjustment()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# css (scale)
2020-09-29 13:39:21 +03:00
style_context=self._scale.get_style_context()
2020-07-04 14:16:17 +03:00
provider=Gtk.CssProvider()
css=b"""scale fill { background-color: @theme_selected_bg_color; }"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# connect
2020-09-18 23:28:51 +03:00
self._elapsed_event_box.connect("button-release-event", self._on_elapsed_button_release_event)
2020-09-29 13:39:21 +03:00
self._elapsed_event_box.connect("button-press-event", lambda *args: self._scale.grab_focus())
2020-09-18 23:28:51 +03:00
self._rest_event_box.connect("button-release-event", self._on_rest_button_release_event)
2020-09-29 13:39:21 +03:00
self._rest_event_box.connect("button-press-event", lambda *args: self._scale.grab_focus())
self._scale.connect("change-value", self._on_change_value)
self._scale.connect("scroll-event", lambda *args: True) # disable mouse wheel
self._scale.connect("button-press-event", self._on_scale_button_press_event)
self._scale.connect("button-release-event", self._on_scale_button_release_event)
2020-09-18 23:28:51 +03:00
self._client.emitter.connect("disconnected", self._disable)
self._client.emitter.connect("state", self._on_state)
2020-08-21 19:03:36 +03:00
self._client.emitter.connect("elapsed_changed", self._refresh)
2020-07-04 14:16:17 +03:00
# packing
self._elapsed_event_box.add(self._elapsed)
self._rest_event_box.add(self._rest)
self.pack_start(self._elapsed_event_box, False, False, 0)
2020-09-29 13:39:21 +03:00
self.pack_start(self._scale, True, True, 0)
self.pack_end(self._rest_event_box, False, False, 0)
2020-01-11 13:25:15 +03:00
2020-08-21 19:03:36 +03:00
def _refresh(self, emitter, elapsed, duration):
2020-09-18 23:28:51 +03:00
self.set_sensitive(True)
2020-08-21 19:03:36 +03:00
if elapsed > duration: # fix display error
elapsed=duration
self._adjustment.set_upper(duration)
2020-08-21 19:03:36 +03:00
if self._update:
2020-09-29 13:39:21 +03:00
self._scale.set_value(elapsed)
2020-09-30 12:06:00 +03:00
self._elapsed.set_text(ClientHelper.seconds_to_display_time(int(elapsed)))
self._rest.set_text("-"+ClientHelper.seconds_to_display_time(int(duration-elapsed)))
2020-09-29 13:39:21 +03:00
self._scale.set_fill_level(elapsed)
2020-08-21 19:03:36 +03:00
def _disable(self, *args):
self.set_sensitive(False)
2020-09-29 13:39:21 +03:00
self._scale.set_fill_level(0)
self._scale.set_range(0, 0)
2020-09-30 12:06:00 +03:00
self._elapsed.set_text("")
self._rest.set_text("")
2020-08-21 19:03:36 +03:00
def _on_scale_button_press_event(self, widget, event):
2020-07-04 14:16:17 +03:00
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
self._update=False
2020-09-29 13:39:21 +03:00
self._scale.set_has_origin(False)
2020-07-04 14:16:17 +03:00
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self._jumped=False
2020-06-05 18:43:34 +03:00
def _on_scale_button_release_event(self, widget, event):
2020-07-04 14:16:17 +03:00
if event.button == 1:
self._update=True
2020-09-29 13:39:21 +03:00
self._scale.set_has_origin(True)
if self._jumped: # actual seek
2020-09-29 13:39:21 +03:00
self._client.wrapped_call("seekcur", self._scale.get_value())
self._jumped=False
2020-09-18 23:28:51 +03:00
else: # restore state
status=self._client.wrapped_call("status")
2020-08-21 19:03:36 +03:00
self._refresh(None, float(status["elapsed"]), float(status["duration"]))
2020-01-11 13:25:15 +03:00
2020-09-18 23:28:51 +03:00
def _on_change_value(self, range, scroll, value): # value is inaccurate (can be above upper limit)
if (scroll == Gtk.ScrollType.STEP_BACKWARD or scroll == Gtk.ScrollType.STEP_FORWARD or
scroll == Gtk.ScrollType.PAGE_BACKWARD or scroll == Gtk.ScrollType.PAGE_FORWARD):
self._client.wrapped_call("seekcur", value)
2020-07-04 14:16:17 +03:00
elif scroll == Gtk.ScrollType.JUMP:
2020-09-18 23:28:51 +03:00
duration=self._adjustment.get_upper()
if value > duration: # fix display error
elapsed=duration
else:
elapsed=value
2020-09-30 12:06:00 +03:00
self._elapsed.set_text(ClientHelper.seconds_to_display_time(int(elapsed)))
self._rest.set_text("-"+ClientHelper.seconds_to_display_time(int(duration-elapsed)))
self._jumped=True
2020-01-11 13:25:15 +03:00
2020-09-18 23:28:51 +03:00
def _on_elapsed_button_release_event(self, widget, event):
if event.button == 1:
self._client.wrapped_call("seekcur", "-"+str(self._adjustment.get_property("step-increment")))
elif event.button == 3:
self._client.wrapped_call("seekcur", "+"+str(self._adjustment.get_property("step-increment")))
2020-09-18 23:28:51 +03:00
def _on_rest_button_release_event(self, widget, event):
if event.button == 1:
self._client.wrapped_call("seekcur", "+"+str(self._adjustment.get_property("step-increment")))
elif event.button == 3:
self._client.wrapped_call("seekcur", "-"+str(self._adjustment.get_property("step-increment")))
def _on_state(self, emitter, state):
if state == "stop":
self._disable()
2020-08-11 20:10:34 +03:00
2020-07-04 14:16:17 +03:00
class PlaybackOptions(Gtk.Box):
def __init__(self, client, settings):
2020-08-31 11:44:23 +03:00
super().__init__(spacing=6)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self._client=client
self._settings=settings
self._icon_size=self._settings.get_int("icon-size")
2020-07-04 14:16:17 +03:00
# widgets
self._icons={}
2020-09-29 14:02:51 +03:00
icons_data=("media-playlist-shuffle-symbolic", "media-playlist-repeat-symbolic", "zoom-original-symbolic", "edit-cut-symbolic")
2020-07-04 14:16:17 +03:00
for data in icons_data:
self._icons[data]=PixelSizedIcon(data, self._icon_size)
2020-09-29 14:02:51 +03:00
self._random_button=Gtk.ToggleButton(image=self._icons["media-playlist-shuffle-symbolic"], tooltip_text=_("Random mode"))
self._random_button.set_can_focus(False)
self._repeat_button=Gtk.ToggleButton(image=self._icons["media-playlist-repeat-symbolic"], tooltip_text=_("Repeat mode"))
self._repeat_button.set_can_focus(False)
self._single_button=Gtk.ToggleButton(image=self._icons["zoom-original-symbolic"], tooltip_text=_("Single mode"))
self._single_button.set_can_focus(False)
self._consume_button=Gtk.ToggleButton(image=self._icons["edit-cut-symbolic"], tooltip_text=_("Consume mode"))
self._consume_button.set_can_focus(False)
self._volume_button=Gtk.VolumeButton(use_symbolic=True, size=self._settings.get_gtk_icon_size("icon-size"))
self._volume_button.set_can_focus(False)
self._volume_button.set_sensitive(False) # do not allow volume change by user when MPD has not yet reported volume
2020-08-24 20:38:00 +03:00
adj=self._volume_button.get_adjustment()
adj.set_step_increment(0.05)
adj.set_page_increment(0.1)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# connect
self._random_button_toggled=self._random_button.connect("toggled", self._set_option, "random")
self._repeat_button_toggled=self._repeat_button.connect("toggled", self._set_option, "repeat")
self._single_button_toggled=self._single_button.connect("toggled", self._set_option, "single")
self._consume_button_toggled=self._consume_button.connect("toggled", self._set_option, "consume")
self._volume_button_changed=self._volume_button.connect("value-changed", self._set_volume)
self._repeat_changed=self._client.emitter.connect("repeat", self._repeat_refresh)
self._random_changed=self._client.emitter.connect("random", self._random_refresh)
self._single_changed=self._client.emitter.connect("single", self._single_refresh)
self._consume_changed=self._client.emitter.connect("consume", self._consume_refresh)
self._volume_changed=self._client.emitter.connect("volume_changed", self._volume_refresh)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-09-15 19:45:30 +03:00
self._settings.connect("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::icon-size", self._on_icon_size_changed)
2020-07-04 14:16:17 +03:00
# packing
2020-09-15 19:45:30 +03:00
self._button_box=Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.EXPAND)
self._button_box.pack_start(self._repeat_button, True, True, 0)
self._button_box.pack_start(self._random_button, True, True, 0)
self._button_box.pack_start(self._single_button, True, True, 0)
self._button_box.pack_start(self._consume_button, True, True, 0)
self.pack_start(self._button_box, True, True, 0)
self.pack_start(self._volume_button, True, True, 0)
2020-01-11 13:25:15 +03:00
def _set_option(self, widget, option):
2020-07-04 14:16:17 +03:00
if widget.get_active():
self._client.wrapped_call(option, "1")
2020-07-04 14:16:17 +03:00
else:
self._client.wrapped_call(option, "0")
def _set_volume(self, widget, value):
self._client.wrapped_call("setvol", str(int(value*100)))
def _repeat_refresh(self, emitter, val):
self._repeat_button.handler_block(self._repeat_button_toggled)
self._repeat_button.set_active(val)
self._repeat_button.handler_unblock(self._repeat_button_toggled)
def _random_refresh(self, emitter, val):
self._random_button.handler_block(self._random_button_toggled)
self._random_button.set_active(val)
self._random_button.handler_unblock(self._random_button_toggled)
def _single_refresh(self, emitter, val):
self._single_button.handler_block(self._single_button_toggled)
self._single_button.set_active(val)
self._single_button.handler_unblock(self._single_button_toggled)
def _consume_refresh(self, emitter, val):
self._consume_button.handler_block(self._consume_button_toggled)
self._consume_button.set_active(val)
self._consume_button.handler_unblock(self._consume_button_toggled)
def _volume_refresh(self, emitter, volume):
self._volume_button.handler_block(self._volume_button_changed)
if volume < 0:
self._volume_button.set_sensitive(False)
self._volume_button.set_value(0)
else:
self._volume_button.set_value(volume/100)
self._volume_button.set_sensitive(True)
self._volume_button.handler_unblock(self._volume_button_changed)
def _on_reconnected(self, *args):
self._repeat_button.set_sensitive(True)
self._random_button.set_sensitive(True)
self._single_button.set_sensitive(True)
self._consume_button.set_sensitive(True)
2020-08-13 18:40:27 +03:00
def _on_disconnected(self, *args):
self._repeat_button.set_sensitive(False)
self._random_button.set_sensitive(False)
self._single_button.set_sensitive(False)
self._consume_button.set_sensitive(False)
self._repeat_refresh(None, False)
self._random_refresh(None, False)
self._single_refresh(None, False)
self._consume_refresh(None, False)
self._volume_refresh(None, -1)
2020-08-13 18:40:27 +03:00
2020-09-15 19:45:30 +03:00
def _on_mini_player(self, obj, typestring):
visibility=not(obj.get_property("mini-player"))
self._button_box.set_property("visible", visibility)
def _on_icon_size_changed(self, *args):
pixel_size=self._settings.get_int("icon-size")
for icon in self._icons.values():
icon.set_pixel_size(pixel_size)
self._volume_button.set_property("size", self._settings.get_gtk_icon_size("icon-size"))
2020-09-29 13:39:21 +03:00
###################
# MPD gio actions #
###################
class MPDActionGroup(Gio.SimpleActionGroup):
def __init__(self, client):
super().__init__()
# adding vars
self._client=client
# actions
self._simple_actions_data=(
"toggle-play","stop","next","prev","seek-forward","seek-backward","clear","update",
"repeat","random","single","consume"
)
for name in self._simple_actions_data:
action=Gio.SimpleAction.new(name, None)
action.connect("activate", getattr(self, ("_on_"+name.replace("-","_"))))
self.add_action(action)
# connect
2020-09-30 12:21:41 +03:00
self._client.emitter.connect("state", self._on_state)
2020-09-29 13:39:21 +03:00
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
def _on_toggle_play(self, action, param):
self._client.wrapped_call("toggle_play")
def _on_stop(self, action, param):
self._client.wrapped_call("stop")
def _on_next(self, action, param):
self._client.wrapped_call("next")
def _on_prev(self, action, param):
self._client.wrapped_call("previous")
def _on_seek_forward(self, action, param):
self._client.wrapped_call("seekcur", "+10")
def _on_seek_backward(self, action, param):
self._client.wrapped_call("seekcur", "-10")
def _on_clear(self, action, param):
self._client.wrapped_call("clear")
def _on_update(self, action, param):
self._client.wrapped_call("update")
def _on_repeat(self, action, param):
self._client.wrapped_call("toggle_option", "repeat")
def _on_random(self, action, param):
self._client.wrapped_call("toggle_option", "random")
def _on_single(self, action, param):
self._client.wrapped_call("toggle_option", "single")
def _on_consume(self, action, param):
self._client.wrapped_call("toggle_option", "consume")
2020-09-30 12:21:41 +03:00
def _on_state(self, emitter, state):
state_dict={"play": True, "pause": True, "stop": False}
for action in ("next","prev","seek-forward","seek-backward"):
self.lookup_action(action).set_enabled(state_dict[state])
2020-09-29 13:39:21 +03:00
def _on_disconnected(self, *args):
for action in self._simple_actions_data:
self.lookup_action(action).set_enabled(False)
def _on_reconnected(self, *args):
for action in self._simple_actions_data:
self.lookup_action(action).set_enabled(True)
2020-09-16 20:15:17 +03:00
####################
# shortcuts window #
####################
class ShortcutsWindow(Gtk.ShortcutsWindow):
def __init__(self):
2020-09-29 15:01:31 +03:00
super().__init__()
2020-09-16 20:15:17 +03:00
general_group=Gtk.ShortcutsGroup(title=_("General"), visible=True)
window_group=Gtk.ShortcutsGroup(title=_("Window"), visible=True)
playback_group=Gtk.ShortcutsGroup(title=_("Playback"), visible=True)
2020-09-17 13:31:25 +03:00
items_group=Gtk.ShortcutsGroup(title=_("Search, Album Dialog and Album List"), visible=True)
playlist_group=Gtk.ShortcutsGroup(title=_("Playlist"), visible=True)
2020-09-16 20:15:17 +03:00
section=Gtk.ShortcutsSection(section_name="shortcuts", visible=True)
section.add(general_group)
section.add(window_group)
section.add(playback_group)
2020-09-17 13:31:25 +03:00
section.add(items_group)
section.add(playlist_group)
2020-09-16 20:15:17 +03:00
2020-09-29 13:39:21 +03:00
shortcut_data=(
("F1", _("Open online help"), None, general_group),
("<Control>question", _("Open shortcuts window"), None, general_group),
("F10", _("Open menu"), None, general_group),
("F5", _("Update database"), None, general_group),
("<Control>q", _("Quit"), None, general_group),
2020-09-30 14:00:24 +03:00
("<Control>p", _("Cycle through profiles"), None, window_group),
("<Shift><Control>p", _("Cycle through profiles in reversed order"), None, window_group),
2020-09-17 13:31:25 +03:00
("<Control>m", _("Toggle mini player"), None, window_group),
("<Control>l", _("Toggle lyrics"), None, window_group),
2020-09-17 13:38:54 +03:00
("<Control>f", _("Toggle search"), None, window_group),
2020-09-17 13:31:25 +03:00
("Escape", _("Back to current album"), None, window_group),
("space", _("Play/Pause"), None, playback_group),
2020-09-29 13:39:21 +03:00
("<Control>space", _("Stop"), None, playback_group),
2020-09-17 13:31:25 +03:00
("KP_Add", _("Next title"), None, playback_group),
("KP_Subtract", _("Previous title"), None, playback_group),
("KP_Multiply", _("Seek forward"), None, playback_group),
("KP_Divide", _("Seek backward"), None, playback_group),
2020-09-29 13:39:21 +03:00
("<Control>r", _("Toggle repeat mode"), None, playback_group),
("<Control>s", _("Toggle random mode"), None, playback_group),
("<Control>1", _("Toggle single mode"), None, playback_group),
("<Control>o", _("Toggle consume mode"), None, playback_group),
2020-09-17 13:31:25 +03:00
("p", _("Play selected item (next)"), _("Left-click"), items_group),
("a", _("Append selected item"), _("Middle-click"), items_group),
("Return", _("Play selected item immediately"), _("Double-click"), items_group),
("Menu", _("Show additional information"), _("Right-click"), items_group),
("Delete", _("Remove selected song"), _("Middle-click"), playlist_group),
2020-09-29 13:39:21 +03:00
("<Shift>Delete", _("Clear playlist"), None, playlist_group),
2020-09-17 13:31:25 +03:00
("Menu", _("Show additional information"), _("Right-click"), playlist_group)
2020-09-29 13:39:21 +03:00
)
2020-09-17 13:31:25 +03:00
for accel, title, subtitle, group in shortcut_data:
shortcut=Gtk.ShortcutsShortcut(visible=True, accelerator=accel, title=title, subtitle=subtitle)
2020-09-16 20:15:17 +03:00
group.pack_start(shortcut, False, False, 0)
self.add(section)
2020-07-04 14:16:17 +03:00
###############
# main window #
###############
class ConnectionNotify(Gtk.Revealer):
def __init__(self, client, settings):
super().__init__(valign=Gtk.Align.START, halign=Gtk.Align.CENTER)
# adding vars
self._client=client
self._settings=settings
# widgets
self._label=Gtk.Label(wrap=True)
close_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON))
close_button.set_relief(Gtk.ReliefStyle.NONE)
connect_button=Gtk.Button(label=_("Connect"))
# connect
close_button.connect("clicked", self._on_close_button_clicked)
connect_button.connect("clicked", self._on_connect_button_clicked)
self._client.emitter.connect("connection_error", self._on_connection_error)
self._client.emitter.connect("reconnected", self._on_reconnected)
# packing
box=Gtk.Box(spacing=12)
box.get_style_context().add_class("app-notification")
box.pack_start(self._label, False, True, 6)
box.pack_end(close_button, False, True, 0)
box.pack_end(connect_button, False, True, 0)
self.add(box)
def _on_connection_error(self, *args):
active=self._settings.get_int("active-profile")
2020-09-30 12:06:00 +03:00
string=_("Connection to “{profile}” ({host}:{port}) failed").format(
2020-09-24 22:17:10 +03:00
profile=self._settings.get_value("profiles")[active],
host=self._settings.get_value("hosts")[active],
port=self._settings.get_value("ports")[active]
)
self._label.set_text(string)
self.set_reveal_child(True)
def _on_reconnected(self, *args):
self.set_reveal_child(False)
def _on_close_button_clicked(self, *args):
self.set_reveal_child(False)
def _on_connect_button_clicked(self, *args):
self._client.reconnect()
2020-01-11 13:25:15 +03:00
class MainWindow(Gtk.ApplicationWindow):
2020-03-30 12:54:04 +03:00
def __init__(self, app, client, settings):
super().__init__(title=("mpdevil"), icon_name="mpdevil", application=app)
2020-03-03 17:59:18 +03:00
Notify.init("mpdevil")
self.set_default_size(settings.get_int("width"), settings.get_int("height"))
2020-09-29 15:01:31 +03:00
shortcuts_window=ShortcutsWindow()
self.set_help_overlay(shortcuts_window)
shortcuts_window.set_modal(False)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self._client=client
self._settings=settings
self._use_csd=self._settings.get_boolean("use-csd")
if self._use_csd:
self._icon_size=0
2020-06-27 17:11:41 +03:00
else:
self._icon_size=self._settings.get_int("icon-size")
2020-09-29 23:19:55 +03:00
self._icons={"open-menu-symbolic": PixelSizedIcon("open-menu-symbolic", self._icon_size)}
self._tmp_saved_size=None # needed to restore size after leaving mini player mode
self._tmp_saved_maximized=None # needed to restore maximize state after leaving mini player mode
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# MPRIS
dbus_service=MPRISInterface(self, self._client, self._settings)
2020-03-21 00:09:13 +03:00
2020-07-04 13:35:39 +03:00
# actions
2020-09-30 11:20:55 +03:00
simple_actions_data=(
"save","settings","stats","help","menu",
"toggle-lyrics","back-to-current-album","toggle-search",
"profile-next","profile-prev"
)
2020-09-16 15:38:58 +03:00
for name in simple_actions_data:
action=Gio.SimpleAction.new(name, None)
action.connect("activate", getattr(self, ("_on_"+name.replace("-","_"))))
self.add_action(action)
2020-09-15 19:45:30 +03:00
mini_player_action=Gio.PropertyAction.new("mini-player", self._settings, "mini-player")
self.add_action(mini_player_action)
2020-09-29 23:19:55 +03:00
self._profiles_action=Gio.SimpleAction.new_stateful("profiles", GLib.VariantType.new("i"), GLib.Variant("i", 0))
self._profiles_action.connect("change-state", self._on_profiles)
self.add_action(self._profiles_action)
2020-09-29 13:39:21 +03:00
self._mpd_action_group=MPDActionGroup(self._client)
self.insert_action_group("mpd", self._mpd_action_group)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# widgets
self._browser=Browser(self._client, self._settings, self)
self._cover_playlist_window=CoverPlaylistWindow(self._client, self._settings, self)
2020-09-29 13:39:21 +03:00
playback_control=PlaybackControl(self._client, self._settings)
seek_bar=SeekBar(self._client)
playback_options=PlaybackOptions(self._client, self._settings)
connection_notify=ConnectionNotify(self._client, self._settings)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# menu
2020-05-28 23:46:38 +03:00
subsection=Gio.Menu()
subsection.append(_("Settings"), "win.settings")
2020-09-16 20:15:17 +03:00
subsection.append(_("Keyboard shortcuts"), "win.show-help-overlay")
2020-05-28 23:46:38 +03:00
subsection.append(_("Help"), "win.help")
subsection.append(_("About"), "app.about")
subsection.append(_("Quit"), "app.quit")
2020-09-11 14:48:40 +03:00
mpd_subsection=Gio.Menu()
2020-09-29 13:39:21 +03:00
mpd_subsection.append(_("Update database"), "mpd.update")
2020-09-11 14:48:40 +03:00
mpd_subsection.append(_("Server stats"), "win.stats")
2020-09-29 23:19:55 +03:00
self._profiles_submenu=Gio.Menu()
self._refresh_profiles_menu()
2020-04-09 01:26:21 +03:00
menu=Gio.Menu()
2020-09-29 23:19:55 +03:00
menu.append_submenu(_("Profiles"), self._profiles_submenu)
2020-09-15 19:45:30 +03:00
menu.append(_("Mini player"), "win.mini-player")
2020-09-29 23:19:55 +03:00
menu.append(_("Save window layout"), "win.save")
2020-09-11 14:48:40 +03:00
menu.append_section(None, mpd_subsection)
2020-05-28 23:46:38 +03:00
menu.append_section(None, subsection)
2020-01-11 13:25:15 +03:00
2020-09-29 23:19:55 +03:00
# menu button / popover
2020-09-29 14:02:51 +03:00
self._menu_button=Gtk.MenuButton(image=self._icons["open-menu-symbolic"], tooltip_text=_("Menu"))
self._menu_button.set_can_focus(False)
2020-09-29 13:39:21 +03:00
menu_popover=Gtk.Popover.new_from_model(self._menu_button, menu)
self._menu_button.set_popover(menu_popover)
# action bar
action_bar=Gtk.ActionBar()
2020-09-29 13:39:21 +03:00
action_bar.pack_start(playback_control)
action_bar.pack_start(seek_bar)
action_bar.pack_start(playback_options)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-09-29 23:19:55 +03:00
self._settings.connect("changed::profiles", self._refresh_profiles_menu)
self._settings.connect("changed::active-profile", self._on_active_profile_changed)
2020-09-15 19:45:30 +03:00
self._settings.connect_after("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::playlist-right", self._on_playlist_pos_changed)
if not self._use_csd:
self._settings.connect("changed::icon-size", self._on_icon_size_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
2020-09-16 15:38:58 +03:00
self._browser.connect("search_focus_changed", self._on_search_focus_changed)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# packing
self._paned2=Gtk.Paned()
self._paned2.set_position(self._settings.get_int("paned2"))
self._on_playlist_pos_changed() # set orientation
self._paned2.pack1(self._browser, True, False)
self._paned2.pack2(self._cover_playlist_window, False, False)
vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
vbox.pack_start(self._paned2, True, True, 0)
vbox.pack_start(action_bar, False, False, 0)
overlay=Gtk.Overlay()
overlay.add(vbox)
overlay.add_overlay(connection_notify)
if self._use_csd:
self._header_bar=Gtk.HeaderBar()
self._header_bar.set_show_close_button(True)
self.set_titlebar(self._header_bar)
self._header_bar.pack_start(self._browser.back_to_current_album_button)
2020-09-29 13:39:21 +03:00
self._header_bar.pack_end(self._menu_button)
self._header_bar.pack_end(self._browser.search_button)
else:
action_bar.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.VERTICAL))
2020-09-29 13:39:21 +03:00
action_bar.pack_start(self._menu_button)
2020-01-11 13:25:15 +03:00
self.add(overlay)
2020-01-27 00:38:06 +03:00
2020-01-19 02:00:40 +03:00
self.show_all()
if self._settings.get_boolean("maximize"):
2020-05-21 22:33:39 +03:00
self.maximize()
self._client.start() # connect client
2020-01-11 13:25:15 +03:00
2020-09-29 13:39:21 +03:00
def _on_toggle_lyrics(self, action, param):
self._cover_playlist_window.lyrics_button.set_active(not(self._cover_playlist_window.lyrics_button.get_active()))
def _on_back_to_current_album(self, action, param):
self._browser.back_to_current_album_button.emit("clicked")
def _on_toggle_search(self, action, param):
self._browser.search_button.set_active(not(self._browser.search_button.get_active()))
def _on_save(self, action, param):
size=self.get_size()
self._settings.set_int("width", size[0])
self._settings.set_int("height", size[1])
self._settings.set_boolean("maximize", self.is_maximized())
self._browser.save_settings()
self._cover_playlist_window.save_settings()
self._settings.set_int("paned2", self._paned2.get_position())
def _on_settings(self, action, param):
settings=SettingsDialog(self, self._client, self._settings)
settings.run()
settings.destroy()
def _on_stats(self, action, param):
stats=ServerStats(self, self._client, self._settings)
stats.destroy()
def _on_help(self, action, param):
Gtk.show_uri_on_window(self, "https://github.com/SoongNoonien/mpdevil/wiki/Usage", Gdk.CURRENT_TIME)
def _on_menu(self, action, param):
self._menu_button.emit("clicked")
2020-09-30 11:20:55 +03:00
def _on_profile_next(self, action, param):
total_profiles=len(self._settings.get_value("profiles"))
current_profile=self._settings.get_int("active-profile")
self._settings.set_int("active-profile", (current_profile+1)%total_profiles)
def _on_profile_prev(self, action, param):
total_profiles=len(self._settings.get_value("profiles"))
current_profile=self._settings.get_int("active-profile")
self._settings.set_int("active-profile", (current_profile-1)%total_profiles)
2020-09-29 23:19:55 +03:00
def _on_profiles(self, action, param):
self._settings.set_int("active-profile", param.unpack())
action.set_state(param)
def _on_song_changed(self, *args):
song=self._client.wrapped_call("currentsong")
2020-07-15 23:39:26 +03:00
if song == {}:
if self._use_csd:
2020-08-29 11:51:31 +03:00
self.set_title("mpdevil")
self._header_bar.set_subtitle("")
2020-07-15 23:39:26 +03:00
else:
self.set_title("mpdevil")
else:
song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(song))
2020-08-21 13:03:42 +03:00
if song["date"] == "":
date=""
2020-08-21 13:03:42 +03:00
else:
date=" ("+song["date"]+")"
if self._use_csd:
2020-08-29 11:51:31 +03:00
self.set_title(song["title"]+" - "+song["artist"])
self._header_bar.set_subtitle(song["album"]+date)
else:
2020-05-24 23:42:43 +03:00
self.set_title(song["title"]+" - "+song["artist"]+" - "+song["album"]+date)
if self._settings.get_boolean("send-notify"):
if not self.is_active() and self._client.wrapped_call("status")["state"] == "play":
notify=Notify.Notification.new(song["title"], song["artist"]+"\n"+song["album"]+date)
pixbuf=Cover(self._settings, song).get_pixbuf(400)
2020-03-22 16:25:04 +03:00
notify.set_image_from_pixbuf(pixbuf)
notify.show()
def _on_reconnected(self, *args):
2020-09-29 13:39:21 +03:00
for action in ("save","stats","toggle-lyrics","back-to-current-album","toggle-search"):
self.lookup_action(action).set_enabled(True)
def _on_disconnected(self, *args):
2020-09-11 00:40:27 +03:00
self.set_title("mpdevil")
if self._use_csd:
2020-09-11 00:40:27 +03:00
self._header_bar.set_subtitle("")
2020-09-29 13:39:21 +03:00
for action in ("save","stats","toggle-lyrics","back-to-current-album","toggle-search"):
self.lookup_action(action).set_enabled(False)
2020-01-11 13:25:15 +03:00
2020-09-16 15:38:58 +03:00
def _on_search_focus_changed(self, obj, focus):
2020-09-29 13:39:21 +03:00
self._mpd_action_group.lookup_action("toggle-play").set_enabled(not(focus))
2020-05-26 23:53:59 +03:00
2020-09-15 19:45:30 +03:00
def _on_mini_player(self, obj, typestring):
if obj.get_property("mini-player"):
2020-09-29 13:39:21 +03:00
self.lookup_action("save").set_enabled(False)
self._tmp_saved_size=self.get_size()
self._tmp_saved_miximized=self.is_maximized()
if self._tmp_saved_miximized:
2020-09-15 20:28:40 +03:00
self.unmaximize()
2020-09-15 19:45:30 +03:00
self.resize(1,1)
2020-03-24 18:14:01 +03:00
else:
2020-09-16 15:38:58 +03:00
self.lookup_action("save").set_enabled(True)
self.resize(self._tmp_saved_size[0], self._tmp_saved_size[1])
if self._tmp_saved_miximized:
self.maximize()
self._tmp_saved_size=None
self._tmp_saved_maximized=None
2020-02-07 22:13:38 +03:00
def _on_playlist_pos_changed(self, *args):
if self._settings.get_boolean("playlist-right"):
self._cover_playlist_window.set_orientation(Gtk.Orientation.VERTICAL)
self._paned2.set_orientation(Gtk.Orientation.HORIZONTAL)
else:
self._cover_playlist_window.set_orientation(Gtk.Orientation.HORIZONTAL)
self._paned2.set_orientation(Gtk.Orientation.VERTICAL)
2020-09-29 23:19:55 +03:00
def _refresh_profiles_menu(self, *args):
self._profiles_submenu.remove_all()
for num, profile in enumerate(self._settings.get_value("profiles")):
item=Gio.MenuItem.new(profile, None)
item.set_action_and_target_value("win.profiles", GLib.Variant("i", num))
self._profiles_submenu.append_item(item)
self._profiles_action.set_state(GLib.Variant("i", self._settings.get_int("active-profile")))
def _on_active_profile_changed(self, *args):
self._profiles_action.set_state(GLib.Variant("i", self._settings.get_int("active-profile")))
def _on_icon_size_changed(self, *args):
pixel_size=self._settings.get_int("icon-size")
for icon in self._icons.values():
2020-06-27 17:11:41 +03:00
icon.set_pixel_size(pixel_size)
2020-07-04 14:16:17 +03:00
###################
# Gtk application #
###################
2020-01-11 13:25:15 +03:00
class mpdevil(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.mpdevil", flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs)
self._settings=Settings()
self._client=Client(self._settings)
self._window=None
2020-01-11 13:25:15 +03:00
def do_activate(self):
if not self._window: # allow just one instance
self._window=MainWindow(self, self._client, self._settings)
self._window.connect("delete-event", self._on_delete_event)
2020-09-16 15:38:58 +03:00
# accelerators
2020-09-29 13:39:21 +03:00
action_accels=(
("app.quit", ["<Control>q"]),("win.mini-player", ["<Control>m"]),("win.help", ["F1"]),("win.menu", ["F10"]),
("win.show-help-overlay", ["<Control>question"]),("win.toggle-lyrics", ["<Control>l"]),
("win.back-to-current-album", ["Escape"]),("win.toggle-search", ["<control>f"]),
("mpd.update", ["F5"]),("mpd.clear", ["<Shift>Delete"]),("mpd.toggle-play", ["space"]),
("mpd.stop", ["<Control>space"]),("mpd.next", ["KP_Add"]),("mpd.prev", ["KP_Subtract"]),
("mpd.repeat", ["<Control>r"]),("mpd.random", ["<Control>s"]),("mpd.single", ["<Control>1"]),
2020-09-30 11:20:55 +03:00
("mpd.consume", ["<Control>o"]),("mpd.seek-forward", ["KP_Multiply"]),("mpd.seek-backward", ["KP_Divide"]),
("win.profile-next", ["<Control>p"]),("win.profile-prev", ["<Shift><Control>p"])
2020-09-29 13:39:21 +03:00
)
for action, accels in action_accels:
self.set_accels_for_action(action, accels)
self._window.present()
2020-01-11 13:25:15 +03:00
def do_startup(self):
Gtk.Application.do_startup(self)
2020-04-09 01:26:21 +03:00
action=Gio.SimpleAction.new("about", None)
action.connect("activate", self._on_about)
2020-01-11 13:25:15 +03:00
self.add_action(action)
2020-04-09 01:26:21 +03:00
action=Gio.SimpleAction.new("quit", None)
action.connect("activate", self._on_quit)
2020-01-11 13:25:15 +03:00
self.add_action(action)
def _on_delete_event(self, *args):
if self._settings.get_boolean("stop-on-quit") and self._client.connected():
self._client.wrapped_call("stop")
2020-01-11 13:25:15 +03:00
self.quit()
def _on_about(self, action, param):
dialog=AboutDialog(self._window)
2020-01-11 13:25:15 +03:00
dialog.run()
dialog.destroy()
def _on_quit(self, action, param):
if self._settings.get_boolean("stop-on-quit") and self._client.connected():
self._client.wrapped_call("stop")
2020-01-11 13:25:15 +03:00
self.quit()
2020-09-24 22:17:10 +03:00
if __name__ == "__main__":
2020-04-09 01:26:21 +03:00
app=mpdevil()
2020-01-11 13:25:15 +03:00
app.run(sys.argv)