mpdevil/bin/mpdevil.py

3635 lines
125 KiB
Python
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-21 00:09:13 +03:00
# MPRIS interface based on 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun <eon@patapon.info>, Mantas Mikulėnas <grawity@gmail.com>
2020-03-22 19:05:51 +03:00
import gi
2020-01-11 13:25:15 +03:00
gi.require_version('Gtk', '3.0')
2020-03-03 17:59:18 +03:00
gi.require_version('Notify', '0.7')
from gi.repository import Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify
from mpd import MPDClient, base as MPDBase
2020-07-04 13:35:39 +03:00
import requests
2020-03-22 19:05:51 +03:00
from bs4 import BeautifulSoup, Comment
import threading
2020-01-11 13:25:15 +03:00
import locale
import gettext
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
import base64
2020-04-09 01:26:21 +03:00
DATADIR='@datadir@'
NAME='mpdevil'
VERSION='@version@'
PACKAGE=NAME.lower()
2020-03-22 19:05:51 +03:00
2020-07-04 14:16:17 +03:00
#################
# lang settings #
#################
2020-03-22 19:05:51 +03:00
try:
locale.setlocale(locale.LC_ALL, '')
locale.bindtextdomain(PACKAGE, '@datadir@/locale')
gettext.bindtextdomain(PACKAGE, '@datadir@/locale')
gettext.textdomain(PACKAGE)
gettext.install(PACKAGE, localedir='@datadir@/locale')
except locale.Error:
print(' cannot use system locale.')
locale.setlocale(locale.LC_ALL, 'C')
gettext.textdomain(PACKAGE)
gettext.install(PACKAGE, localedir='@datadir@/locale')
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
__introspect_interface="org.freedesktop.DBus.Introspectable"
__prop_interface=dbus.PROPERTIES_IFACE
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# python dbus bindings don't include annotations and properties
MPRIS2_INTROSPECTION="""<node name="/org/mpris/MediaPlayer2">
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg direction="out" name="xml_data" type="s"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg direction="in" name="interface_name" type="s"/>
<arg direction="in" name="property_name" type="s"/>
<arg direction="out" name="value" type="v"/>
</method>
<method name="GetAll">
<arg direction="in" name="interface_name" type="s"/>
<arg direction="out" name="properties" type="a{sv}"/>
</method>
<method name="Set">
<arg direction="in" name="interface_name" type="s"/>
<arg direction="in" name="property_name" type="s"/>
<arg direction="in" name="value" type="v"/>
</method>
<signal name="PropertiesChanged">
<arg name="interface_name" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise"/>
<method name="Quit"/>
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
<property name="CanQuit" type="b" access="read"/>
<property name="CanRaise" type="b" access="read"/>
<property name="HasTrackList" type="b" access="read"/>
<property name="Identity" type="s" access="read"/>
<property name="DesktopEntry" type="s" access="read"/>
<property name="SupportedUriSchemes" type="as" access="read"/>
<property name="SupportedMimeTypes" type="as" access="read"/>
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next"/>
<method name="Previous"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Seek">
<arg direction="in" name="Offset" type="x"/>
</method>
<method name="SetPosition">
<arg direction="in" name="TrackId" type="o"/>
<arg direction="in" name="Position" type="x"/>
</method>
<method name="OpenUri">
<arg direction="in" name="Uri" type="s"/>
</method>
<signal name="Seeked">
<arg name="Position" type="x"/>
</signal>
<property name="PlaybackStatus" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="LoopStatus" type="s" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="Rate" type="d" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="Shuffle" type="b" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="Metadata" type="a{sv}" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="Volume" type="d" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
</property>
<property name="Position" type="x" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
</property>
<property name="MinimumRate" type="d" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="MaximumRate" type="d" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanGoNext" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanGoPrevious" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanPlay" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanPause" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanSeek" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<property name="CanControl" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
</property>
</interface>
</node>"""
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# MPRIS allowed metadata tags
allowed_tags={
'mpris:trackid': dbus.ObjectPath,
'mpris:length': dbus.Int64,
'mpris:artUrl': str,
'xesam:album': str,
'xesam:albumArtist': list,
'xesam:artist': list,
'xesam:asText': str,
'xesam:audioBPM': int,
'xesam:comment': list,
'xesam:composer': list,
'xesam:contentCreated': str,
'xesam:discNumber': int,
'xesam:firstUsed': str,
'xesam:genre': list,
'xesam:lastUsed': str,
'xesam:lyricist': str,
'xesam:title': str,
'xesam:trackNumber': int,
'xesam:url': str,
'xesam:useCount': int,
'xesam:userRating': float,
}
2020-06-27 17:11:41 +03:00
2020-07-04 14:16:17 +03:00
def __init__(self, window, client, settings):
dbus.service.Object.__init__(self, dbus.SessionBus(), "/org/mpris/MediaPlayer2")
self._name="org.mpris.MediaPlayer2.mpdevil"
2020-07-04 14:16:17 +03:00
self._bus=dbus.SessionBus()
self._uname=self._bus.get_unique_name()
self._dbus_obj=self._bus.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus")
self._dbus_obj.connect_to_signal("NameOwnerChanged", self._name_owner_changed_callback, arg0=self._name)
2020-07-04 14:16:17 +03:00
self.window=window
self.client=client
self.settings=settings
self.metadata={}
2020-03-30 18:08:59 +03:00
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)
def on_state_changed(self, *args):
2020-07-04 14:16:17 +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')
def on_song_changed(self, *args):
2020-07-04 14:16:17 +03:00
self.update_metadata()
self.update_property('org.mpris.MediaPlayer2.Player', 'Metadata')
2020-07-04 14:16:17 +03:00
def on_volume_changed(self, *args):
self.update_property('org.mpris.MediaPlayer2.Player', 'Volume')
2020-01-11 13:25:15 +03:00
def on_loop_changed(self, *args):
2020-07-04 14:16:17 +03:00
self.update_property('org.mpris.MediaPlayer2.Player', 'LoopStatus')
def on_random_changed(self, *args):
2020-07-04 14:16:17 +03:00
self.update_property('org.mpris.MediaPlayer2.Player', 'Shuffle')
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def update_metadata(self): # TODO
"""
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-07-04 14:16:17 +03:00
self.metadata={}
2020-07-04 14:16:17 +03:00
for tag in ('album', 'title'):
if tag in mpd_meta:
self.metadata['xesam:%s' % tag]=mpd_meta[tag]
2020-07-04 14:16:17 +03:00
if 'id' in mpd_meta:
self.metadata['mpris:trackid']="/org/mpris/MediaPlayer2/Track/%s" % mpd_meta['id']
2020-07-04 14:16:17 +03:00
if 'time' in mpd_meta:
self.metadata['mpris:length']=int(mpd_meta['time']) * 1000000
2020-07-04 14:16:17 +03:00
if 'date' in mpd_meta:
self.metadata['xesam:contentCreated']=mpd_meta['date'][0:4]
2020-07-04 14:16:17 +03:00
if 'track' in mpd_meta:
# TODO: Is it even *possible* for mpd_meta['track'] to be a list?
if type(mpd_meta['track']) == list and len(mpd_meta['track']) > 0:
track=str(mpd_meta['track'][0])
else:
track=str(mpd_meta['track'])
2020-07-04 14:16:17 +03:00
m=re.match('^([0-9]+)', track)
if m:
self.metadata['xesam:trackNumber']=int(m.group(1))
# Ensure the integer is signed 32bit
if self.metadata['xesam:trackNumber'] & 0x80000000:
self.metadata['xesam:trackNumber'] += -0x100000000
else:
self.metadata['xesam:trackNumber']=0
2020-07-04 14:16:17 +03:00
if 'disc' in mpd_meta:
# TODO: Same as above. When is it a list?
if type(mpd_meta['disc']) == list and len(mpd_meta['disc']) > 0:
disc=str(mpd_meta['disc'][0])
else:
disc=str(mpd_meta['disc'])
2020-07-04 14:16:17 +03:00
m=re.match('^([0-9]+)', disc)
if m:
self.metadata['xesam:discNumber']=int(m.group(1))
2020-03-22 16:25:04 +03:00
2020-07-04 14:16:17 +03:00
if 'artist' in mpd_meta:
if type(mpd_meta['artist']) == list:
self.metadata['xesam:artist']=mpd_meta['artist']
else:
self.metadata['xesam:artist']=[mpd_meta['artist']]
2020-02-22 17:22:57 +03:00
2020-07-04 14:16:17 +03:00
if 'composer' in mpd_meta:
if type(mpd_meta['composer']) == list:
self.metadata['xesam:composer']=mpd_meta['composer']
else:
self.metadata['xesam:composer']=[mpd_meta['composer']]
2020-07-04 14:16:17 +03:00
# Stream: populate some missings tags with stream's name
if 'name' in mpd_meta:
if 'xesam:title' not in self.metadata:
self.metadata['xesam:title']=mpd_meta['name']
elif 'xesam:album' not in self.metadata:
self.metadata['xesam:album']=mpd_meta['name']
2020-07-04 14:16:17 +03:00
if 'file' in mpd_meta:
song_file=mpd_meta['file']
self.metadata['xesam:url']="file://"+os.path.join(self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file)
cover=Cover(lib_path=self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file=song_file)
if not cover.path == None:
self.metadata['mpris:artUrl']="file://"+cover.path
else:
self.metadata['mpris:artUrl']=None
2020-07-04 14:16:17 +03:00
# Cast self.metadata to the correct type, or discard it
for key, value in self.metadata.items():
try:
self.metadata[key]=self.allowed_tags[key](value)
except ValueError:
del self.metadata[key]
2020-07-04 14:16:17 +03:00
def _name_owner_changed_callback(self, name, old_owner, new_owner):
if name == self._name and old_owner == self._uname and new_owner != "":
try:
2020-07-04 14:16:17 +03:00
pid=self._dbus_obj.GetConnectionUnixProcessID(new_owner)
except:
2020-07-04 14:16:17 +03:00
pid=None
loop.quit()
2020-07-04 14:16:17 +03:00
def acquire_name(self):
self._bus_name=dbus.service.BusName(self._name, bus=self._bus, allow_replacement=True, replace_existing=True)
2020-07-04 14:16:17 +03:00
def release_name(self):
if hasattr(self, "_bus_name"):
del self._bus_name
2020-03-30 12:54:04 +03:00
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):
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
return {'play': 'Playing', 'pause': 'Paused', 'stop': 'Stopped'}[status['state']]
2020-07-04 14:16:17 +03:00
def __set_loop_status(self, value):
if value == "Playlist":
self.client.wrapped_call("repeat", 1)
self.client.wrapped_call("single", 0)
2020-07-04 14:16:17 +03:00
elif value == "Track":
self.client.wrapped_call("repeat", 1)
self.client.wrapped_call("single", 1)
2020-07-04 14:16:17 +03:00
elif value == "None":
self.client.wrapped_call("repeat", 0)
self.client.wrapped_call("single", 0)
2020-07-04 14:16:17 +03:00
else:
raise dbus.exceptions.DBusException("Loop mode %r not supported" % value)
return
2020-07-04 14:16:17 +03:00
def __get_loop_status(self):
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
if int(status['repeat']) == 1:
if int(status.get('single', 0)) == 1:
return "Track"
else:
return "Playlist"
else:
return "None"
def __set_shuffle(self, value):
self.client.wrapped_call("random", value)
2020-07-04 14:16:17 +03:00
return
def __get_shuffle(self):
if int(self.client.wrapped_call("status")['random']) == 1:
return True
2020-07-04 14:16:17 +03:00
else:
return False
2020-07-04 14:16:17 +03:00
def __get_metadata(self):
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):
vol=float(self.client.wrapped_call("status").get('volume', 0))
2020-07-04 14:16:17 +03:00
if vol > 0:
return vol / 100.0
2020-03-30 12:54:04 +03:00
else:
2020-07-04 14:16:17 +03:00
return 0.0
2020-07-04 14:16:17 +03:00
def __set_volume(self, value):
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):
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
if 'time' in status:
current, end=status['time'].split(':')
return dbus.Int64((int(current) * 1000000))
else:
2020-07-04 14:16:17 +03:00
return dbus.Int64(0)
2020-07-04 14:16:17 +03:00
def __get_can_next_prev(self):
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
if status['state'] == "stop":
return False
2020-07-04 14:16:17 +03:00
else:
return True
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),
"CanPlay": (True, None),
"CanPause": (True, None),
"CanSeek": (True, None),
"CanControl": (True, None),
}
2020-04-07 19:02:43 +03:00
2020-07-04 14:16:17 +03:00
__prop_mapping={
__player_interface: __player_props,
__root_interface: __root_props,
2020-04-07 19:02:43 +03:00
}
2020-07-04 14:16:17 +03:00
@dbus.service.method(__introspect_interface)
def Introspect(self):
return self.MPRIS2_INTROSPECTION
2020-03-21 00:09:13 +03:00
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
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
# Root methods
@dbus.service.method(__root_interface, in_signature='', out_signature='')
def Raise(self):
self.window.present()
return
@dbus.service.method(__root_interface, in_signature='', out_signature='')
def Quit(self):
return
# Player methods
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def Next(self):
self.client.wrapped_call("next")
2020-07-04 14:16:17 +03:00
return
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def Previous(self):
self.client.wrapped_call("previous")
2020-07-04 14:16:17 +03:00
return
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def Pause(self):
self.client.wrapped_call("pause", 1)
2020-07-04 14:16:17 +03:00
return
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def PlayPause(self):
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +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
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def Stop(self):
self.client.wrapped_call("stop")
2020-07-04 14:16:17 +03:00
return
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def Play(self):
self.client.wrapped_call("play")
2020-07-04 14:16:17 +03:00
return
@dbus.service.method(__player_interface, in_signature='x', out_signature='')
def Seek(self, offset): # TODO
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
current, end=status['time'].split(':')
current=int(current)
end=int(end)
offset=int(offset) / 1000000
if current + offset <= end:
position=current + offset
if position < 0:
position=0
self.client.wrapped_call("seekid", int(status['songid']), position)
2020-07-04 14:16:17 +03:00
self.Seeked(position * 1000000)
return
@dbus.service.method(__player_interface, in_signature='ox', out_signature='')
def SetPosition(self, trackid, position):
song=self.client.wrapped_call("currentsong")
2020-07-04 14:16:17 +03:00
# FIXME: use real dbus objects
if str(trackid) != '/org/mpris/MediaPlayer2/Track/%s' % song['id']:
return
# Convert position to seconds
position=int(position) / 1000000
if position <= int(song['time']):
self.client.wrapped_call("seekid", int(song['id']), position)
2020-07-04 14:16:17 +03:00
self.Seeked(position * 1000000)
return
@dbus.service.signal(__player_interface, signature='x')
def Seeked(self, position):
return float(position)
@dbus.service.method(__player_interface, in_signature='', out_signature='')
def OpenUri(self):
return
#################################
# small general purpose widgets #
#################################
class IntEntry(Gtk.SpinButton):
def __init__(self, default, lower, upper, step):
Gtk.SpinButton.__init__(self)
adj=Gtk.Adjustment(value=default, lower=lower, upper=upper, step_increment=step)
self.set_adjustment(adj)
def get_int(self):
return int(self.get_value())
def set_int(self, value):
self.set_value(value)
class PixelSizedIcon(Gtk.Image):
def __init__(self, icon_name, pixel_size):
Gtk.Image.__init__(self)
self.set_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
if pixel_size > 0:
self.set_pixel_size(pixel_size)
class FocusFrame(Gtk.Overlay):
def __init__(self):
Gtk.Overlay.__init__(self)
self.frame=Gtk.Frame()
self.frame.set_no_show_all(True)
self.style_context=self.frame.get_style_context()
self.provider=Gtk.CssProvider()
css=b"""* {border-color: @theme_selected_bg_color; border-width: 2px;}"""
self.provider.load_from_data(css)
self.style_context.add_provider(self.provider, 800)
2020-03-22 16:25:04 +03:00
2020-07-04 14:16:17 +03:00
self.add_overlay(self.frame)
self.set_overlay_pass_through(self.frame, True)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +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-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def on_focus_in_event(self, *args):
self.frame.show()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def on_focus_out_event(self, *args):
self.frame.hide()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class SongPopover(Gtk.Popover):
def __init__(self, song, relative, x, y):
Gtk.Popover.__init__(self)
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-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
# Store
# (tag, display-value, tooltip)
self.store=Gtk.ListStore(str, str, str)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
# TreeView
self.treeview=Gtk.TreeView(model=self.store)
self.treeview.set_can_focus(False)
self.treeview.set_search_column(-1)
self.treeview.set_tooltip_column(2)
self.treeview.set_headers_visible(False)
sel=self.treeview.get_selection()
sel.set_mode(Gtk.SelectionMode.NONE)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
frame=Gtk.Frame()
frame.add(self.treeview)
frame.set_property("border-width", 3)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +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-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
self.column_tag=Gtk.TreeViewColumn(_("MPD-Tag"), renderer_text_ralign, text=0)
self.column_tag.set_property("resizable", False)
self.treeview.append_column(self.column_tag)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
self.column_value=Gtk.TreeViewColumn(_("Value"), renderer_text, text=1)
self.column_value.set_property("resizable", False)
self.treeview.append_column(self.column_value)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
# packing
self.add(frame)
song=ClientHelper.song_to_str_dict(song)
for tag, value in song.items():
tooltip=value.replace("&", "&amp;")
if tag == "time":
self.store.append([tag+":", str(datetime.timedelta(seconds=int(value))), tooltip])
elif tag == "last-modified":
time=datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
self.store.append([tag+":", time.strftime('%a %d %B %Y, %H:%M UTC'), tooltip])
2020-03-21 00:09:13 +03:00
else:
2020-07-04 14:16:17 +03:00
self.store.append([tag+":", value, tooltip])
frame.show_all()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class Cover(object):
regex=re.compile(r'^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$', flags=re.IGNORECASE)
def __init__(self, lib_path, song_file):
self.lib_path=lib_path or ""
self.path=None
if not song_file == None:
head, tail=os.path.split(song_file)
song_dir=os.path.join(self.lib_path, head)
if os.path.exists(song_dir):
for f in os.listdir(song_dir):
if self.regex.match(f):
self.path=os.path.join(song_dir, f)
break
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def get_pixbuf(self, size):
if self.path == None:
2020-07-17 17:42:47 +03:00
self.path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename() # fallback cover
2020-07-04 14:16:17 +03:00
return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size)
2020-03-21 00:09:13 +03:00
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():
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:
return_song[tag]=(', '.join(value))
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):
base_song={"title": _("Unknown Title"), "track": "0", "disc": "", "artist": _("Unknown Artist"), "album": _("Unknown Album"), "duration": "0.0", "date": "", "genre": ""}
base_song.update(song)
base_song["human_duration"]=str(datetime.timedelta(seconds=int(float(base_song["duration"])))).lstrip("0").lstrip(":")
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-03-21 00:09:13 +03:00
try:
2020-07-04 14:16:17 +03:00
dura=float(song["duration"])
2020-03-21 00:09:13 +03:00
except:
2020-07-04 14:16:17 +03:00
dura=0.0
length=length+dura
return str(datetime.timedelta(seconds=int(length))).lstrip("0").lstrip(":")
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class MpdEventEmitter(GObject.Object):
__gsignals__={
'update': (GObject.SignalFlags.RUN_FIRST, None, ()),
'disconnected': (GObject.SignalFlags.RUN_FIRST, None, ()),
'reconnected': (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, (float,int,int,)),
'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
# gsignals
2020-07-04 14:16:17 +03:00
def do_update(self):
pass
2020-03-21 00:09:13 +03:00
def do_disconnected(self):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_reconnected(self):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_current_file_changed(self):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_state(self, state):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_elapsed_changed(self, elapsed, duration):
2020-03-21 00:09:13 +03:00
pass
def do_volume_changed(self, volume):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_playlist_changed(self, version):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_audio(self, sampelrate, bits, channels):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
def do_bitrate(self, rate):
2020-07-04 14:16:17 +03:00
pass
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
class Client(MPDClient):
def __init__(self, settings):
MPDClient.__init__(self)
self.settings=settings
self.settings.connect("changed::active-profile", self.on_settings_changed)
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self.settings=settings
self.emitter=MpdEventEmitter()
2020-07-12 16:20:29 +03:00
self.last_status={}
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
self.current_file=None
2020-03-21 00:09:13 +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):
if self.disconnected_loop():
self.disconnected_timeout_id=GLib.timeout_add(1000, self.disconnected_loop)
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-07-04 14:16:17 +03:00
def on_settings_changed(self, *args):
self.disconnect()
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def files_to_playlist(self, files, append, force=False):
if append:
for f in files:
self.add(f)
else:
if self.settings.get_boolean("force-mode") or force or self.status()["state"] == "stop":
if not files == []:
self.clear()
for f in files:
self.add(f)
self.play()
else:
status=self.status()
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:
if not f == current_song_file:
self.add(f)
else:
self.move(0, (len(self.playlistinfo())-1))
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def album_to_playlist(self, album, artist, year, append, force=False):
songs=self.find("album", album, "date", year, self.settings.get_artist_type(), artist)
self.files_to_playlist([song['file'] for song in songs], append, force)
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
if "group" in args:
raise ValueError("'group' is not supported")
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]
meta_extra=self.readcomments(uri) # contains comment tag
meta_base.update(meta_extra)
return meta_base
2020-07-04 14:16:17 +03:00
def loop(self, *args):
try:
status=self.status()
2020-07-12 16:20:29 +03:00
diff=set(status.items())-set(self.last_status.items())
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":
samplerate, bits, channels=val.split(':')
self.emitter.emit("audio", float(samplerate), int(bits), int(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", 0)
if "updating_db" in diff:
self.emitter.emit("update")
2020-07-12 16:20:29 +03:00
self.last_status=status
except MPDBase.ConnectionError:
2020-07-12 16:20:29 +03:00
self.last_status={}
2020-07-04 14:16:17 +03:00
self.emitter.emit("disconnected")
if self.disconnected_loop():
self.disconnected_timeout_id=GLib.timeout_add(1000, self.disconnected_loop)
return False
return True
2020-03-21 00:09:13 +03:00
2020-07-04 14:16:17 +03:00
def disconnected_loop(self, *args):
self.current_file=None
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:
print("connect failed")
return True
# connect successful
self.main_timeout_id=GLib.timeout_add(100, self.loop)
self.emitter.emit("reconnected")
return False
2020-03-21 00:09:13 +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-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-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-01-28 20:39:18 +03:00
2020-07-04 13:35:39 +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))
2020-07-04 13:35:39 +03:00
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))
2020-07-04 13:35:39 +03:00
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-07-04 14:16:17 +03:00
###########
# browser #
###########
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
class SearchWindow(Gtk.Box):
def __init__(self, client):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self.client=client
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
# tag switcher
self.tags=Gtk.ComboBoxText()
# search entry
self.search_entry=Gtk.SearchEntry()
# label
self.label=Gtk.Label()
self.label.set_xalign(1)
self.label.set_margin_end(6)
# store
# (track, title, artist, album, duration, file)
self.store=Gtk.ListStore(int, str, str, str, str, str)
# songs view
self.songs_view=SongsView(self.client, self.store, 5)
# columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
self.column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
self.column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_track.set_property("resizable", False)
self.songs_view.append_column(self.column_track)
self.column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, text=1)
self.column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_title.set_property("resizable", False)
self.column_title.set_property("expand", True)
self.songs_view.append_column(self.column_title)
self.column_artist=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=2)
self.column_artist.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_artist.set_property("resizable", False)
self.column_artist.set_property("expand", True)
self.songs_view.append_column(self.column_artist)
self.column_album=Gtk.TreeViewColumn(_("Album"), renderer_text, text=3)
self.column_album.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_album.set_property("resizable", False)
self.column_album.set_property("expand", True)
self.songs_view.append_column(self.column_album)
self.column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=4)
self.column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_time.set_property("resizable", False)
self.songs_view.append_column(self.column_time)
self.column_track.set_sort_column_id(0)
self.column_title.set_sort_column_id(1)
self.column_artist.set_sort_column_id(2)
self.column_album.set_sort_column_id(3)
self.column_time.set_sort_column_id(4)
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self.songs_view)
# buttons
self.add_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_ADD), label=_("Add"))
self.add_button.set_sensitive(False)
self.add_button.set_relief(Gtk.ReliefStyle.NONE)
self.play_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_MEDIA_PLAY), label=_("Play"))
self.play_button.set_sensitive(False)
self.play_button.set_relief(Gtk.ReliefStyle.NONE)
self.open_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_OPEN), label=_("Open"))
self.open_button.set_sensitive(False)
self.open_button.set_relief(Gtk.ReliefStyle.NONE)
# connect
self.search_entry.connect("search-changed", self.on_search_changed)
self.tags.connect("changed", self.on_search_changed)
self.add_button.connect("clicked", self.on_add_clicked)
self.play_button.connect("clicked", self.on_play_clicked)
self.open_button.connect("clicked", self.on_open_clicked)
self.client.emitter.connect("reconnected", self.on_reconnected)
# packing
vbox=Gtk.Box(spacing=6)
vbox.set_property("border-width", 6)
vbox.pack_start(self.search_entry, True, True, 0)
vbox.pack_end(self.tags, False, False, 0)
frame=FocusFrame()
frame.set_widget(self.songs_view)
frame.add(scroll)
ButtonBox=Gtk.ButtonBox(spacing=1)
ButtonBox.set_property("border-width", 1)
ButtonBox.pack_start(self.add_button, True, True, 0)
ButtonBox.pack_start(self.play_button, True, True, 0)
ButtonBox.pack_start(self.open_button, True, True, 0)
hbox=Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
hbox.pack_start(ButtonBox, 0, False, False)
hbox.pack_end(self.label, 0, False, False)
self.pack_start(vbox, False, False, 0)
self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
self.pack_start(frame, True, True, 0)
self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
self.pack_start(hbox, False, False, 0)
def start(self):
self.search_entry.grab_focus()
def started(self):
return self.search_entry.has_focus()
def clear(self, *args):
self.songs_view.clear()
self.search_entry.set_text("")
self.tags.remove_all()
def on_reconnected(self, *args):
self.tags.append_text("any")
for tag in self.client.wrapped_call("tagtypes"):
2020-07-04 14:16:17 +03:00
if not tag.startswith("MUSICBRAINZ"):
self.tags.append_text(tag)
self.tags.set_active(0)
def on_search_changed(self, widget):
self.songs_view.clear()
self.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())
2020-07-04 14:16:17 +03:00
for s in songs:
song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s))
self.store.append([int(song["track"]), song["title"], song["artist"], song["album"], song["human_duration"], song["file"]])
self.label.set_text(_("hits: %i") % (self.songs_view.count()))
if self.songs_view.count() == 0:
self.add_button.set_sensitive(False)
self.play_button.set_sensitive(False)
self.open_button.set_sensitive(False)
else:
self.add_button.set_sensitive(True)
self.play_button.set_sensitive(True)
self.open_button.set_sensitive(True)
def on_add_clicked(self, *args):
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", self.songs_view.get_files(), True)
2020-07-04 14:16:17 +03:00
def on_play_clicked(self, *args):
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", self.songs_view.get_files(), False, True)
2020-07-04 14:16:17 +03:00
def on_open_clicked(self, *args):
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", self.songs_view.get_files(), False)
2020-07-04 14:16:17 +03:00
class LyricsWindow(Gtk.Overlay):
def __init__(self, client, settings):
Gtk.Overlay.__init__(self)
# adding vars
self.settings=settings
self.client=client
# widgets
self.text_view=Gtk.TextView()
self.text_view.set_editable(False)
self.text_view.set_left_margin(5)
self.text_view.set_bottom_margin(5)
self.text_view.set_cursor_visible(False)
self.text_view.set_wrap_mode(Gtk.WrapMode.WORD)
self.text_view.set_justification(Gtk.Justification.CENTER)
self.text_buffer=self.text_view.get_buffer()
# scroll
self.scroll=Gtk.ScrolledWindow()
self.scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self.scroll.add(self.text_view)
# frame
frame=FocusFrame()
frame.set_widget(self.text_view)
style_context=frame.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {border: 0px; background-color: @theme_base_color; opacity: 0.9;}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
2020-07-04 14:16:17 +03:00
# close button
close_button=Gtk.ToggleButton(image=Gtk.Image.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON))
close_button.set_margin_top(6)
close_button.set_margin_end(6)
style_context=close_button.get_style_context()
style_context.add_class("circular")
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
close_button.set_halign(2)
close_button.set_valign(1)
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
# connect
self.song_changed=self.client.emitter.connect("current_song_changed", self.refresh)
2020-07-04 14:16:17 +03:00
self.connect("destroy", self.remove_handlers)
close_button.connect("clicked", self.on_close_button_clicked)
2020-05-26 16:04:16 +03:00
2020-07-04 13:35:39 +03:00
# packing
2020-07-04 14:16:17 +03:00
frame.add(self.scroll)
self.add(frame)
2020-07-04 14:16:17 +03:00
self.add_overlay(close_button)
2020-05-26 16:04:16 +03:00
2020-07-04 14:16:17 +03:00
self.show_all()
self.refresh()
GLib.idle_add(self.text_view.grab_focus) # focus textview
def remove_handlers(self, *args):
self.client.emitter.disconnect(self.song_changed)
2020-07-04 14:16:17 +03:00
def display_lyrics(self, current_song):
GLib.idle_add(self.text_buffer.set_text, _("searching..."), -1)
try:
text=self.getLyrics(current_song["artist"], current_song["title"])
except:
text=_("lyrics not found")
GLib.idle_add(self.text_buffer.set_text, text, -1)
def refresh(self, *args):
update_thread=threading.Thread(target=self.display_lyrics, kwargs={"current_song": ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong"))}, daemon=True)
2020-07-04 14:16:17 +03:00
update_thread.start()
def getLyrics(self, singer, song): # partially copied from PyLyrics 1.1.0
# Replace spaces with _
singer=singer.replace(' ', '_')
song=song.replace(' ', '_')
r=requests.get('http://lyrics.wikia.com/{0}:{1}'.format(singer,song))
s=BeautifulSoup(r.text)
# Get main lyrics holder
lyrics=s.find("div",{'class':'lyricbox'})
if lyrics is None:
raise ValueError("Song or Singer does not exist or the API does not have Lyrics")
return None
# Remove Scripts
[s.extract() for s in lyrics('script')]
# Remove Comments
comments=lyrics.findAll(text=lambda text:isinstance(text, Comment))
[comment.extract() for comment in comments]
# Remove span tag (Needed for instrumantal)
if not lyrics.span == None:
lyrics.span.extract()
# Remove unecessary tags
for tag in ['div','i','b','a']:
for match in lyrics.findAll(tag):
match.replaceWithChildren()
# Get output as a string and remove non unicode characters and replace <br> with newlines
output=str(lyrics).encode('utf-8', errors='replace')[22:-6:].decode("utf-8").replace('\n','').replace('<br/>','\n')
try:
return output
except:
return output.encode('utf-8')
def on_close_button_clicked(self, *args):
self.destroy()
2020-05-26 16:04:16 +03:00
class SongsView(Gtk.TreeView):
def __init__(self, client, store, file_column_id):
Gtk.TreeView.__init__(self)
self.set_model(store)
self.set_search_column(-1)
self.columns_autosize()
2020-01-18 00:13:58 +03:00
2020-07-04 13:35:39 +03:00
# add vars
2020-01-18 00:13:58 +03:00
self.client=client
self.store=store
self.file_column_id=file_column_id
2020-01-18 00:13:58 +03:00
2020-07-04 13:35:39 +03:00
# selection
self.selection=self.get_selection()
2020-01-18 00:13:58 +03:00
self.selection.set_mode(Gtk.SelectionMode.SINGLE)
2020-07-04 13:35:39 +03:00
# 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)
2020-01-18 00:13:58 +03:00
def on_row_activated(self, widget, path, view_column):
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", [self.store[path][self.file_column_id]], False, True)
2020-01-18 00:13:58 +03:00
2020-03-30 11:28:40 +03:00
def on_button_press_event(self, widget, event):
2020-03-30 21:44:28 +03:00
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
2020-03-30 11:28:40 +03:00
try:
2020-04-09 01:26:21 +03:00
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", [self.store[path][self.file_column_id]], False)
except:
pass
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
2020-04-09 01:26:21 +03:00
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", [self.store[path][self.file_column_id]], True)
2020-03-30 11:28:40 +03:00
except:
pass
2020-05-26 16:04:16 +03:00
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))
2020-05-26 16:04:16 +03:00
pop.popup()
pop.show_all()
2020-05-26 16:04:16 +03:00
except:
pass
2020-01-18 00:13:58 +03:00
def on_key_press_event(self, widget, event):
self.handler_block(self.key_press_event)
2020-07-04 13:35:39 +03:00
if event.keyval == 112: # p
treeview, treeiter=self.selection.get_selected()
if not treeiter == None:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", [self.store.get_value(treeiter, self.file_column_id)], False)
2020-07-04 13:35:39 +03:00
elif event.keyval == 97: # a
treeview, treeiter=self.selection.get_selected()
if not treeiter == None:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("files_to_playlist", [self.store.get_value(treeiter, self.file_column_id)], True)
2020-07-04 13:35:39 +03:00
elif event.keyval == 65383: # menu key
2020-07-01 19:04:01 +03:00
treeview, treeiter=self.selection.get_selected()
if not treeiter == 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))
2020-07-01 19:04:01 +03:00
pop.popup()
pop.show_all()
self.handler_unblock(self.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
class AlbumDialog(Gtk.Dialog):
def __init__(self, parent, client, settings, album, artist, year):
2020-04-02 21:40:43 +03:00
Gtk.Dialog.__init__(self, transient_for=parent)
self.add_buttons(Gtk.STOCK_ADD, Gtk.ResponseType.ACCEPT, Gtk.STOCK_MEDIA_PLAY, Gtk.ResponseType.YES, Gtk.STOCK_OPEN, Gtk.ResponseType.OK, Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
2020-05-26 20:49:55 +03:00
2020-07-04 13:35:39 +03:00
# metadata
2020-06-14 23:34:01 +03:00
self.album=album
self.artist=artist
self.year=year
2020-07-04 13:35:39 +03:00
# adding vars
2020-06-14 23:34:01 +03:00
self.client=client
self.settings=settings
songs=self.client.wrapped_call("find", "album", self.album, "date", self.year, self.settings.get_artist_type(), self.artist)
2020-06-14 23:34:01 +03:00
2020-07-04 13:35:39 +03:00
# determine size
2020-05-26 20:49:55 +03:00
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)
2020-07-04 13:35:39 +03:00
# title
2020-06-14 23:34:01 +03:00
album_duration=ClientHelper.calc_display_length(songs)
2020-04-02 21:40:43 +03:00
if year == "":
2020-06-14 23:34:01 +03:00
self.set_title(artist+" - "+album+" ("+album_duration+")")
2020-04-02 21:40:43 +03:00
else:
2020-06-14 23:34:01 +03:00
self.set_title(artist+" - "+album+" ("+year+") ("+album_duration+")")
2020-07-04 13:35:39 +03:00
# store
# (track, title (artist), duration, file)
self.store=Gtk.ListStore(int, str, str, str)
2020-07-04 13:35:39 +03:00
# songs view
self.songs_view=SongsView(self.client, self.store, 3)
for s in songs:
song=ClientHelper.extend_song_for_display(s)
if type(song["title"]) == list: # could be impossible
title=(', '.join(song["title"]))
else:
title=song["title"]
if type(song["artist"]) == list:
2020-06-04 21:00:51 +03:00
try:
song["artist"].remove(self.artist)
except:
pass
artist=(', '.join(song["artist"]))
else:
artist=song["artist"]
if artist != self.artist:
title_artist="<b>"+title+"</b> - "+artist
else:
title_artist="<b>"+title+"</b>"
title_artist=title_artist.replace("&", "&amp;")
self.store.append([int(song["track"]), title_artist, song["human_duration"], song["file"]])
2020-07-04 13:35:39 +03:00
# columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
self.column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
self.column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_track.set_property("resizable", False)
self.songs_view.append_column(self.column_track)
self.column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1)
self.column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_title.set_property("resizable", False)
self.column_title.set_property("expand", True)
self.songs_view.append_column(self.column_title)
self.column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=2)
self.column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self.column_time.set_property("resizable", False)
self.songs_view.append_column(self.column_time)
2020-07-04 13:35:39 +03:00
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self.songs_view)
2020-07-04 13:35:39 +03:00
# packing
self.vbox.pack_start(scroll, True, True, 0) # vbox default widget of dialogs
self.vbox.set_spacing(3)
self.show_all()
2020-01-18 00:13:58 +03:00
2020-03-30 23:20:14 +03:00
def open(self):
2020-04-09 01:26:21 +03:00
response=self.run()
2020-03-30 23:20:14 +03:00
if response == Gtk.ResponseType.OK:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", self.album, self.artist, self.year, False)
2020-03-30 23:20:14 +03:00
elif response == Gtk.ResponseType.ACCEPT:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", self.album, self.artist, self.year, True)
2020-03-30 23:20:14 +03:00
elif response == Gtk.ResponseType.YES:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", self.album, self.artist, self.year, False, True)
2020-03-30 23:20:14 +03:00
2020-03-27 19:42:11 +03:00
class GenreSelect(Gtk.ComboBoxText):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings):
2020-03-27 19:42:11 +03:00
Gtk.ComboBoxText.__init__(self)
2020-02-27 22:05:48 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-02-27 22:05:48 +03:00
self.client=client
self.settings=settings
2020-07-04 13:35:39 +03:00
# connect
2020-03-27 19:42:11 +03:00
self.changed=self.connect("changed", self.on_changed)
self.client.emitter.connect("reconnected", self.refresh)
2020-03-30 12:54:04 +03:00
self.update_signal=self.client.emitter.connect("update", self.refresh)
2020-02-27 22:05:48 +03:00
2020-02-29 01:23:53 +03:00
def deactivate(self):
2020-03-27 19:42:11 +03:00
self.set_active(0)
2020-02-29 01:23:53 +03:00
2020-02-27 22:05:48 +03:00
def refresh(self, *args):
2020-03-27 19:42:11 +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"):
2020-03-27 19:42:11 +03:00
self.append_text(genre)
self.set_active(0)
self.handler_unblock(self.changed)
2020-02-27 22:05:48 +03:00
def clear(self, *args):
2020-03-27 19:42:11 +03:00
self.handler_block(self.changed)
self.remove_all()
self.handler_unblock(self.changed)
2020-02-27 22:05:48 +03:00
def get_value(self):
2020-03-27 19:42:11 +03:00
if self.get_active() == 0:
2020-02-27 22:05:48 +03:00
return None
2020-02-28 13:47:57 +03:00
else:
2020-03-27 19:42:11 +03:00
return self.get_active_text()
2020-02-27 22:05:48 +03:00
@GObject.Signal
def genre_changed(self):
pass
2020-03-27 19:42:11 +03:00
def on_changed(self, *args):
self.emit("genre_changed")
2020-02-27 22:05:48 +03:00
2020-03-31 18:36:41 +03:00
class ArtistView(FocusFrame):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings, genre_select):
2020-03-31 18:36:41 +03:00
FocusFrame.__init__(self)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-01-11 13:25:15 +03:00
self.client=client
2020-02-26 01:37:27 +03:00
self.settings=settings
2020-02-27 22:05:48 +03:00
self.genre_select=genre_select
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# artistStore
# (name, weight, initial-letter, weight-initials)
2020-04-09 01:26:21 +03:00
self.store=Gtk.ListStore(str, Pango.Weight, str, Pango.Weight)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# TreeView
2020-04-09 01:26:21 +03:00
self.treeview=Gtk.TreeView(model=self.store)
self.treeview.set_search_column(0)
2020-02-26 01:37:27 +03:00
self.treeview.columns_autosize()
self.treeview.set_property("activate-on-single-click", True)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# artistSelection
2020-04-09 01:26:21 +03:00
self.selection=self.treeview.get_selection()
self.selection.set_mode(Gtk.SelectionMode.SINGLE)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# Columns
2020-04-09 01:26:21 +03:00
renderer_text_malign=Gtk.CellRendererText(xalign=0.5)
self.column_initials=Gtk.TreeViewColumn("", renderer_text_malign, text=2, weight=3)
2020-03-12 19:09:24 +03:00
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)
2020-05-25 22:32:50 +03:00
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
2020-04-09 01:26:21 +03:00
self.column_name=Gtk.TreeViewColumn("", renderer_text, text=0, weight=1)
2020-01-11 13:25:15 +03:00
self.column_name.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
2020-02-26 01:37:27 +03:00
self.column_name.set_property("resizable", False)
2020-01-11 13:25:15 +03:00
self.treeview.append_column(self.column_name)
2020-07-04 13:35:39 +03:00
# scroll
2020-03-31 18:36:41 +03:00
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
self.treeview.connect("row-activated", self.on_row_activated)
self.settings.connect("changed::use-album-artist", self.refresh)
2020-03-12 19:09:24 +03:00
self.settings.connect("changed::show-initials", self.on_show_initials_settings_changed)
self.client.emitter.connect("reconnected", self.refresh)
2020-03-30 12:54:04 +03:00
self.client.emitter.connect("update", self.refresh)
self.genre_select.connect("genre_changed", self.refresh)
2020-02-02 20:08:59 +03:00
2020-03-31 18:36:41 +03:00
self.set_widget(self.treeview)
self.add(scroll)
2020-01-11 13:25:15 +03:00
@GObject.Signal
def artists_changed(self):
pass
2020-02-01 16:12:56 +03:00
def clear(self):
self.store.clear()
def refresh(self, *args):
self.selection.set_mode(Gtk.SelectionMode.NONE)
self.clear()
2020-02-28 16:27:58 +03:00
if self.settings.get_artist_type() == "albumartist":
self.column_name.set_title(_("Album Artist"))
else:
self.column_name.set_title(_("Artist"))
self.store.append([_("all artists"), Pango.Weight.BOOK, "", Pango.Weight.BOOK])
2020-02-27 22:05:48 +03:00
genre=self.genre_select.get_value()
if genre == None:
artists=self.client.wrapped_call("comp_list", self.settings.get_artist_type())
2020-02-27 22:05:48 +03:00
else:
artists=self.client.wrapped_call("comp_list", self.settings.get_artist_type(), "genre", genre)
2020-03-04 01:54:20 +03:00
current_char=""
2020-02-27 22:05:48 +03:00
for artist in artists:
2020-03-04 01:54:20 +03:00
try:
if current_char != artist[0]:
self.store.append([artist, Pango.Weight.BOOK, artist[0], Pango.Weight.BOLD])
2020-03-04 01:54:20 +03:00
current_char=artist[0]
2020-03-12 19:09:24 +03:00
else:
self.store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK])
2020-03-04 01:54:20 +03:00
except:
self.store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK])
self.selection.set_mode(Gtk.SelectionMode.SINGLE)
2020-01-11 13:25:15 +03:00
2020-02-01 16:36:57 +03:00
def get_selected_artists(self):
artists=[]
if self.store[Gtk.TreePath(0)][1] == Pango.Weight.BOLD:
for row in self.store:
artists.append(row[0])
return artists[1:]
else:
for row in self.store:
if row[1] == Pango.Weight.BOLD:
artists.append(row[0])
break
return artists
2020-02-01 16:36:57 +03:00
2020-05-19 18:23:15 +03:00
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 on_row_activated(self, widget, path, view_column):
2020-07-04 13:35:39 +03:00
for row in self.store: # reset bold text
row[1]=Pango.Weight.BOOK
self.store[path][1]=Pango.Weight.BOLD
self.emit("artists_changed")
2020-03-12 19:09:24 +03:00
def on_show_initials_settings_changed(self, *args):
self.column_initials.set_visible(self.settings.get_boolean("show-initials"))
2020-03-30 23:20:14 +03:00
class AlbumIconView(Gtk.IconView):
2020-02-27 22:05:48 +03:00
def __init__(self, client, settings, genre_select, window):
Gtk.IconView.__init__(self)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-01-11 13:25:15 +03:00
self.settings=settings
self.client=client
2020-02-27 22:05:48 +03:00
self.genre_select=genre_select
self.window=window
self.stop_flag=True
self.button_event=(None, None)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# cover, display_label, display_label_artist, tooltip(titles), album, year, artist
2020-05-27 22:08:01 +03:00
self.store=Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str, str, str, str)
2020-02-28 18:03:45 +03:00
self.sort_settings()
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# iconview
self.set_model(self.store)
self.set_pixbuf_column(0)
2020-05-27 22:08:01 +03:00
self.set_markup_column(1)
self.set_item_width(0)
self.tooltip_settings()
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-03-30 23:20:14 +03:00
self.connect("item-activated", self.on_item_activated)
self.connect("button-release-event", self.on_button_release_event)
2020-03-30 23:20:14 +03:00
self.connect("button-press-event", self.on_button_press_event)
self.key_press_event=self.connect("key-press-event", self.on_key_press_event)
self.settings.connect("changed::show-album-view-tooltips", self.tooltip_settings)
2020-02-28 18:03:45 +03:00
self.settings.connect("changed::sort-albums-by-year", self.sort_settings)
2020-01-11 13:25:15 +03:00
@GObject.Signal
2020-03-26 19:01:15 +03:00
def done(self):
2020-03-29 19:06:37 +03:00
self.stop_flag=True
pass
def tooltip_settings(self, *args):
2020-01-11 13:25:15 +03:00
if self.settings.get_boolean("show-album-view-tooltips"):
2020-05-27 22:08:01 +03:00
self.set_tooltip_column(3)
2020-01-11 13:25:15 +03:00
else:
self.set_tooltip_column(-1)
2020-02-28 18:03:45 +03:00
def sort_settings(self, *args):
if self.settings.get_boolean("sort-albums-by-year"):
2020-05-27 22:08:01 +03:00
self.store.set_sort_column_id(5, Gtk.SortType.ASCENDING)
2020-02-28 18:03:45 +03:00
else:
self.store.set_sort_column_id(1, Gtk.SortType.ASCENDING)
2020-03-29 19:06:37 +03:00
return False
2020-02-28 18:03:45 +03:00
2020-07-04 13:48:35 +03:00
def add_row(self, row):
2020-03-26 19:01:15 +03:00
self.store.append(row)
return False
def populate(self, artists):
2020-03-29 19:06:37 +03:00
self.stop_flag=False
2020-07-04 13:35:39 +03:00
# prepare albmus list
2020-03-26 19:01:15 +03:00
self.store.clear()
2020-05-27 22:08:01 +03:00
if len(artists) > 1:
self.set_markup_column(2)
else:
self.set_markup_column(1)
albums=[]
2020-02-27 22:05:48 +03:00
genre=self.genre_select.get_value()
2020-03-26 19:01:15 +03:00
artist_type=self.settings.get_artist_type()
for artist in artists:
2020-07-04 13:35:39 +03:00
try: # client cloud meanwhile disconnect
2020-03-30 13:28:30 +03:00
if not self.stop_flag:
if genre == None:
album_candidates=self.client.wrapped_call("comp_list", "album", artist_type, artist)
2020-03-30 13:28:30 +03:00
else:
album_candidates=self.client.wrapped_call("comp_list", "album", artist_type, artist, "genre", genre)
2020-03-30 13:28:30 +03:00
for album in album_candidates:
years=self.client.wrapped_call("comp_list", "date", "album", album, artist_type, artist)
2020-03-30 13:28:30 +03:00
for year in years:
songs=self.client.wrapped_call("find", "album", album, "date", year, artist_type, artist)
2020-03-30 13:28:30 +03:00
albums.append({"artist": artist, "album": album, "year": year, "songs": songs})
while Gtk.events_pending():
Gtk.main_iteration_do(True)
2020-03-29 19:06:37 +03:00
else:
2020-03-30 13:28:30 +03:00
GLib.idle_add(self.emit, "done")
return
2020-07-15 23:39:26 +03:00
except MPDBase.ConnectionError:
2020-03-29 19:06:37 +03:00
GLib.idle_add(self.emit, "done")
return
2020-07-04 13:35:39 +03:00
# display albums
if self.settings.get_boolean("sort-albums-by-year"):
2020-04-09 01:26:21 +03:00
albums=sorted(albums, key=lambda k: k['year'])
else:
2020-04-09 01:26:21 +03:00
albums=sorted(albums, key=lambda k: k['album'])
2020-03-29 19:06:37 +03:00
music_lib=self.settings.get_value("paths")[self.settings.get_int("active-profile")]
size=self.settings.get_int("album-cover")
2020-04-24 23:20:17 +03:00
for i, album in enumerate(albums):
2020-03-29 19:06:37 +03:00
if not self.stop_flag:
2020-07-04 13:48:35 +03:00
cover=Cover(lib_path=music_lib, song_file=album["songs"][0]["file"]).get_pixbuf(size)
2020-07-04 13:35:39 +03:00
# tooltip
length_human_readable=ClientHelper.calc_display_length(album["songs"])
2020-05-12 18:56:59 +03:00
try:
discs=int(album["songs"][-1]["disc"])
except:
discs=1
if discs > 1:
tooltip=(_("%(total_tracks)i titles on %(discs)i discs (%(total_length)s)") % {"total_tracks": len(album["songs"]), "discs": discs, "total_length": length_human_readable})
else:
tooltip=(_("%(total_tracks)i titles (%(total_length)s)") % {"total_tracks": len(album["songs"]), "total_length": length_human_readable})
2020-05-27 22:08:01 +03:00
display_label="<b>"+album["album"]+"</b>"
if album["year"] != "":
display_label=display_label+" ("+album["year"]+")"
display_label_artist=display_label+"\n"+album["artist"]
display_label=display_label.replace("&", "&amp;")
display_label_artist=display_label_artist.replace("&", "&amp;")
2020-07-04 13:48:35 +03:00
GLib.idle_add(self.add_row, [cover, display_label, display_label_artist, tooltip, album["album"], album["year"], album["artist"]])
2020-04-24 23:20:17 +03:00
if i%16 == 0:
2020-03-29 19:06:37 +03:00
while Gtk.events_pending():
Gtk.main_iteration_do(True)
else:
break
GLib.idle_add(self.emit, "done")
2020-04-07 02:37:53 +03:00
def scroll_to_selected_album(self):
song=ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong"))
self.unselect_all()
row_num=len(self.store)
for i in range(0, row_num):
path=Gtk.TreePath(i)
2020-04-09 01:26:21 +03:00
treeiter=self.store.get_iter(path)
2020-05-27 22:08:01 +03:00
if self.store.get_value(treeiter, 4) == song["album"]:
2020-02-25 15:59:43 +03:00
self.set_cursor(path, None, False)
self.select_path(path)
self.scroll_to_path(path, True, 0, 0)
break
2020-03-30 23:20:14 +03:00
def path_to_playlist(self, path, add, force=False):
2020-05-27 22:08:01 +03:00
album=self.store[path][4]
year=self.store[path][5]
artist=self.store[path][6]
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", album, artist, year, add, force)
2020-03-30 23:20:14 +03:00
def open_album_dialog(self, path):
if self.client.connected():
2020-05-27 22:08:01 +03:00
album=self.store[path][4]
year=self.store[path][5]
artist=self.store[path][6]
2020-04-09 01:26:21 +03:00
album_dialog=AlbumDialog(self.window, self.client, self.settings, album, artist, year)
2020-03-30 23:20:14 +03:00
album_dialog.open()
album_dialog.destroy()
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):
2020-04-09 01:26:21 +03:00
path=widget.get_path_at_pos(int(event.x), int(event.y))
if not path == None:
if self.button_event == (event.button, path):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_RELEASE:
self.path_to_playlist(path, False)
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_RELEASE:
self.path_to_playlist(path, True)
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_RELEASE:
self.open_album_dialog(path)
2020-03-30 23:20:14 +03:00
def on_key_press_event(self, widget, event):
self.handler_block(self.key_press_event)
2020-07-04 13:35:39 +03:00
if event.keyval == 112: # p
2020-03-30 23:20:14 +03:00
paths=self.get_selected_items()
if not len(paths) == 0:
self.path_to_playlist(paths[0], False)
2020-07-04 13:35:39 +03:00
elif event.keyval == 97: # a
2020-03-30 23:20:14 +03:00
paths=self.get_selected_items()
if not len(paths) == 0:
self.path_to_playlist(paths[0], True)
2020-07-04 13:35:39 +03:00
elif event.keyval == 65383: # menu key
2020-03-30 23:20:14 +03:00
paths=self.get_selected_items()
if not len(paths) == 0:
self.open_album_dialog(paths[0])
self.handler_unblock(self.key_press_event)
def on_item_activated(self, widget, path):
treeiter=self.store.get_iter(path)
2020-05-27 22:08:01 +03:00
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)
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", selected_album, selected_artist, selected_album_year, False, True)
2020-03-31 18:36:41 +03:00
class AlbumView(FocusFrame):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings, genre_select, window):
2020-03-31 18:36:41 +03:00
FocusFrame.__init__(self)
2020-07-04 13:35:39 +03:00
# adding vars
self.settings=settings
self.client=client
2020-02-27 22:05:48 +03:00
self.genre_select=genre_select
self.window=window
self.artists=[]
2020-03-29 19:06:37 +03:00
self.done=True
2020-03-26 19:01:15 +03:00
self.pending=[]
2020-07-04 13:35:39 +03:00
# iconview
2020-02-27 22:05:48 +03:00
self.iconview=AlbumIconView(self.client, self.settings, self.genre_select, self.window)
2020-07-04 13:35:39 +03:00
# scroll
2020-03-31 18:36:41 +03:00
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self.iconview)
2020-07-04 13:35:39 +03:00
# connect
2020-03-10 15:50:36 +03:00
self.settings.connect("changed::album-cover", self.on_settings_changed)
2020-03-26 19:01:15 +03:00
self.iconview.connect("done", self.on_done)
2020-03-30 12:54:04 +03:00
self.client.emitter.connect("update", self.clear)
self.genre_select.connect("genre_changed", self.clear)
self.settings.connect("changed::use-album-artist", self.clear)
2020-03-10 15:50:36 +03:00
2020-03-31 18:36:41 +03:00
self.set_widget(self.iconview)
self.add(scroll)
2020-03-27 19:42:11 +03:00
def clear(self, *args):
2020-03-30 13:28:30 +03:00
if self.done:
self.iconview.store.clear()
elif not self.clear in self.pending:
self.iconview.stop_flag=True
self.pending.append(self.clear)
2020-02-01 16:12:56 +03:00
def refresh(self, artists):
2020-03-26 19:01:15 +03:00
self.artists=artists
2020-03-29 19:06:37 +03:00
if self.done:
self.done=False
self.populate()
elif not self.populate in self.pending:
self.iconview.stop_flag=True
self.pending.append(self.populate)
def populate(self):
self.iconview.populate(self.artists)
2020-02-03 01:10:33 +03:00
def scroll_to_selected_album(self):
2020-03-26 19:01:15 +03:00
if self.done:
self.iconview.scroll_to_selected_album()
elif not self.scroll_to_selected_album in self.pending:
self.pending.append(self.scroll_to_selected_album)
def on_done(self, *args):
self.done=True
pending=self.pending
self.pending=[]
for p in pending:
try:
p()
except:
pass
2020-03-22 19:05:51 +03:00
2020-07-04 14:16:17 +03:00
def on_settings_changed(self, *args):
if self.done:
self.populate()
class Browser(Gtk.Paned):
def __init__(self, client, settings, window):
Gtk.Paned.__init__(self) # paned1
self.set_orientation(Gtk.Orientation.HORIZONTAL)
# adding vars
self.client=client
self.settings=settings
self.window=window
self.use_csd=self.settings.get_boolean("use-csd")
if self.use_csd:
self.icon_size=0
else:
self.icon_size=self.settings.get_int("icon-size")
# widgets
self.icons={}
icons_data=["go-previous-symbolic", "system-search-symbolic"]
for data in icons_data:
self.icons[data]=PixelSizedIcon(data, self.icon_size)
self.back_to_album_button=Gtk.Button(image=self.icons["go-previous-symbolic"])
self.back_to_album_button.set_tooltip_text(_("Back to current album"))
self.search_button=Gtk.ToggleButton(image=self.icons["system-search-symbolic"])
self.search_button.set_tooltip_text(_("Search"))
self.genre_select=GenreSelect(self.client, self.settings)
self.artist_view=ArtistView(self.client, self.settings, self.genre_select)
self.search=SearchWindow(self.client)
self.album_view=AlbumView(self.client, self.settings, self.genre_select, self.window)
# connect
self.back_to_album_button.connect("clicked", self.back_to_album)
self.search_button.connect("toggled", self.on_search_toggled)
self.artist_view.connect("artists_changed", self.on_artists_changed)
if not self.use_csd:
self.settings.connect("changed::icon-size", self.on_icon_size_changed)
self.client.emitter.connect("disconnected", self.on_disconnected)
self.client.emitter.connect("reconnected", self.on_reconnected)
# packing
self.stack=Gtk.Stack()
self.stack.set_transition_type(1)
self.stack.add_named(self.album_view, "albums")
self.stack.add_named(self.search, "search")
if self.use_csd:
self.pack1(self.artist_view, False, False)
else:
hbox=Gtk.Box(spacing=6)
hbox.set_property("border-width", 6)
hbox.pack_start(self.back_to_album_button, False, False, 0)
hbox.pack_start(self.genre_select, True, True, 0)
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_view, True, True, 0)
self.pack1(box1, False, False)
self.pack2(self.stack, True, False)
self.set_position(self.settings.get_int("paned1"))
def save_settings(self):
self.settings.set_int("paned1", self.get_position())
def clear(self, *args):
self.genre_select.clear()
self.artist_view.clear()
self.album_view.clear()
self.search.clear()
def search_started(self):
return self.search.started()
2020-07-15 23:39:26 +03:00
def back_to_album(self, *args): # TODO
2020-07-04 14:16:17 +03:00
try: # since this can still be running when the connection is lost, various exceptions can occur
song=ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong"))
2020-07-04 14:16:17 +03:00
try:
artist=song[self.settings.get_artist_type()]
except:
try:
artist=song["artist"]
except:
artist=""
try:
if not song['genre'] == self.genre_select.get_value():
self.genre_select.deactivate() # deactivate genre filter to show all artists
except:
self.genre_select.deactivate() # deactivate genre filter to show all artists
if len(self.artist_view.get_selected_artists()) <= 1:
row_num=len(self.artist_view.store)
for i in range(0, row_num):
path=Gtk.TreePath(i)
if self.artist_view.store[path][0] == artist:
self.artist_view.treeview.set_cursor(path, None, False)
if not self.artist_view.get_selected_artists() == [artist]:
self.artist_view.treeview.row_activated(path, self.artist_view.column_name)
else:
self.search_button.set_active(False)
self.artist_view.highlight_selected()
break
else:
self.search_button.set_active(False)
self.artist_view.treeview.set_cursor(Gtk.TreePath(0), None, False) # set cursor to 'all artists'
self.album_view.scroll_to_selected_album()
except:
pass
def on_search_toggled(self, widget):
if widget.get_active():
self.stack.set_visible_child_name("search")
self.search.start()
else:
self.stack.set_visible_child_name("albums")
def on_reconnected(self, *args):
self.back_to_album_button.set_sensitive(True)
self.search_button.set_sensitive(True)
self.genre_select.set_sensitive(True)
def on_disconnected(self, *args):
self.clear()
self.back_to_album_button.set_sensitive(False)
self.search_button.set_active(False)
self.search_button.set_sensitive(False)
self.genre_select.set_sensitive(False)
def on_artists_changed(self, *args):
self.search_button.set_active(False)
artists=self.artist_view.get_selected_artists()
self.album_view.refresh(artists)
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)
######################
# playlist and cover #
######################
class AudioType(Gtk.Label):
def __init__(self, client):
Gtk.Label.__init__(self)
# adding vars
self.client=client
self.freq=0
self.res=0
self.chan=0
self.brate=0
self.file_type=""
2020-07-04 14:16:17 +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)
2020-07-04 14:16:17 +03:00
self.client.emitter.connect("disconnected", self.clear)
self.client.emitter.connect("state", self.on_state)
2020-07-04 14:16:17 +03:00
def clear(self, *args):
self.set_text("")
self.freq=0
self.res=0
self.chan=0
self.brate=0
self.file_type=""
2020-07-04 14:16:17 +03:00
def refresh(self, *args):
string=_("%(bitrate)s kb/s, %(frequency)s kHz, %(resolution)i bit, %(channels)i channels, %(file_type)s") % {"bitrate": str(self.brate), "frequency": str(self.freq), "resolution": self.res, "channels": self.chan, "file_type": self.file_type}
self.set_text(string)
def on_audio(self, emitter, freq, res, chan):
self.freq=freq/1000
self.res=res
self.chan=chan
self.refresh()
def on_bitrate(self, emitter, brate):
self.brate=brate
self.refresh()
def on_song_changed(self, *args):
2020-07-04 14:16:17 +03:00
try:
self.file_type=self.client.wrapped_call("currentsong")["file"].split('.')[-1]
self.refresh()
2020-07-04 14:16:17 +03:00
except:
pass
2020-07-04 14:16:17 +03:00
def on_state(self, emitter, state):
if state == "stop":
2020-07-04 14:16:17 +03:00
self.clear()
2020-03-10 15:50:36 +03:00
2020-03-30 18:08:59 +03:00
class MainCover(Gtk.Frame):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings, window):
2020-03-30 18:08:59 +03:00
Gtk.Frame.__init__(self)
2020-07-04 13:35:39 +03:00
# diable auto resize
2020-05-18 16:53:29 +03:00
self.set_halign(3)
self.set_valign(3)
2020-07-04 13:35:39 +03:00
# css
2020-03-30 18:08:59 +03:00
style_context=self.get_style_context()
2020-04-09 01:26:21 +03:00
provider=Gtk.CssProvider()
css=b"""* {background-color: @theme_base_color; border-radius: 6px;}"""
2020-03-30 18:08:59 +03:00
provider.load_from_data(css)
style_context.add_provider(provider, 800)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-01-11 13:25:15 +03:00
self.client=client
2020-03-03 15:41:46 +03:00
self.settings=settings
2020-02-03 01:10:33 +03:00
self.window=window
2020-03-03 15:41:46 +03:00
2020-07-04 13:35:39 +03:00
# event box
2020-03-30 18:08:59 +03:00
event_box=Gtk.EventBox()
event_box.set_property("border-width", 6)
2020-07-04 13:35:39 +03:00
# cover
2020-03-03 15:41:46 +03:00
self.cover=Gtk.Image.new()
size=self.settings.get_int("track-cover")
2020-07-04 13:35:39 +03:00
self.cover.set_from_pixbuf(Cover(lib_path=self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file=None).get_pixbuf(size)) # set to fallback cover
# set default size
self.cover.set_size_request(size, size)
2020-07-04 13:35:39 +03:00
# connect
2020-03-30 18:08:59 +03:00
event_box.connect("button-press-event", self.on_button_press_event)
self.client.emitter.connect("current_song_changed", self.refresh)
2020-03-10 15:50:36 +03:00
self.settings.connect("changed::track-cover", self.on_settings_changed)
2020-03-03 15:41:46 +03:00
2020-03-30 18:08:59 +03:00
event_box.add(self.cover)
self.add(event_box)
2020-03-03 15:41:46 +03:00
def refresh(self, *args):
2020-03-22 20:13:40 +03:00
try:
current_song=self.client.wrapped_call("currentsong")
2020-03-22 16:25:04 +03:00
song_file=current_song['file']
2020-03-22 20:13:40 +03:00
except:
song_file=None
2020-03-22 16:25:04 +03:00
self.cover.set_from_pixbuf(Cover(lib_path=self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file=song_file).get_pixbuf(self.settings.get_int("track-cover")))
2020-03-03 15:41:46 +03:00
2020-03-22 19:05:51 +03:00
def clear(self, *args):
self.cover.set_from_pixbuf(Cover(lib_path=self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file=None).get_pixbuf(self.settings.get_int("track-cover")))
self.song_file=None
2020-03-03 15:41:46 +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"))
2020-03-03 15:41:46 +03:00
if not song == {}:
try:
artist=song[self.settings.get_artist_type()]
except:
try:
artist=song["artist"]
except:
artist=""
2020-03-03 15:41:46 +03:00
try:
album=song["album"]
except:
album=""
try:
album_year=song["date"]
except:
album_year=""
2020-03-30 21:44:28 +03:00
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", album, artist, album_year, False)
2020-03-30 21:44:28 +03:00
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
2020-07-11 13:38:18 +03:00
self.client.wrapped_call("album_to_playlist", album, artist, album_year, True)
2020-03-30 21:44:28 +03:00
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
2020-04-09 01:26:21 +03:00
album_dialog=AlbumDialog(self.window, self.client, self.settings, album, artist, album_year)
2020-03-30 23:20:14 +03:00
album_dialog.open()
2020-03-03 15:41:46 +03:00
album_dialog.destroy()
2020-03-10 15:50:36 +03:00
def on_settings_changed(self, *args):
size=self.settings.get_int("track-cover")
self.cover.set_size_request(size, size)
2020-03-10 15:50:36 +03:00
self.song_file=None
self.refresh()
2020-03-22 19:05:51 +03:00
class PlaylistView(Gtk.Box):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings):
2020-03-03 15:41:46 +03:00
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
2020-07-04 13:35:39 +03:00
# adding vars
2020-03-03 15:41:46 +03:00
self.client=client
2020-03-04 18:39:59 +03:00
self.settings=settings
self.playlist_version=None
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# Store
# (track, disc, title, artist, album, duration, date, genre, file, weight)
2020-04-09 01:26:21 +03:00
self.store=Gtk.ListStore(str, str, str, str, str, str, str, str, str, Pango.Weight)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# TreeView
2020-04-09 01:26:21 +03:00
self.treeview=Gtk.TreeView(model=self.store)
self.treeview.set_search_column(2)
self.treeview.set_property("activate-on-single-click", True)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# selection
2020-04-09 01:26:21 +03:00
self.selection=self.treeview.get_selection()
2020-01-11 13:25:15 +03:00
self.selection.set_mode(Gtk.SelectionMode.SINGLE)
2020-07-04 13:35:39 +03:00
# Column
2020-05-25 22:32:50 +03:00
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
2020-04-09 01:26:21 +03:00
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
2020-03-04 18:39:59 +03:00
self.columns=[None, None, None, None, None, None, None, None]
2020-01-11 13:25:15 +03:00
2020-04-09 01:26:21 +03:00
self.columns[0]=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[0].set_property("resizable", True)
2020-01-11 13:25:15 +03:00
2020-04-09 01:26:21 +03:00
self.columns[1]=Gtk.TreeViewColumn(_("Disc"), renderer_text_ralign, text=1, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[1].set_property("resizable", True)
2020-01-11 13:25:15 +03:00
2020-04-09 01:26:21 +03:00
self.columns[2]=Gtk.TreeViewColumn(_("Title"), renderer_text, text=2, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[2].set_property("resizable", True)
2020-04-09 01:26:21 +03:00
self.columns[3]=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=3, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[3].set_property("resizable", True)
2020-04-09 01:26:21 +03:00
self.columns[4]=Gtk.TreeViewColumn(_("Album"), renderer_text, text=4, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[4].set_property("resizable", True)
2020-04-09 01:26:21 +03:00
self.columns[5]=Gtk.TreeViewColumn(_("Length"), renderer_text, text=5, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[5].set_property("resizable", True)
2020-01-11 13:25:15 +03:00
2020-04-09 01:26:21 +03:00
self.columns[6]=Gtk.TreeViewColumn(_("Year"), renderer_text, text=6, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[6].set_property("resizable", True)
2020-04-09 01:26:21 +03:00
self.columns[7]=Gtk.TreeViewColumn(_("Genre"), renderer_text, text=7, weight=9)
2020-03-04 18:39:59 +03:00
self.columns[7].set_property("resizable", True)
self.load_settings()
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# scroll
2020-01-11 13:25:15 +03:00
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self.treeview)
2020-07-04 13:35:39 +03:00
# frame
2020-03-31 18:36:41 +03:00
frame=FocusFrame()
frame.set_widget(self.treeview)
frame.add(scroll)
2020-07-04 13:35:39 +03:00
# audio infos
2020-01-11 13:25:15 +03:00
audio=AudioType(self.client)
2020-05-26 16:04:16 +03:00
audio.set_margin_end(3)
audio.set_xalign(1)
audio.set_ellipsize(Pango.EllipsizeMode.END)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# playlist info
2020-01-27 22:27:35 +03:00
self.playlist_info=Gtk.Label()
2020-05-26 16:04:16 +03:00
self.playlist_info.set_margin_start(3)
2020-01-27 22:27:35 +03:00
self.playlist_info.set_xalign(0)
self.playlist_info.set_ellipsize(Pango.EllipsizeMode.END)
2020-07-04 13:35:39 +03:00
# status bar
2020-03-24 21:18:29 +03:00
status_bar=Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
2020-05-26 16:04:16 +03:00
status_bar.set_property("border-width", 3)
2020-01-27 22:27:35 +03:00
status_bar.pack_start(self.playlist_info, True, True, 0)
status_bar.pack_end(audio, False, False, 0)
2020-07-04 13:35:39 +03:00
# connect
2020-01-11 13:25:15 +03:00
self.treeview.connect("row-activated", self.on_row_activated)
self.key_press_event=self.treeview.connect("key-press-event", self.on_key_press_event)
2020-03-26 13:53:36 +03:00
self.treeview.connect("button-press-event", self.on_button_press_event)
2020-01-11 13:25:15 +03:00
self.client.emitter.connect("playlist_changed", self.on_playlist_changed)
self.client.emitter.connect("current_song_changed", self.on_song_changed)
2020-03-30 12:54:04 +03:00
self.client.emitter.connect("disconnected", self.on_disconnected)
2020-03-04 18:39:59 +03:00
self.settings.connect("changed::column-visibilities", self.load_settings)
self.settings.connect("changed::column-permutation", self.load_settings)
2020-07-04 13:35:39 +03:00
# packing
2020-03-31 18:36:41 +03:00
self.pack_start(frame, True, True, 0)
2020-03-24 21:18:29 +03:00
self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
2020-01-27 22:27:35 +03:00
self.pack_end(status_bar, False, False, 0)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
def save_settings(self): # only saves the column sizes
2020-03-04 18:39:59 +03:00
columns=self.treeview.get_columns()
2020-03-04 20:55:43 +03:00
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()
2020-03-04 18:39:59 +03:00
self.settings.set_value("column-sizes", GLib.Variant("ai", sizes))
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"):
2020-03-04 20:55:43 +03:00
if sizes[i] > 0:
self.columns[i].set_fixed_width(sizes[i])
self.columns[i].set_visible(visibilities[i])
2020-03-04 18:39:59 +03:00
self.treeview.append_column(self.columns[i])
def scroll_to_selected_title(self):
2020-07-04 14:16:17 +03:00
treeview, treeiter=self.selection.get_selected()
if not treeiter == None:
path=treeview.get_path(treeiter)
self.treeview.scroll_to_cell(path, None, True, 0.25)
2020-07-04 14:16:17 +03:00
def refresh_playlist_info(self):
songs=self.client.wrapped_call("playlistinfo")
2020-07-04 14:16:17 +03:00
if not songs == []:
whole_length_human_readable=ClientHelper.calc_display_length(songs)
self.playlist_info.set_text(_("%(total_tracks)i titles (%(total_length)s)") % {"total_tracks": len(songs), "total_length": whole_length_human_readable})
else:
2020-07-04 14:16:17 +03:00
self.playlist_info.set_text("")
2020-03-04 01:43:44 +03:00
2020-07-04 14:16:17 +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"]
2020-07-04 14:16:17 +03:00
path=Gtk.TreePath(int(song))
self.selection.select_path(path)
self.store[path][9]=Pango.Weight.BOLD
self.scroll_to_selected_title()
except:
self.selection.unselect_all()
2020-01-11 13:25:15 +03:00
def clear(self, *args):
2020-07-04 14:16:17 +03:00
self.playlist_info.set_text("")
self.store.clear()
self.playlist_version=None
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def remove_song(self, path):
self.client.wrapped_call("delete", path) # bad song index possible
2020-07-04 14:16:17 +03:00
self.store.remove(self.store.get_iter(path))
self.playlist_version=self.client.wrapped_call("status")["playlist"]
2020-05-17 23:52:10 +03:00
2020-07-04 14:16:17 +03:00
def on_key_press_event(self, widget, event):
self.treeview.handler_block(self.key_press_event)
if event.keyval == 65535: # entf
treeview, treeiter=self.selection.get_selected()
if not treeiter == None:
path=self.store.get_path(treeiter)
try:
2020-07-04 14:16:17 +03:00
self.remove_song(path)
except:
2020-07-04 14:16:17 +03:00
pass
elif event.keyval == 65383: # menu key
treeview, treeiter=self.selection.get_selected()
if not treeiter == 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))
2020-07-04 14:16:17 +03:00
pop.popup()
pop.show_all()
self.treeview.handler_unblock(self.key_press_event)
def on_button_press_event(self, widget, event):
if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
2020-07-04 14:16:17 +03:00
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self.remove_song(path)
except:
2020-07-04 14:16:17 +03:00
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))
2020-07-04 14:16:17 +03:00
pop.popup()
except:
pass
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_row_activated(self, widget, path, view_column):
self.client.wrapped_call("play", path)
2020-07-04 14:16:17 +03:00
def on_playlist_changed(self, emitter, version):
2020-07-04 14:16:17 +03:00
songs=[]
if not self.playlist_version == None:
songs=self.client.wrapped_call("plchanges", self.playlist_version)
else:
songs=self.client.wrapped_call("playlistinfo")
2020-07-04 14:16:17 +03:00
if not 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))):
2020-07-04 14:16:17 +03:00
treeiter=self.store.get_iter(i)
self.store.remove(treeiter)
self.refresh_playlist_info()
if self.playlist_version == None or not songs == []:
self.refresh_selection()
self.playlist_version=version
def on_song_changed(self, *args):
2020-07-04 14:16:17 +03:00
self.refresh_selection()
def on_disconnected(self, *args):
2020-03-30 13:28:30 +03:00
self.clear()
2020-06-27 17:11:41 +03:00
2020-07-04 14:16:17 +03:00
class CoverLyricsOSD(Gtk.Overlay):
def __init__(self, client, settings, window):
Gtk.Overlay.__init__(self)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-07-04 14:16:17 +03:00
self.client=client
2020-04-09 01:26:21 +03:00
self.settings=settings
2020-07-04 14:16:17 +03:00
self.window=window
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# cover
self.cover=MainCover(self.client, self.settings, self.window)
self.cover.set_property("border-width", 3)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# lyrics button
self.lyrics_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("media-view-subtitles-symbolic", Gtk.IconSize.BUTTON))
self.lyrics_button.set_tooltip_text(_("Show lyrics"))
style_context=self.lyrics_button.get_style_context()
style_context.add_class("circular")
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# revealer
# workaround to get tooltips in overlay
self.revealer=Gtk.Revealer()
self.revealer.set_halign(2)
self.revealer.set_valign(1)
self.revealer.set_margin_top(6)
self.revealer.set_margin_end(6)
self.revealer.add(self.lyrics_button)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# event box
self.event_box=Gtk.EventBox()
self.event_box.add(self.cover)
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
# packing
self.add(self.event_box)
self.add_overlay(self.revealer)
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
# connect
self.lyrics_button.connect("clicked", self.on_lyrics_clicked)
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-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
self.on_settings_changed() # hide lyrics button
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
def show_lyrics(self, *args):
if self.lyrics_button.get_sensitive():
self.lyrics_button.emit("clicked")
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_reconnected(self, *args):
self.lyrics_button.set_sensitive(True)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_disconnected(self, *args):
self.lyrics_button.set_sensitive(False)
self.cover.clear()
try:
self.lyrics_win.destroy()
except:
pass
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_lyrics_clicked(self, widget):
self.lyrics_button.set_sensitive(False)
self.lyrics_win=LyricsWindow(self.client, self.settings)
def on_destroy(*args):
self.lyrics_button.set_sensitive(True)
self.lyrics_win.connect("destroy", on_destroy)
self.add_overlay(self.lyrics_win)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_settings_changed(self, *args):
if self.settings.get_boolean("show-lyrics-button"):
self.revealer.set_reveal_child(True)
2020-02-02 16:20:25 +03:00
else:
2020-07-04 14:16:17 +03:00
self.revealer.set_reveal_child(False)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
class CoverPlaylistView(Gtk.Paned):
def __init__(self, client, settings, window):
Gtk.Paned.__init__(self) # paned0
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.window=window
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# widgets
self.cover=CoverLyricsOSD(self.client, self.settings, self.window)
self.playlist_view=PlaylistView(self.client, self.settings)
2020-02-01 15:27:46 +03:00
2020-07-04 14:16:17 +03:00
# packing
self.pack1(self.cover, False, False)
self.pack2(self.playlist_view, True, False)
2020-07-04 14:16:17 +03:00
self.set_position(self.settings.get_int("paned0"))
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def show_lyrics(self, *args):
self.cover.show_lyrics()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def save_settings(self):
self.settings.set_int("paned0", self.get_position())
self.playlist_view.save_settings()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
###################
# settings dialog #
###################
2020-01-11 13:25:15 +03:00
2020-03-24 18:14:01 +03:00
class GeneralSettings(Gtk.Box):
2020-01-11 13:25:15 +03:00
def __init__(self, settings):
2020-03-24 18:14:01 +03:00
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.set_property("border-width", 18)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-04-09 01:26:21 +03:00
self.settings=settings
2020-06-26 21:24:06 +03:00
self.settings_handlers=[]
2020-07-04 13:35:39 +03:00
# int_settings
2020-06-26 21:24:06 +03:00
int_settings={}
int_settings_data=[(_("Main cover size:"), (100, 1200, 10), "track-cover"),\
(_("Album view cover size:"), (50, 600, 10), "album-cover"),\
2020-06-27 17:11:41 +03:00
(_("Button icon size:"), (16, 64, 2), "icon-size")]
2020-06-26 21:24:06 +03:00
for data in int_settings_data:
int_settings[data[2]]=(Gtk.Label(), IntEntry(self.settings.get_int(data[2]), data[1][0], data[1][1], data[1][2]))
int_settings[data[2]][0].set_label(data[0])
int_settings[data[2]][0].set_xalign(0)
int_settings[data[2]][1].connect("value-changed", self.on_int_changed, data[2])
self.settings_handlers.append(self.settings.connect("changed::"+data[2], self.on_int_settings_changed, int_settings[data[2]][1]))
2020-07-04 13:35:39 +03:00
# combo_settings
2020-04-01 19:52:08 +03:00
combo_settings={}
combo_settings_data=[(_("Sort albums by:"), _("name"), _("year"), "sort-albums-by-year"), \
(_("Position of playlist:"), _("bottom"), _("right"), "playlist-right")]
for data in combo_settings_data:
combo_settings[data[3]]=(Gtk.Label(), Gtk.ComboBoxText())
combo_settings[data[3]][0].set_label(data[0])
combo_settings[data[3]][0].set_xalign(0)
combo_settings[data[3]][1].set_entry_text_column(0)
combo_settings[data[3]][1].append_text(data[1])
combo_settings[data[3]][1].append_text(data[2])
if self.settings.get_boolean(data[3]):
combo_settings[data[3]][1].set_active(1)
else:
combo_settings[data[3]][1].set_active(0)
combo_settings[data[3]][1].connect("changed", self.on_combo_changed, data[3])
2020-06-26 21:24:06 +03:00
self.settings_handlers.append(self.settings.connect("changed::"+data[3], self.on_combo_settings_changed, combo_settings[data[3]][1]))
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# check buttons
2020-03-24 18:14:01 +03:00
check_buttons={}
2020-06-26 21:24:06 +03:00
check_buttons_data=[(_("Use Client-side decoration"), "use-csd"), \
(_("Show stop button"), "show-stop"), \
2020-06-22 18:00:54 +03:00
(_("Show lyrics button"), "show-lyrics-button"), \
2020-03-22 23:49:55 +03:00
(_("Show initials in artist view"), "show-initials"), \
2020-03-12 19:09:24 +03:00
(_("Show tooltips in album view"), "show-album-view-tooltips"), \
(_("Use 'Album Artist' tag"), "use-album-artist"), \
2020-03-22 23:49:55 +03:00
(_("Send notification on title change"), "send-notify"), \
(_("Stop playback on quit"), "stop-on-quit"), \
2020-04-01 14:22:50 +03:00
(_("Play selected albums and titles immediately"), "force-mode")]
2020-01-11 13:25:15 +03:00
2020-06-26 21:24:06 +03:00
for data in check_buttons_data:
2020-03-24 18:14:01 +03:00
check_buttons[data[1]]=Gtk.CheckButton(label=data[0])
check_buttons[data[1]].set_active(self.settings.get_boolean(data[1]))
check_buttons[data[1]].set_margin_start(12)
2020-06-26 21:24:06 +03:00
check_buttons[data[1]].connect("toggled", self.on_toggled, data[1])
self.settings_handlers.append(self.settings.connect("changed::"+data[1], self.on_check_settings_changed, check_buttons[data[1]]))
2020-07-04 13:35:39 +03:00
# headings
2020-06-26 21:24:06 +03:00
view_heading=Gtk.Label()
view_heading.set_markup(_("<b>View</b>"))
view_heading.set_xalign(0)
behavior_heading=Gtk.Label()
behavior_heading.set_markup(_("<b>Behavior</b>"))
behavior_heading.set_xalign(0)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# view grid
view_grid=Gtk.Grid()
view_grid.set_row_spacing(6)
view_grid.set_column_spacing(12)
view_grid.set_margin_start(12)
2020-06-26 21:24:06 +03:00
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(combo_settings["playlist-right"][0], int_settings["icon-size"][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)
2020-04-01 19:52:08 +03:00
view_grid.attach_next_to(combo_settings["playlist-right"][1], combo_settings["playlist-right"][0], Gtk.PositionType.RIGHT, 1, 1)
2020-07-04 13:35:39 +03:00
# behavior grid
behavior_grid=Gtk.Grid()
behavior_grid.set_row_spacing(6)
behavior_grid.set_column_spacing(12)
behavior_grid.set_margin_start(12)
2020-04-01 19:52:08 +03:00
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 13:35:39 +03:00
# connect
2020-06-26 21:24:06 +03:00
self.connect("destroy", self.remove_handlers)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# packing
2020-05-21 23:28:58 +03:00
box=Gtk.Box(spacing=12)
box.pack_start(check_buttons["use-csd"], False, False, 0)
2020-06-26 21:24:06 +03:00
box.pack_start(Gtk.Label(label=_("(restart required)"), sensitive=False), False, False, 0)
2020-03-24 18:14:01 +03:00
self.pack_start(view_heading, True, True, 0)
2020-05-21 23:28:58 +03:00
self.pack_start(box, True, True, 0)
2020-03-24 18:14:01 +03:00
self.pack_start(check_buttons["show-stop"], True, True, 0)
2020-06-22 18:00:54 +03:00
self.pack_start(check_buttons["show-lyrics-button"], True, True, 0)
2020-03-24 18:14:01 +03:00
self.pack_start(check_buttons["show-initials"], True, True, 0)
self.pack_start(check_buttons["show-album-view-tooltips"], True, True, 0)
self.pack_start(view_grid, True, True, 0)
2020-03-24 18:14:01 +03:00
self.pack_start(behavior_heading, True, True, 0)
self.pack_start(check_buttons["use-album-artist"], True, True, 0)
2020-03-24 18:14:01 +03:00
self.pack_start(check_buttons["send-notify"], True, True, 0)
self.pack_start(check_buttons["stop-on-quit"], True, True, 0)
self.pack_start(check_buttons["force-mode"], True, True, 0)
self.pack_start(behavior_grid, True, True, 0)
2020-01-11 13:25:15 +03:00
2020-06-26 21:24:06 +03:00
def remove_handlers(self, *args):
for handler in self.settings_handlers:
self.settings.disconnect(handler)
def on_int_settings_changed(self, settings, key, entry):
entry.set_value(settings.get_int(key))
def on_combo_settings_changed(self, settings, key, combo):
if settings.get_boolean(key):
combo.set_active(1)
else:
combo.set_active(0)
def on_check_settings_changed(self, settings, key, button):
button.set_active(settings.get_boolean(key))
2020-01-11 13:25:15 +03:00
def on_int_changed(self, widget, key):
self.settings.set_int(key, widget.get_int())
2020-04-01 19:52:08 +03:00
def on_combo_changed(self, box, key):
active=box.get_active()
if active == 0:
2020-04-01 19:52:08 +03:00
self.settings.set_boolean(key, False)
else:
2020-04-01 19:52:08 +03:00
self.settings.set_boolean(key, True)
2020-03-24 18:14:01 +03:00
def on_toggled(self, widget, key):
self.settings.set_boolean(key, widget.get_active())
2020-07-04 14:16:17 +03:00
class ProfileSettings(Gtk.Grid):
def __init__(self, parent, settings):
Gtk.Grid.__init__(self)
self.set_row_spacing(6)
self.set_column_spacing(12)
2020-03-24 18:14:01 +03:00
self.set_property("border-width", 18)
2020-03-04 18:39:59 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-04-09 01:26:21 +03:00
self.settings=settings
2020-07-06 19:03:37 +03:00
self.gui_modification=False # indicates whether the settings were changed from the settings dialog
2020-03-04 18:39:59 +03:00
2020-07-04 14:16:17 +03:00
# widgets
self.profiles_combo=Gtk.ComboBoxText()
self.profiles_combo.set_entry_text_column(0)
2020-03-04 18:39:59 +03:00
2020-07-04 14:16:17 +03:00
add_button=Gtk.Button(label=None, image=Gtk.Image(stock=Gtk.STOCK_ADD))
delete_button=Gtk.Button(label=None, image=Gtk.Image(stock=Gtk.STOCK_DELETE))
add_delete_buttons=Gtk.ButtonBox()
add_delete_buttons.set_property("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-03-24 18:14:01 +03:00
2020-07-04 14:16:17 +03:00
self.profile_entry=Gtk.Entry()
self.host_entry=Gtk.Entry()
self.port_entry=IntEntry(0, 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()
self.password_entry.set_visibility(False)
self.path_entry=Gtk.Entry()
self.path_select_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_OPEN))
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)
2020-03-24 18:14:01 +03:00
2020-07-04 14:16:17 +03:00
profiles_label=Gtk.Label(label=_("Profile:"))
profiles_label.set_xalign(1)
profile_label=Gtk.Label(label=_("Name:"))
profile_label.set_xalign(1)
host_label=Gtk.Label(label=_("Host:"))
host_label.set_xalign(1)
password_label=Gtk.Label(label=_("Password:"))
password_label.set_xalign(1)
path_label=Gtk.Label(label=_("Music lib:"))
path_label.set_xalign(1)
2020-03-04 18:39:59 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-07-04 14:16:17 +03:00
add_button.connect("clicked", self.on_add_button_clicked)
delete_button.connect("clicked", self.on_delete_button_clicked)
self.path_select_button.connect("clicked", self.on_path_select_button_clicked, parent)
self.profiles_combo_changed=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)))
2020-06-26 21:24:06 +03:00
self.settings_handlers=[]
2020-07-04 14:16:17 +03:00
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))
2020-06-26 21:24:06 +03:00
self.connect("destroy", self.remove_handlers)
2020-03-24 18:14:01 +03:00
2020-07-04 14:16:17 +03:00
self.profiles_combo_reload()
self.profiles_combo.set_active(0)
# 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(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)
2020-03-04 18:39:59 +03:00
2020-06-26 21:24:06 +03:00
def remove_handlers(self, *args):
for handler in self.settings_handlers:
self.settings.disconnect(handler)
2020-07-04 14:16:17 +03:00
def on_settings_changed(self, *args):
if self.gui_modification:
self.gui_modification=False
2020-06-26 21:24:06 +03:00
else:
2020-07-04 14:16:17 +03:00
self.profiles_combo_reload()
self.profiles_combo.set_active(0)
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
def block_entry_changed_handlers(self, *args):
for obj, handler in self.entry_changed_handlers:
obj.handler_block(handler)
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
def unblock_entry_changed_handlers(self, *args):
for obj, handler in self.entry_changed_handlers:
obj.handler_unblock(handler)
2020-03-24 18:14:01 +03:00
2020-07-04 14:16:17 +03:00
def profiles_combo_reload(self, *args):
self.block_entry_changed_handlers()
2020-03-24 18:14:01 +03:00
2020-07-04 14:16:17 +03:00
self.profiles_combo.remove_all()
for profile in self.settings.get_value("profiles"):
self.profiles_combo.append_text(profile)
2020-06-26 21:24:06 +03:00
2020-07-04 14:16:17 +03:00
self.unblock_entry_changed_handlers()
2020-03-24 18:14:01 +03:00
2020-07-04 14:16:17 +03:00
def on_add_button_clicked(self, *args):
pos=self.profiles_combo.get_active()
self.settings.array_append('as', "profiles", "new profile")
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.profiles_combo_reload()
self.profiles_combo.set_active(pos)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_delete_button_clicked(self, *args):
pos=self.profiles_combo.get_active()
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)
if len(self.settings.get_value("profiles")) == 0:
self.on_add_button_clicked()
else:
self.profiles_combo_reload()
self.profiles_combo.set_active(0)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_profile_entry_changed(self, *args):
self.gui_modification=True
pos=self.profiles_combo.get_active()
self.settings.array_modify('as', "profiles", pos, self.profile_entry.get_text())
self.profiles_combo_reload()
self.profiles_combo.set_active(pos)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_host_entry_changed(self, *args):
self.gui_modification=True
self.settings.array_modify('as', "hosts", self.profiles_combo.get_active(), self.host_entry.get_text())
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_port_entry_changed(self, *args):
self.gui_modification=True
self.settings.array_modify('ai', "ports", self.profiles_combo.get_active(), self.port_entry.get_int())
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_password_entry_changed(self, *args):
self.gui_modification=True
self.settings.array_modify('as', "passwords", self.profiles_combo.get_active(), self.password_entry.get_text())
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_path_entry_changed(self, *args):
self.gui_modification=True
self.settings.array_modify('as', "paths", self.profiles_combo.get_active(), self.path_entry.get_text())
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +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
self.settings.array_modify('as', "paths", self.profiles_combo.get_active(), dialog.get_filename())
self.path_entry.set_text(dialog.get_filename())
dialog.destroy()
2020-06-27 17:11:41 +03:00
2020-07-04 14:16:17 +03:00
def on_profiles_changed(self, *args):
active=self.profiles_combo.get_active()
self.block_entry_changed_handlers()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +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_int(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])
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
self.unblock_entry_changed_handlers()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
class PlaylistSettings(Gtk.Box):
def __init__(self, settings):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.set_property("border-width", 18)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self.settings=settings
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# label
label=Gtk.Label(label=_("Choose the order of information to appear in the playlist:"))
label.set_line_wrap(True)
label.set_xalign(0)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# Store
# (toggle, header, actual_index)
self.store=Gtk.ListStore(bool, str, int)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# TreeView
self.treeview=Gtk.TreeView(model=self.store)
self.treeview.set_search_column(-1)
self.treeview.set_reorderable(True)
self.treeview.set_headers_visible(False)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# selection
self.selection=self.treeview.get_selection()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# Column
renderer_text=Gtk.CellRendererText()
renderer_toggle=Gtk.CellRendererToggle()
2020-06-27 17:11:41 +03:00
2020-07-04 14:16:17 +03:00
column_toggle=Gtk.TreeViewColumn("", renderer_toggle, active=0)
self.treeview.append_column(column_toggle)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
column_text=Gtk.TreeViewColumn("", renderer_text, text=1)
self.treeview.append_column(column_text)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# fill store
self.headers=[_("No"), _("Disc"), _("Title"), _("Artist"), _("Album"), _("Length"), _("Year"), _("Genre")]
self.fill()
2020-02-16 14:20:38 +03:00
2020-07-04 14:16:17 +03:00
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self.treeview)
frame=Gtk.Frame()
frame.add(scroll)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +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-03-28 15:23:56 +03:00
2020-07-04 14:16:17 +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-02-16 14:20:38 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-07-04 14:16:17 +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-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# packing
2020-07-04 14:16:17 +03:00
self.pack_start(label, False, False, 0)
self.pack_start(column_chooser, True, True, 0)
2020-07-04 14:16:17 +03:00
def remove_handlers(self, *args):
for handler in self.settings_handlers:
self.settings.disconnect(handler)
2020-07-04 14:16:17 +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-28 15:23:56 +03:00
2020-07-04 14:16:17 +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-03-28 15:23:56 +03:00
2020-07-04 14:16:17 +03:00
def set_button_sensitivity(self, *args):
treeiter=self.selection.get_selected()[1]
if treeiter == 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) == 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-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_cell_toggled(self, widget, path):
self.store[path][0]=not self.store[path][0]
self.settings.array_modify('ab', "column-visibilities", self.store[path][2], self.store[path][0])
2020-03-03 18:11:30 +03:00
2020-07-04 14:16:17 +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-03-03 18:11:30 +03:00
2020-07-04 14:16:17 +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-03-25 21:10:46 +03:00
2020-07-04 14:16:17 +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-03-25 21:10:46 +03:00
2020-07-04 14:16:17 +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-02-16 14:20:38 +03:00
2020-07-04 14:16:17 +03:00
class SettingsDialog(Gtk.Dialog):
def __init__(self, parent, settings):
Gtk.Dialog.__init__(self, title=_("Settings"), transient_for=parent)
self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
self.set_default_size(500, 400)
2020-02-16 14:20:38 +03:00
2020-07-04 14:16:17 +03:00
# adding vars
self.settings=settings
2020-03-25 21:10:46 +03:00
2020-07-04 14:16:17 +03:00
# widgets
general=GeneralSettings(self.settings)
profiles=ProfileSettings(parent, self.settings)
playlist=PlaylistSettings(self.settings)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +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")))
self.vbox.pack_start(tabs, True, True, 0) # vbox default widget of dialogs
self.vbox.set_spacing(3)
self.show_all()
###################
# control widgets #
###################
class ClientControl(Gtk.ButtonBox):
2020-03-30 12:54:04 +03:00
def __init__(self, client, settings):
2020-07-04 14:16:17 +03:00
Gtk.ButtonBox.__init__(self, spacing=6)
self.set_property("layout-style", Gtk.ButtonBoxStyle.EXPAND)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-01-11 13:25:15 +03:00
self.client=client
2020-01-28 21:59:14 +03:00
self.settings=settings
2020-06-27 17:11:41 +03:00
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
2020-06-27 17:11:41 +03:00
self.icons={}
2020-07-04 14:16:17 +03:00
icons_data=["media-playback-start-symbolic", "media-playback-stop-symbolic", "media-playback-pause-symbolic", \
"media-skip-backward-symbolic", "media-skip-forward-symbolic"]
2020-06-27 17:11:41 +03:00
for data in icons_data:
self.icons[data]=PixelSizedIcon(data, self.icon_size)
2020-07-04 14:16:17 +03:00
self.play_button=Gtk.Button(image=self.icons["media-playback-start-symbolic"])
self.stop_button=Gtk.Button(image=self.icons["media-playback-stop-symbolic"])
self.prev_button=Gtk.Button(image=self.icons["media-skip-backward-symbolic"])
self.next_button=Gtk.Button(image=self.icons["media-skip-forward-symbolic"])
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-07-04 14:16:17 +03:00
self.play_button.connect("clicked", self.on_play_clicked)
self.stop_button.connect("clicked", self.on_stop_clicked)
self.prev_button.connect("clicked", self.on_prev_clicked)
self.next_button.connect("clicked", self.on_next_clicked)
self.settings.connect("changed::show-stop", self.on_settings_changed)
2020-06-27 17:11:41 +03:00
self.settings.connect("changed::icon-size", self.on_icon_size_changed)
self.client.emitter.connect("state", self.on_state)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# packing
2020-07-04 14:16:17 +03:00
self.pack_start(self.prev_button, True, True, 0)
self.pack_start(self.play_button, True, True, 0)
if self.settings.get_boolean("show-stop"):
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_state(self, emitter, state):
if state == "play":
2020-07-04 14:16:17 +03:00
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":
2020-07-04 14:16:17 +03:00
self.play_button.set_image(self.icons["media-playback-start-symbolic"])
self.prev_button.set_sensitive(True)
self.next_button.set_sensitive(True)
2020-01-11 13:25:15 +03:00
else:
2020-07-04 14:16:17 +03:00
self.play_button.set_image(self.icons["media-playback-start-symbolic"])
self.prev_button.set_sensitive(False)
self.next_button.set_sensitive(False)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_play_clicked(self, widget):
if self.client.connected():
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
if status["state"] == "play":
self.client.wrapped_call("pause", 1)
2020-07-04 14:16:17 +03:00
elif status["state"] == "pause":
self.client.wrapped_call("pause", 0)
2020-07-04 14:16:17 +03:00
else:
try:
self.client.wrapped_call("play", status["song"])
2020-07-04 14:16:17 +03:00
except:
try:
self.client.wrapped_call("play", )
2020-07-04 14:16:17 +03:00
except:
pass
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_stop_clicked(self, widget):
if self.client.connected():
self.client.wrapped_call("stop")
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_prev_clicked(self, widget):
if self.client.connected():
self.client.wrapped_call("previous")
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_next_clicked(self, widget):
if self.client.connected():
self.client.wrapped_call("next")
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_settings_changed(self, *args):
if self.settings.get_boolean("show-stop"):
self.pack_start(self.stop_button, True, True, 0)
self.reorder_child(self.stop_button, 2)
self.stop_button.show()
else:
2020-07-04 14:16:17 +03:00
self.remove(self.stop_button)
2020-01-11 13:25:15 +03:00
2020-06-27 17:11:41 +03:00
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)
2020-07-04 14:16:17 +03:00
class SeekBar(Gtk.Box):
2020-01-11 13:25:15 +03:00
def __init__(self, client):
2020-07-04 14:16:17 +03:00
Gtk.Box.__init__(self)
self.set_hexpand(True)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
2020-01-11 13:25:15 +03:00
self.client=client
2020-07-04 14:16:17 +03:00
self.seek_time="10" # seek increment in seconds
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()
self.elapsed.set_width_chars(5)
self.rest=Gtk.Label()
self.rest.set_width_chars(6)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# progress bar
self.scale=Gtk.Scale.new_with_range(orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=0.001)
self.scale.set_show_fill_level(True)
self.scale.set_restrict_to_fill_level(False)
self.scale.set_draw_value(False)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# css (scale)
style_context=self.scale.get_style_context()
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
# event boxes
self.elapsed_event_box=Gtk.EventBox()
self.rest_event_box=Gtk.EventBox()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# connect
self.elapsed_event_box.connect("button-press-event", self.on_elapsed_button_press_event)
self.rest_event_box.connect("button-press-event", self.on_rest_button_press_event)
self.scale.connect("change-value", self.on_change_value)
self.scale.connect("scroll-event", self.dummy) # 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)
self.client.emitter.connect("disconnected", self.disable)
self.client.emitter.connect("state", self.on_state)
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)
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-07-04 14:16:17 +03:00
def dummy(self, *args):
return True
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_scale_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
self.update=False
self.scale.set_has_origin(False)
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self.jumped=False
2020-06-05 18:43:34 +03:00
2020-07-04 14:16:17 +03:00
def on_scale_button_release_event(self, widget, event):
if event.button == 1:
self.update=True
self.scale.set_has_origin(True)
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
if self.jumped: # actual seek
duration=float(status["duration"])
factor=(self.scale.get_value()/100)
pos=(duration*factor)
self.client.wrapped_call("seekcur", pos)
2020-07-04 14:16:17 +03:00
self.jumped=False
else:
self.refresh(None, float(status["elapsed"]), float(status["duration"]))
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_change_value(self, range, scroll, value): # value is inaccurate
if scroll == Gtk.ScrollType.STEP_BACKWARD:
self.seek_backward()
elif scroll == Gtk.ScrollType.STEP_FORWARD:
self.seek_forward()
elif scroll == Gtk.ScrollType.JUMP:
status=self.client.wrapped_call("status")
2020-07-04 14:16:17 +03:00
duration=float(status["duration"])
factor=(value/100)
if factor > 1: # fix display error
factor=1
elapsed=(factor*duration)
self.elapsed.set_text(str(datetime.timedelta(seconds=int(elapsed))).lstrip("0").lstrip(":"))
self.rest.set_text("-"+str(datetime.timedelta(seconds=int(duration-elapsed))).lstrip("0").lstrip(":"))
self.jumped=True
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def seek_forward(self):
self.client.wrapped_call("seekcur", "+"+self.seek_time)
2020-07-04 14:16:17 +03:00
def seek_backward(self):
self.client.wrapped_call("seekcur", "-"+self.seek_time)
2020-07-04 14:16:17 +03:00
def enable(self, *args):
self.scale.set_sensitive(True)
self.scale.set_range(0, 100)
self.elapsed_event_box.set_sensitive(True)
self.rest_event_box.set_sensitive(True)
2020-07-04 14:16:17 +03:00
def disable(self, *args):
self.scale.set_sensitive(False)
self.scale.set_range(0, 0)
self.elapsed_event_box.set_sensitive(False)
self.rest_event_box.set_sensitive(False)
self.elapsed.set_text("00:00")
self.rest.set_text("-00:00")
2020-07-04 14:16:17 +03:00
def on_elapsed_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
self.seek_backward()
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self.seek_forward()
2020-07-04 14:16:17 +03:00
def on_rest_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
self.seek_forward()
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self.seek_backward()
def on_state(self, emitter, state):
if state == "stop":
2020-07-04 14:16:17 +03:00
self.disable()
else:
self.enable()
def refresh(self, emitter, elapsed, duration):
2020-07-14 00:04:09 +03:00
if elapsed > duration: # fix display error
elapsed=duration
fraction=(elapsed/duration)*100
if self.update:
self.scale.set_value(fraction)
self.elapsed.set_text(str(datetime.timedelta(seconds=int(elapsed))).lstrip("0").lstrip(":"))
self.rest.set_text("-"+str(datetime.timedelta(seconds=int(duration-elapsed))).lstrip("0").lstrip(":"))
self.scale.set_fill_level(fraction)
2020-07-04 14:16:17 +03:00
class PlaybackOptions(Gtk.Box):
def __init__(self, client, settings):
Gtk.Box.__init__(self, 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={}
icons_data=["media-playlist-shuffle-symbolic", "media-playlist-repeat-symbolic", "zoom-original-symbolic", "edit-cut-symbolic"]
for data in icons_data:
self.icons[data]=PixelSizedIcon(data, self.icon_size)
2020-07-04 14:16:17 +03:00
self.random=Gtk.ToggleButton(image=self.icons["media-playlist-shuffle-symbolic"])
self.random.set_tooltip_text(_("Random mode"))
self.repeat=Gtk.ToggleButton(image=self.icons["media-playlist-repeat-symbolic"])
self.repeat.set_tooltip_text(_("Repeat mode"))
self.single=Gtk.ToggleButton(image=self.icons["zoom-original-symbolic"])
self.single.set_tooltip_text(_("Single mode"))
self.consume=Gtk.ToggleButton(image=self.icons["edit-cut-symbolic"])
self.consume.set_tooltip_text(_("Consume mode"))
self.volume_button=Gtk.VolumeButton()
self.volume_button.set_property("use-symbolic", True)
self.volume_button.set_property("size", self.settings.get_gtk_icon_size("icon-size"))
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# connect
self.random_toggled=self.random.connect("toggled", self.set_option, "random")
self.repeat_toggled=self.repeat.connect("toggled", self.set_option, "repeat")
self.single_toggled=self.single.connect("toggled", self.set_option, "single")
self.consume_toggled=self.consume.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)
2020-07-04 14:16:17 +03:00
self.settings.connect("changed::icon-size", self.on_icon_size_changed)
2020-07-04 14:16:17 +03:00
# packing
ButtonBox=Gtk.ButtonBox()
ButtonBox.set_property("layout-style", Gtk.ButtonBoxStyle.EXPAND)
ButtonBox.pack_start(self.repeat, True, True, 0)
ButtonBox.pack_start(self.random, True, True, 0)
ButtonBox.pack_start(self.single, True, True, 0)
ButtonBox.pack_start(self.consume, True, True, 0)
self.pack_start(ButtonBox, 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")
2020-07-04 14:16:17 +03:00
def set_volume(self, widget, value):
self.client.wrapped_call("setvol", str(int(value*100)))
def repeat_refresh(self, emitter, val):
2020-07-04 14:16:17 +03:00
self.repeat.handler_block(self.repeat_toggled)
self.repeat.set_active(val)
2020-07-04 14:16:17 +03:00
self.repeat.handler_unblock(self.repeat_toggled)
def random_refresh(self, emitter, val):
self.random.handler_block(self.random_toggled)
self.random.set_active(val)
2020-07-04 14:16:17 +03:00
self.random.handler_unblock(self.random_toggled)
def single_refresh(self, emitter, val):
self.single.handler_block(self.single_toggled)
self.single.set_active(val)
2020-07-04 14:16:17 +03:00
self.single.handler_unblock(self.single_toggled)
def consume_refresh(self, emitter, val):
self.consume.handler_block(self.consume_toggled)
self.consume.set_active(val)
2020-07-04 14:16:17 +03:00
self.consume.handler_unblock(self.consume_toggled)
def volume_refresh(self, emitter, volume):
self.volume_button.handler_block(self.volume_button_changed)
2020-07-04 14:16:17 +03:00
try:
self.volume_button.set_value(volume/100)
2020-07-04 14:16:17 +03:00
except:
self.volume_button.set_value(0)
self.volume_button.handler_unblock(self.volume_button_changed)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
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-07-04 14:16:17 +03:00
#################
# other dialogs #
#################
class ServerStats(Gtk.Dialog):
def __init__(self, parent, client):
Gtk.Dialog.__init__(self, title=_("Stats"), transient_for=parent)
self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# adding vars
self.client=client
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# Store
# (tag, value)
self.store=Gtk.ListStore(str, str)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
# TreeView
self.treeview=Gtk.TreeView(model=self.store)
self.treeview.set_can_focus(False)
self.treeview.set_search_column(-1)
self.treeview.set_headers_visible(False)
2020-07-04 14:16:17 +03:00
# selection
sel=self.treeview.get_selection()
sel.set_mode(Gtk.SelectionMode.NONE)
2020-07-04 14:16:17 +03:00
# Column
renderer_text=Gtk.CellRendererText()
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
2020-05-23 17:14:29 +03:00
2020-07-04 14:16:17 +03:00
self.column_tag=Gtk.TreeViewColumn("", renderer_text_ralign, text=0)
self.treeview.append_column(self.column_tag)
2020-07-04 14:16:17 +03:00
self.column_value=Gtk.TreeViewColumn("", renderer_text, text=1)
self.treeview.append_column(self.column_value)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
self.store.append(["protocol:", str(self.client.mpd_version)])
2020-01-11 13:25:15 +03:00
stats=self.client.wrapped_call("stats")
2020-07-04 14:16:17 +03:00
for key in stats:
print_key=key+":"
if key == "uptime" or key == "playtime" or key == "db_playtime":
self.store.append([print_key, str(datetime.timedelta(seconds=int(stats[key])))])
elif key == "db_update":
self.store.append([print_key, str(datetime.datetime.fromtimestamp(int(stats[key])))])
else:
self.store.append([print_key, stats[key]])
frame=Gtk.Frame()
frame.add(self.treeview)
self.vbox.pack_start(frame, True, True, 0)
self.vbox.set_spacing(3)
2020-01-11 13:25:15 +03:00
self.show_all()
2020-07-04 14:16:17 +03:00
self.run()
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
class AboutDialog(Gtk.AboutDialog):
def __init__(self, window):
Gtk.AboutDialog.__init__(self, transient_for=window, modal=True)
self.set_program_name(NAME)
self.set_version(VERSION)
self.set_comments(_("A small MPD client written in python"))
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(PACKAGE)
###############
# main window #
###############
2020-07-04 14:16:17 +03:00
class ProfileSelect(Gtk.ComboBoxText):
def __init__(self, client, settings):
Gtk.ComboBoxText.__init__(self)
# adding vars
self.client=client
self.settings=settings
# connect
self.changed=self.connect("changed", self.on_changed)
self.settings.connect("changed::profiles", self.refresh)
self.settings.connect("changed::hosts", self.refresh)
self.settings.connect("changed::ports", self.refresh)
self.settings.connect("changed::passwords", self.refresh)
self.settings.connect("changed::paths", self.refresh)
self.refresh()
2020-03-22 19:05:51 +03:00
def refresh(self, *args):
2020-07-04 14:16:17 +03:00
self.handler_block(self.changed)
self.remove_all()
for profile in self.settings.get_value("profiles"):
self.append_text(profile)
self.set_active(self.settings.get_int("active-profile"))
self.handler_unblock(self.changed)
2020-01-11 13:25:15 +03:00
2020-07-04 14:16:17 +03:00
def on_changed(self, *args):
active=self.get_active()
self.settings.set_int("active-profile", active)
2020-01-11 13:25:15 +03:00
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):
2020-01-11 13:25:15 +03:00
Gtk.ApplicationWindow.__init__(self, title=("mpdevil"), application=app)
2020-03-03 17:59:18 +03:00
Notify.init("mpdevil")
2020-01-11 13:25:15 +03:00
self.set_icon_name("mpdevil")
2020-04-09 01:26:21 +03:00
self.settings=settings
2020-01-11 13:25:15 +03:00
self.set_default_size(self.settings.get_int("width"), self.settings.get_int("height"))
2020-07-04 13:35:39 +03:00
# adding vars
self.app=app
2020-01-11 13:25:15 +03:00
self.client=client
self.use_csd=self.settings.get_boolean("use-csd")
2020-06-27 17:11:41 +03:00
if self.use_csd:
self.icon_size=0
else:
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
# MPRIS
2020-03-21 00:09:13 +03:00
DBusGMainLoop(set_as_default=True)
2020-04-09 01:26:21 +03:00
self.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-04-09 01:26:21 +03:00
save_action=Gio.SimpleAction.new("save", None)
2020-01-11 13:25:15 +03:00
save_action.connect("activate", self.on_save)
self.add_action(save_action)
2020-04-09 01:26:21 +03:00
settings_action=Gio.SimpleAction.new("settings", None)
2020-01-11 13:25:15 +03:00
settings_action.connect("activate", self.on_settings)
self.add_action(settings_action)
2020-04-09 01:26:21 +03:00
stats_action=Gio.SimpleAction.new("stats", None)
2020-01-11 13:25:15 +03:00
stats_action.connect("activate", self.on_stats)
self.add_action(stats_action)
2020-05-16 11:58:13 +03:00
self.update_action=Gio.SimpleAction.new("update", None)
self.update_action.connect("activate", self.on_update)
self.add_action(self.update_action)
2020-01-11 13:25:15 +03:00
2020-05-26 23:53:59 +03:00
self.help_action=Gio.SimpleAction.new("help", None)
self.help_action.connect("activate", self.on_help)
self.add_action(self.help_action)
2020-07-04 13:35:39 +03:00
# widgets
2020-06-27 17:11:41 +03:00
self.icons={}
icons_data=["open-menu-symbolic"]
for data in icons_data:
self.icons[data]=PixelSizedIcon(data, self.icon_size)
2020-03-30 12:54:04 +03:00
self.browser=Browser(self.client, self.settings, self)
self.cover_playlist_view=CoverPlaylistView(self.client, self.settings, self)
2020-03-30 12:54:04 +03:00
self.profiles=ProfileSelect(self.client, self.settings)
2020-01-11 13:25:15 +03:00
self.profiles.set_tooltip_text(_("Select profile"))
2020-03-30 12:54:04 +03:00
self.control=ClientControl(self.client, self.settings)
self.progress=SeekBar(self.client)
self.play_opts=PlaybackOptions(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")
subsection.append(_("Help"), "win.help")
subsection.append(_("About"), "app.about")
subsection.append(_("Quit"), "app.quit")
2020-04-09 01:26:21 +03:00
menu=Gio.Menu()
2020-03-04 18:39:59 +03:00
menu.append(_("Save window layout"), "win.save")
2020-01-11 13:25:15 +03:00
menu.append(_("Update database"), "win.update")
menu.append(_("Server stats"), "win.stats")
2020-05-28 23:46:38 +03:00
menu.append_section(None, subsection)
2020-01-11 13:25:15 +03:00
2020-04-09 01:26:21 +03:00
menu_button=Gtk.MenuButton.new()
menu_popover=Gtk.Popover.new_from_model(menu_button, menu)
2020-01-11 13:25:15 +03:00
menu_button.set_popover(menu_popover)
2020-03-22 23:49:55 +03:00
menu_button.set_tooltip_text(_("Menu"))
2020-06-27 17:11:41 +03:00
menu_button.set_image(image=self.icons["open-menu-symbolic"])
2020-01-11 13:25:15 +03:00
2020-07-04 13:35:39 +03:00
# connect
2020-02-07 22:13:38 +03:00
self.settings.connect("changed::profiles", self.on_settings_changed)
self.settings.connect("changed::playlist-right", self.on_playlist_pos_settings_changed)
2020-06-27 17:11:41 +03:00
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)
2020-03-30 12:54:04 +03:00
self.client.emitter.connect("disconnected", self.on_disconnected)
self.client.emitter.connect("reconnected", self.on_reconnected)
2020-07-04 13:35:39 +03:00
# unmap space
2020-01-11 13:25:15 +03:00
binding_set=Gtk.binding_set_find('GtkTreeView')
Gtk.binding_entry_remove(binding_set, 32, Gdk.ModifierType.MOD2_MASK)
2020-07-04 13:35:39 +03:00
# map space play/pause
2020-01-11 13:25:15 +03:00
self.connect("key-press-event", self.on_key_press_event)
2020-07-04 13:35:39 +03:00
# packing
self.paned2=Gtk.Paned()
self.paned2.set_position(self.settings.get_int("paned2"))
2020-07-04 13:35:39 +03:00
self.on_playlist_pos_settings_changed() # set orientation
self.paned2.pack1(self.browser, True, False)
self.paned2.pack2(self.cover_playlist_view, False, False)
2020-03-24 18:14:01 +03:00
self.vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.action_bar=Gtk.ActionBar()
self.vbox.pack_start(self.paned2, True, True, 0)
2020-03-24 18:14:01 +03:00
self.vbox.pack_start(self.action_bar, False, False, 0)
self.action_bar.pack_start(self.control)
self.action_bar.pack_start(self.progress)
2020-05-27 18:36:49 +03:00
self.action_bar.pack_start(self.play_opts)
if self.use_csd:
self.header_bar=Gtk.HeaderBar()
self.header_bar.set_show_close_button(True)
self.header_bar.set_title("mpdevil")
self.set_titlebar(self.header_bar)
self.header_bar.pack_start(self.browser.back_to_album_button)
self.header_bar.pack_start(self.browser.genre_select)
self.header_bar.pack_end(menu_button)
self.header_bar.pack_end(self.profiles)
self.header_bar.pack_end(self.browser.search_button)
else:
2020-05-27 18:36:49 +03:00
self.action_bar.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.VERTICAL))
self.action_bar.pack_start(self.profiles)
2020-05-27 18:36:49 +03:00
self.action_bar.pack_start(menu_button)
2020-01-11 13:25:15 +03:00
self.add(self.vbox)
2020-01-27 00:38:06 +03:00
2020-01-19 02:00:40 +03:00
self.show_all()
2020-05-21 22:33:39 +03:00
if self.settings.get_boolean("maximize"):
self.maximize()
2020-07-04 13:35:39 +03:00
self.on_settings_changed() # hide profiles button
self.client.start() # connect client
2020-01-11 13:25:15 +03:00
def on_song_changed(self, *args):
2020-07-15 23:39:26 +03:00
song=self.client.wrapped_call("currentsong")
if song == {}:
if self.use_csd:
self.header_bar.set_title("mpdevil")
self.header_bar.set_subtitle("")
else:
self.set_title("mpdevil")
else:
song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(song))
if song["date"] != "":
date=" ("+song["date"]+")"
else:
date=""
if self.use_csd:
self.header_bar.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)
2020-03-22 16:25:04 +03:00
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)
2020-03-22 16:25:04 +03:00
pixbuf=Cover(lib_path=self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file=song["file"]).get_pixbuf(400)
notify.set_image_from_pixbuf(pixbuf)
notify.show()
def on_reconnected(self, *args):
2020-03-21 00:09:13 +03:00
self.dbus_service.acquire_name()
self.progress.set_sensitive(True)
self.control.set_sensitive(True)
self.play_opts.set_sensitive(True)
2020-03-29 22:34:51 +03:00
self.browser.back_to_album()
def on_disconnected(self, *args):
2020-03-21 00:09:13 +03:00
self.dbus_service.release_name()
if self.use_csd:
self.header_bar.set_title("mpdevil")
self.header_bar.set_subtitle("(not connected)")
else:
self.set_title("mpdevil (not connected)")
self.songid_playing=None
self.progress.set_sensitive(False)
self.control.set_sensitive(False)
self.play_opts.set_sensitive(False)
2020-01-11 13:25:15 +03:00
def on_key_press_event(self, widget, event):
ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK)
if ctrl:
2020-07-04 13:35:39 +03:00
if event.keyval == 108: # ctrl + l
self.cover_playlist_view.show_lyrics()
else:
2020-07-04 13:35:39 +03:00
if event.keyval == 32: # space
if not self.browser.search_started():
self.control.play_button.grab_focus()
2020-07-04 13:35:39 +03:00
elif event.keyval == 269025044: # AudioPlay
2020-05-17 23:52:10 +03:00
self.control.play_button.grab_focus()
self.control.play_button.emit("clicked")
2020-07-04 13:35:39 +03:00
elif event.keyval == 269025047: # AudioNext
2020-05-19 17:50:20 +03:00
self.control.next_button.grab_focus()
self.control.next_button.emit("clicked")
2020-07-04 13:35:39 +03:00
elif event.keyval == 43 or event.keyval == 65451: # +
if not self.browser.search_started():
self.control.next_button.grab_focus()
self.control.next_button.emit("clicked")
2020-07-04 13:35:39 +03:00
elif event.keyval == 269025046: # AudioPrev
2020-05-19 17:50:20 +03:00
self.control.prev_button.grab_focus()
self.control.prev_button.emit("clicked")
2020-07-04 13:35:39 +03:00
elif event.keyval == 45 or event.keyval == 65453: # -
if not self.browser.search_started():
self.control.prev_button.grab_focus()
self.control.prev_button.emit("clicked")
2020-07-04 13:35:39 +03:00
elif event.keyval == 65307: # esc
self.browser.back_to_album()
2020-07-04 13:35:39 +03:00
elif event.keyval == 65450: # *
if not self.browser.search_started():
self.progress.scale.grab_focus()
self.progress.seek_forward()
2020-07-04 13:35:39 +03:00
elif event.keyval == 65455: # /
if not self.browser.search_started():
self.progress.scale.grab_focus()
self.progress.seek_backward()
2020-07-04 13:35:39 +03:00
elif event.keyval == 65474: # F5
self.update_action.emit("activate", None)
2020-07-04 13:35:39 +03:00
elif event.keyval == 65470: # F1
self.help_action.emit("activate", None)
2020-01-11 13:25:15 +03:00
def on_save(self, action, param):
size=self.get_size()
self.settings.set_int("width", size[0])
self.settings.set_int("height", size[1])
2020-05-21 22:33:39 +03:00
self.settings.set_boolean("maximize", self.is_maximized())
2020-01-11 13:25:15 +03:00
self.browser.save_settings()
self.cover_playlist_view.save_settings()
self.settings.set_int("paned2", self.paned2.get_position())
2020-01-11 13:25:15 +03:00
def on_settings(self, action, param):
2020-04-09 01:26:21 +03:00
settings=SettingsDialog(self, self.settings)
2020-01-11 13:25:15 +03:00
settings.run()
settings.destroy()
def on_stats(self, action, param):
if self.client.connected():
2020-04-09 01:26:21 +03:00
stats=ServerStats(self, self.client)
2020-01-11 13:25:15 +03:00
stats.destroy()
def on_update(self, action, param):
if self.client.connected():
self.client.wrapped_call("update")
2020-01-11 13:25:15 +03:00
2020-05-26 23:53:59 +03:00
def on_help(self, action, param):
Gtk.show_uri_on_window(self, "https://github.com/SoongNoonien/mpdevil/wiki/Usage", Gdk.CURRENT_TIME)
2020-02-07 22:13:38 +03:00
def on_settings_changed(self, *args):
if len(self.settings.get_value("profiles")) > 1:
2020-03-24 18:14:01 +03:00
self.profiles.set_property("visible", True)
else:
self.profiles.set_property("visible", False)
2020-02-07 22:13:38 +03:00
def on_playlist_pos_settings_changed(self, *args):
if self.settings.get_boolean("playlist-right"):
self.cover_playlist_view.set_orientation(Gtk.Orientation.VERTICAL)
self.paned2.set_orientation(Gtk.Orientation.HORIZONTAL)
else:
self.cover_playlist_view.set_orientation(Gtk.Orientation.HORIZONTAL)
self.paned2.set_orientation(Gtk.Orientation.VERTICAL)
2020-06-27 17:11:41 +03:00
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)
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)
2020-04-09 01:26:21 +03:00
self.settings=Settings()
2020-01-27 00:23:21 +03:00
self.client=Client(self.settings)
2020-01-11 13:25:15 +03:00
self.window=None
def do_activate(self):
2020-07-04 13:35:39 +03:00
if not self.window: # allow just one instance
2020-04-09 01:26:21 +03:00
self.window=MainWindow(self, self.client, self.settings)
2020-01-19 02:00:40 +03:00
self.window.connect("delete-event", self.on_delete_event)
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)
2020-01-11 13:25:15 +03:00
action.connect("activate", self.on_about)
self.add_action(action)
2020-04-09 01:26:21 +03:00
action=Gio.SimpleAction.new("quit", None)
2020-01-11 13:25:15 +03:00
action.connect("activate", self.on_quit)
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):
2020-07-04 14:16:17 +03:00
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()
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)