19 Commits

Author SHA1 Message Date
Martin Wagner
70ca48b7cc preparations for 0.9.7 2020-12-13 19:28:53 +01:00
Martin Wagner
b407f0466d added missing return 2020-12-13 13:44:25 +01:00
Martin Wagner
e6c292f658 removed unneeded stop_flag check in AlbumWindow._refresh() 2020-12-13 11:53:16 +01:00
Martin Wagner
ebe73cc4ea updated readme 2020-12-12 13:30:12 +01:00
Martin Wagner
9e4068f399 unified logic of button and key press handling 2020-12-12 13:04:47 +01:00
Martin Wagner
fd822aad94 changed FocusFrame design 2020-12-10 22:39:46 +01:00
Martin Wagner
4257369486 fixed css priority 2020-12-09 19:12:12 +01:00
Martin Wagner
d2fc1aff18 increased max height of SongPopover 2020-12-08 20:03:40 +01:00
Martin Wagner
79e7ea5973 fixed height allocation of AlbumPopover 2020-12-08 18:27:49 +01:00
Martin Wagner
33e57b9e7f fixed max size of SongPopover and AlbumPopover 2020-12-06 22:37:48 +01:00
Martin Wagner
58a7385186 added missing comment 2020-12-06 15:41:54 +01:00
Martin Wagner
d60d600df2 small fixes in SongPopover and AlbumPopover 2020-12-06 15:36:49 +01:00
Martin Wagner
090c7c814c made album loading faster (~40% less time) 2020-12-06 14:11:40 +01:00
Martin Wagner
b76631e98a improved key and mouse button bindings in AlbumWindow and SongsView 2020-12-06 12:03:49 +01:00
Martin Wagner
2633c841cd replaced AlbumDialog with AlbumPopover 2020-12-06 00:45:25 +01:00
Martin Wagner
e8d19066fb removed unneeded event emit 2020-12-05 14:02:14 +01:00
Martin Wagner
7e7d6c673f fixed cover loading error when no music dir is specified 2020-11-20 21:39:16 +01:00
Martin Wagner
2cd18d931f enabled search in playlist 2020-11-16 18:22:23 +01:00
Martin Wagner
52de223b0f do not hang on GLib error see: #14 2020-11-13 19:06:01 +01:00
3 changed files with 308 additions and 296 deletions

View File

@@ -1,21 +1,23 @@
README for mpdevil
==================
mpdevil is focused on playing your local music directly instead of managing playlists or playing network streams. So it neither supports saving playlists nor restoring them. Therefore mpdevil is mainly a music browser which aims to be easy to use. mpdevil dosen't store any client side database of your music library. Instead all tags and covers get presented to you in real time. So you'll never see any outdated information in your browser. mpdevil strongly relies on tags.
Mpdevil is focused on playing your local music directly instead of managing playlists or playing network streams. So it neither supports saving playlists nor restoring them. Therefore mpdevil is mainly a simple music browser which aims to be easy to use. Instead of maintaining a client side database of your music library mpdevil loads all tags and covers on demand. So you'll never see any outdated information in your browser. Mpdevil strongly relies on tags.
![ScreenShot](screenshots/mainwindow_0.9.5.png)
Features
--------
- play songs without doubleclicking
- search songs in your music library
- manage multiple mpd servers
- filter by genre
- control with media keys
- displays covers
- sends notifications on title change
- fetches lyrics from 'www.letras.mus.br'
- display large covers
- play songs without doubleclick
- lyrics from 'www.letras.mus.br'
- MPRIS interface (based on mpDris2)
- notifications on title change
- basic queue manipulation (move and delete single tracks)
- search songs
- filter by genre
- media keys support
- many shortcuts
- manage multiple mpd servers
See: https://github.com/SoongNoonien/mpdevil/wiki/Usage
@@ -74,5 +76,5 @@ sudo update-desktop-database
Translation
-----------
mpdevil is currently available in English, German and Dutch. If you speek another language you can easily translate mpdevil by using `poedit`. Just import `po/mpdevil.pot` from this repo into `poedit`. To test your translation copy the new `.po` file into the `po` directory of your cloned mpdevil repo and proceed as described in the Building section. To get your translation integrated into mpdevil just send me an e-mail or create a pull request. Link to `poedit`: https://poedit.net/
Mpdevil is currently available in English, German and Dutch. If you speek another language you can easily translate mpdevil by using `poedit`. Just import `po/mpdevil.pot` from this repo into `poedit`. To test your translation copy the new `.po` file into the `po` directory of your cloned mpdevil repo and proceed as described in the Building section. To get your translation integrated into mpdevil just send me an e-mail or create a pull request. Link to `poedit`: https://poedit.net/

View File

@@ -40,7 +40,7 @@ import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
VERSION="0.9.6" # sync with setup.py
VERSION="0.9.7" # sync with setup.py
COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$"
@@ -653,6 +653,7 @@ class Settings(Gio.Settings):
BASE_KEY="org.mpdevil"
# temp settings
mini_player=GObject.Property(type=bool, default=False)
cursor_watch=GObject.Property(type=bool, default=False)
def __init__(self):
super().__init__(schema=self.BASE_KEY)
@@ -1287,9 +1288,9 @@ class AboutDialog(Gtk.AboutDialog):
self.set_copyright("\xa9 2020 Martin Wagner")
self.set_logo_icon_name("mpdevil")
#################################
# small general purpose widgets #
#################################
###########################
# general purpose widgets #
###########################
class AutoSizedIcon(Gtk.Image):
def __init__(self, icon_name, settings_key, settings):
@@ -1317,9 +1318,9 @@ class FocusFrame(Gtk.Overlay):
# css
style_context=self._frame.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {border-color: @theme_selected_bg_color; border-width: 2px;}"""
css=b"""* {border-color: @theme_selected_bg_color; border-width: 0px; border-top-width: 3px;}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
style_context.add_provider(provider, 600)
self.add_overlay(self._frame)
self.set_overlay_pass_through(self._frame, True)
@@ -1337,17 +1338,25 @@ class FocusFrame(Gtk.Overlay):
self._widget.connect("focus-out-event", lambda *args: self._frame.hide())
class SongPopover(Gtk.Popover):
def __init__(self, song, relative, x, y):
def __init__(self, song, relative, x, y, offset=26):
super().__init__()
rect=Gdk.Rectangle()
rect.x=x
# Gtk places popovers 26px above the given position for no obvious reasons, so I move them 26px
rect.y=y+26
rect.width = 1
rect.height = 1
# Gtk places popovers in treeviews 26px above the given position for no obvious reasons, so I move them 26px
# This seems to be related to the width/height of the headers in treeviews
rect.y=y+offset
rect.width=1
rect.height=1
self.set_pointing_to(rect)
self.set_relative_to(relative)
# css
style_context=self.get_style_context()
provider=Gtk.CssProvider()
css=b"""* {background-color: @theme_base_color}"""
provider.load_from_data(css)
style_context.add_provider(provider, 600)
# treeview
# (tag, display-value, tooltip)
store=Gtk.ListStore(str, str, str)
@@ -1357,7 +1366,7 @@ class SongPopover(Gtk.Popover):
# columns
renderer_text=Gtk.CellRendererText(width_chars=50, ellipsize=Pango.EllipsizeMode.MIDDLE, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0, weight=Pango.Weight.BOLD)
column_tag=Gtk.TreeViewColumn(_("MPD-Tag"), renderer_text_ralign, text=0)
column_tag.set_property("resizable", False)
treeview.append_column(column_tag)
@@ -1378,53 +1387,15 @@ class SongPopover(Gtk.Popover):
store.append([tag+":", value, tooltip])
# packing
frame=Gtk.Frame(border_width=3)
frame.add(treeview)
self.add(frame)
scroll=Gtk.ScrolledWindow(border_width=3)
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
window=self.get_toplevel()
scroll.set_max_content_height(window.get_size()[1]//2)
scroll.set_propagate_natural_height(True)
scroll.add(treeview)
self.add(scroll)
frame.show_all()
class Cover(object):
def __init__(self, settings, raw_song):
self.path=None
song=ClientHelper.song_to_first_str_dict(raw_song)
if song != {}:
song_file=song["file"]
active_profile=settings.get_int("active-profile")
lib_path=settings.get_value("paths")[active_profile]
if lib_path == "":
lib_path=GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_MUSIC)
regex_str=settings.get_value("regex")[active_profile]
if regex_str == "":
regex=re.compile(COVER_REGEX, flags=re.IGNORECASE)
else:
regex_str=regex_str.replace("%AlbumArtist%", song.get("albumartist", ""))
regex_str=regex_str.replace("%Album%", song.get("album", ""))
try:
regex=re.compile(regex_str, flags=re.IGNORECASE)
except:
print("illegal regex:", regex_str)
return
if song_file is not None:
song_dir=os.path.join(lib_path, os.path.dirname(song_file))
if song_dir.endswith(".cue"):
song_dir=os.path.dirname(song_dir) # get actual directory of .cue file
if os.path.exists(song_dir):
for f in os.listdir(song_dir):
if regex.match(f):
self.path=os.path.join(song_dir, f)
break
def get_pixbuf(self, size):
if self.path is None: # fallback needed
self.path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size)
###########
# browser #
###########
scroll.show_all()
class SongsView(Gtk.TreeView):
def __init__(self, client, store, file_column_id):
@@ -1435,6 +1406,7 @@ class SongsView(Gtk.TreeView):
self._client=client
self._store=store
self._file_column_id=file_column_id
self._button_event=(None, None)
# selection
self._selection=self.get_selection()
@@ -1442,7 +1414,8 @@ class SongsView(Gtk.TreeView):
# 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)
self.connect("button-release-event", self._on_button_release_event)
self.connect("key-release-event", self._on_key_release_event)
def clear(self):
self._store.clear()
@@ -1460,49 +1433,45 @@ class SongsView(Gtk.TreeView):
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]], "play")
def _on_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]])
except:
pass
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]], "append")
except:
pass
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
file_name=self._store[path][self._file_column_id]
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, int(event.x), int(event.y))
pop.popup()
except:
pass
path_re=widget.get_path_at_pos(int(event.x), int(event.y))
if path_re is not None:
if event.type == Gdk.EventType.BUTTON_PRESS:
self._button_event=(event.button, path_re[0])
def _on_key_press_event(self, widget, event):
self.handler_block(self._key_press_event)
if event.keyval == Gdk.keyval_from_name("p"):
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
def _on_button_release_event(self, widget, event):
path_re=widget.get_path_at_pos(int(event.x), int(event.y))
if path_re is not None:
path=path_re[0]
if self._button_event == (event.button, path):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]])
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._client.wrapped_call("files_to_playlist", [self._store[path][self._file_column_id]], "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_RELEASE:
song=self._client.wrapped_call("get_metadata", self._store[path][self._file_column_id])
if self.get_property("headers-visible"):
pop=SongPopover(song, widget, int(event.x), int(event.y))
else:
pop=SongPopover(song, widget, int(event.x), int(event.y), offset=0)
pop.popup()
self._button_event=(None, None)
def _on_key_release_event(self, widget, event):
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
if event.keyval == Gdk.keyval_from_name("p"):
self._client.wrapped_call("files_to_playlist", [self._store.get_value(treeiter, self._file_column_id)])
elif event.keyval == Gdk.keyval_from_name("a"):
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
elif event.keyval == Gdk.keyval_from_name("a"):
self._client.wrapped_call("files_to_playlist", [self._store.get_value(treeiter, self._file_column_id)], "append")
elif event.keyval == Gdk.keyval_from_name("Menu"):
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
elif event.keyval == Gdk.keyval_from_name("Menu"):
path=self._store.get_path(treeiter)
cell=self.get_cell_area(path, None)
file_name=self._store[path][self._file_column_id]
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, int(cell.x), int(cell.y))
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, cell.x, cell.y)
pop.popup()
self.handler_unblock(self._key_press_event)
class SongsWindow(Gtk.Box):
def __init__(self, client, store, file_column_id):
def __init__(self, client, store, file_column_id, focus_indicator=True):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
# adding vars
@@ -1512,9 +1481,9 @@ class SongsWindow(Gtk.Box):
self._songs_view=SongsView(client, store, file_column_id)
# scroll
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self._songs_view)
self._scroll=Gtk.ScrolledWindow()
self._scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self._scroll.add(self._songs_view)
# buttons
append_button=Gtk.Button.new_with_mnemonic(_("_Append"))
@@ -1539,10 +1508,13 @@ class SongsWindow(Gtk.Box):
enqueue_button.connect("clicked", self._on_enqueue_button_clicked)
# packing
frame=FocusFrame()
frame.set_widget(self._songs_view)
frame.add(scroll)
self.pack_start(frame, True, True, 0)
if focus_indicator:
frame=FocusFrame()
frame.set_widget(self._songs_view)
frame.add(self._scroll)
self.pack_start(frame, True, True, 0)
else:
self.pack_start(self._scroll, True, True, 0)
button_box.pack_start(append_button, True, True, 0)
button_box.pack_start(play_button, True, True, 0)
button_box.pack_start(enqueue_button, True, True, 0)
@@ -1555,6 +1527,9 @@ class SongsWindow(Gtk.Box):
def get_action_bar(self):
return self._action_bar
def get_scroll(self):
return self._scroll
def _on_append_button_clicked(self, *args):
self._client.wrapped_call("files_to_playlist", self._songs_view.get_files(), "append")
@@ -1564,6 +1539,123 @@ class SongsWindow(Gtk.Box):
def _on_enqueue_button_clicked(self, *args):
self._client.wrapped_call("files_to_playlist", self._songs_view.get_files(), "enqueue")
class AlbumPopover(Gtk.Popover):
def __init__(self, client, settings, album, album_artist, year, widget, x, y):
super().__init__()
rect=Gdk.Rectangle()
rect.x=x
rect.y=y
rect.width=1
rect.height=1
self.set_pointing_to(rect)
self.set_relative_to(widget)
# adding vars
self._client=client
songs=self._client.wrapped_call("find", "album", album, "date", year, settings.get_artist_type(), album_artist)
# store
# (track, title (artist), duration, file)
store=Gtk.ListStore(str, str, str, str)
for s in songs:
song=ClientHelper.song_to_list_dict(ClientHelper.pepare_song_for_display(s))
track=song["track"][0]
title=(", ".join(song["title"]))
# only show artists =/= albumartist
try:
song["artist"].remove(album_artist)
except:
pass
artist=(", ".join(song["artist"]))
if artist == album_artist or artist == "":
title_artist="<b>{}</b>".format(title)
else:
title_artist="<b>{}</b> - {}".format(title, artist)
title_artist=title_artist.replace("&", "&amp;")
store.append([track, title_artist, song["human_duration"][0], song["file"][0]])
# songs window
songs_window=SongsWindow(self._client, store, 3, focus_indicator=False)
# scroll
scroll=songs_window.get_scroll()
scroll.set_max_content_height(4*widget.get_allocated_height()//7)
scroll.set_propagate_natural_height(True)
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scroll.set_property("margin-start", 3)
scroll.set_property("margin-end", 3)
scroll.set_property("margin-top", 3)
# songs view
songs_view=songs_window.get_treeview()
songs_view.set_property("headers_visible", False)
# columns
renderer_text=Gtk.CellRendererText(width_chars=80, ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
column_track.set_property("resizable", False)
songs_view.append_column(column_track)
column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1)
column_title.set_property("resizable", False)
column_title.set_property("expand", True)
songs_view.append_column(column_title)
column_time=Gtk.TreeViewColumn(_("Length"), renderer_text_ralign, text=2)
column_time.set_property("resizable", False)
songs_view.append_column(column_time)
# packing
self.add(songs_window)
songs_window.show_all()
class Cover(object):
def __init__(self, settings, raw_song):
self.path=None
song=ClientHelper.song_to_first_str_dict(raw_song)
if song != {}:
song_file=song["file"]
active_profile=settings.get_int("active-profile")
lib_path=settings.get_value("paths")[active_profile]
if lib_path == "":
lib_path=GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_MUSIC)
if lib_path is not None:
regex_str=settings.get_value("regex")[active_profile]
if regex_str == "":
regex=re.compile(COVER_REGEX, flags=re.IGNORECASE)
else:
regex_str=regex_str.replace("%AlbumArtist%", song.get("albumartist", ""))
regex_str=regex_str.replace("%Album%", song.get("album", ""))
try:
regex=re.compile(regex_str, flags=re.IGNORECASE)
except:
print("illegal regex:", regex_str)
return
if song_file is not None:
song_dir=os.path.join(lib_path, os.path.dirname(song_file))
if song_dir.endswith(".cue"):
song_dir=os.path.dirname(song_dir) # get actual directory of .cue file
if os.path.exists(song_dir):
for f in os.listdir(song_dir):
if regex.match(f):
self.path=os.path.join(song_dir, f)
break
def get_pixbuf(self, size):
if self.path is None: # fallback needed
path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
return GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
else:
try:
return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size)
except: # load fallback if cover can't be loaded (GLib: Couldnt recognize the image file format for file...)
path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
return GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
###########
# browser #
###########
class SearchWindow(Gtk.Box):
def __init__(self, client):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
@@ -1841,7 +1933,6 @@ class ArtistWindow(FocusFrame):
except:
self._store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK])
self._selection.set_mode(Gtk.SelectionMode.SINGLE)
self.emit("artists_changed")
def _on_row_activated(self, widget, path, view_column):
for row in self._store: # reset bold text
@@ -1860,112 +1951,14 @@ class ArtistWindow(FocusFrame):
def _on_show_initials_changed(self, *args):
self._column_initials.set_visible(self._settings.get_boolean("show-initials"))
class AlbumDialog(Gtk.Dialog): # also used by 'MainCover'
def __init__(self, parent, client, settings, album, album_artist, year):
use_csd=settings.get_boolean("use-csd")
if use_csd:
super().__init__(transient_for=parent, use_header_bar=True)
else:
super().__init__(transient_for=parent)
# adding vars
self._client=client
self._settings=settings
songs=self._client.wrapped_call("find", "album", album, "date", year, self._settings.get_artist_type(), album_artist)
# determine size
size=parent.get_size()
diagonal=(size[0]**2+size[1]**2)**(0.5)
h=diagonal//4
w=h*5//4
self.set_default_size(w, h)
# title
duration=ClientHelper.calc_display_length(songs)
if use_csd:
if year == "":
self.set_title(album)
else:
self.set_title("{} ({})".format(album, year))
header_bar=self.get_header_bar()
header_bar.set_subtitle("{} ({})".format(album_artist, duration))
else:
if year == "":
self.set_title("{} - {} ({})".format(album, album_artist, duration))
else:
self.set_title("{} ({}) - {} ({})".format(album, year, album_artist, duration))
# store
# (track, title (artist), duration, file)
store=Gtk.ListStore(str, str, str, str)
for s in songs:
song=ClientHelper.song_to_list_dict(ClientHelper.pepare_song_for_display(s))
track=song["track"][0]
title=(", ".join(song["title"]))
# only show artists =/= albumartist
try:
song["artist"].remove(album_artist)
except:
pass
artist=(", ".join(song["artist"]))
if artist == album_artist or artist == "":
title_artist="<b>{}</b>".format(title)
else:
title_artist="<b>{}</b> - {}".format(title, artist)
title_artist=title_artist.replace("&", "&amp;")
store.append([track, title_artist, song["human_duration"][0], song["file"][0]])
# songs window
songs_window=SongsWindow(self._client, store, 3)
# songs view
songs_view=songs_window.get_treeview()
# columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
column_track.set_property("resizable", False)
songs_view.append_column(column_track)
column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1)
column_title.set_property("resizable", False)
column_title.set_property("expand", True)
songs_view.append_column(column_title)
column_time=Gtk.TreeViewColumn(_("Length"), renderer_text_ralign, text=2)
column_time.set_property("resizable", False)
songs_view.append_column(column_time)
# close button
close_button=Gtk.ToggleButton(image=Gtk.Image.new_from_icon_name("window-close", Gtk.IconSize.BUTTON), label=_("Close"))
# action bar
action_bar=songs_window.get_action_bar()
action_bar.pack_end(close_button)
# connect
close_button.connect("clicked", self._on_close_button_clicked)
# packing
vbox=self.get_content_area()
vbox.set_property("border-width", 0)
vbox.pack_start(songs_window, True, True, 0)
self.show_all()
def open(self):
response=self.run()
def _on_close_button_clicked(self, *args):
self.destroy()
class AlbumWindow(FocusFrame):
def __init__(self, client, settings, artist_window, window):
def __init__(self, client, settings, artist_window):
super().__init__()
# adding vars
self._settings=settings
self._client=client
self._artist_window=artist_window
self._window=window
self._button_event=(None, None)
self.stop_flag=False
self._done=True
@@ -1983,12 +1976,14 @@ class AlbumWindow(FocusFrame):
scroll=Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self._iconview)
self._scroll_vadj=scroll.get_vadjustment()
self._scroll_hadj=scroll.get_hadjustment()
# connect
self._iconview.connect("item-activated", self._on_item_activated)
self._iconview.connect("button-release-event", self._on_button_release_event)
self._iconview.connect("button-press-event", self._on_button_press_event)
self._key_press_event=self.connect("key-press-event", self._on_key_press_event)
self._iconview.connect("button-release-event", self._on_button_release_event)
self._iconview.connect("key-release-event", self._on_key_release_event)
self._client.emitter.connect("update", self._clear)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
@@ -2050,12 +2045,16 @@ class AlbumWindow(FocusFrame):
return False # stop after one run
def _refresh(self, *args):
def callback():
GLib.idle_add(self._workaround_clear)
try: # self._artist_window could still be empty
if self._done:
self._done=False
self._settings.set_property("cursor-watch", True)
GLib.idle_add(self._store.clear)
self._iconview.set_model(None)
try: # self._artist_window can still be empty (e.g. when client is not connected and cover size gets changed)
genre, artists=self._artist_window.get_selected_artists()
except:
GLib.idle_add(self._done_callback)
return
# show artist names if all albums are shown
if len(artists) > 1:
self._iconview.set_markup_column(2)
@@ -2088,17 +2087,9 @@ class AlbumWindow(FocusFrame):
except MPDBase.ConnectionError:
GLib.idle_add(self._done_callback)
return
# display albums
if self._settings.get_boolean("sort-albums-by-year"):
albums=sorted(albums, key=lambda k: k["year"])
else:
albums=sorted(albums, key=lambda k: k["album"])
size=self._settings.get_int("album-cover")
for i, album in enumerate(albums):
if self.stop_flag:
break
else:
cover=Cover(self._settings, album["songs"][0]).get_pixbuf(size)
def display_albums():
for i, album in enumerate(albums):
# tooltip
length_human_readable=ClientHelper.calc_display_length(album["songs"])
discs=album["songs"][-1].get("disc", 1)
@@ -2121,17 +2112,27 @@ class AlbumWindow(FocusFrame):
display_label=display_label.replace("&", "&amp;")
display_label_artist=display_label_artist.replace("&", "&amp;")
# add album
GLib.idle_add(self._add_row,
[cover, display_label, display_label_artist, tooltip, album["album"], album["year"], album["artist"]]
self._store.append(
[album["cover"], display_label, display_label_artist,
tooltip, album["album"], album["year"], album["artist"]]
)
# execute pending events
if i%16 == 0:
while Gtk.events_pending():
Gtk.main_iteration_do(True)
GLib.idle_add(self._done_callback)
if self._done:
self._done=False
callback()
self._iconview.set_model(self._store)
GLib.idle_add(self._done_callback)
return False
def load_covers():
size=self._settings.get_int("album-cover")
for album in albums:
if self.stop_flag:
break
album["cover"]=Cover(self._settings, album["songs"][0]).get_pixbuf(size)
if self.stop_flag:
GLib.idle_add(self._done_callback)
else:
GLib.idle_add(display_albums)
cover_thread=threading.Thread(target=load_covers, daemon=True)
cover_thread.start()
elif not self._refresh in self._pending:
self.stop_flag=True
self._pending.append(self._refresh)
@@ -2142,16 +2143,8 @@ class AlbumWindow(FocusFrame):
artist=self._store[path][6]
self._client.wrapped_call("album_to_playlist", album, artist, year, mode)
def _open_album_dialog(self, path):
if self._client.connected():
album=self._store[path][4]
year=self._store[path][5]
artist=self._store[path][6]
album_dialog=AlbumDialog(self._window, self._client, self._settings, album, artist, year)
album_dialog.open()
album_dialog.destroy()
def _done_callback(self, *args):
self._settings.set_property("cursor-watch", False)
self.stop_flag=False
self._done=True
pending=self._pending
@@ -2177,23 +2170,31 @@ class AlbumWindow(FocusFrame):
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._path_to_playlist(path, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._open_album_dialog(path)
album=self._store[path][4]
year=self._store[path][5]
artist=self._store[path][6]
v=self._scroll_vadj.get_value()
h=self._scroll_hadj.get_value()
pop=AlbumPopover(self._client, self._settings, album, artist, year, widget, int(event.x-h), int(event.y-v))
pop.popup()
self._button_event=(None, None)
def _on_key_press_event(self, widget, event):
self.handler_block(self._key_press_event)
if event.keyval == Gdk.keyval_from_name("p"):
paths=self._iconview.get_selected_items()
if len(paths) != 0:
def _on_key_release_event(self, widget, event):
paths=widget.get_selected_items()
if len(paths) != 0:
if event.keyval == Gdk.keyval_from_name("p"):
self._path_to_playlist(paths[0])
elif event.keyval == Gdk.keyval_from_name("a"):
paths=self._iconview.get_selected_items()
if len(paths) != 0:
elif event.keyval == Gdk.keyval_from_name("a"):
self._path_to_playlist(paths[0], "append")
elif event.keyval == Gdk.keyval_from_name("Menu"):
paths=self._iconview.get_selected_items()
if len(paths) != 0:
self._open_album_dialog(paths[0])
self.handler_unblock(self._key_press_event)
elif event.keyval == Gdk.keyval_from_name("Menu"):
album=self._store[paths[0]][4]
year=self._store[paths[0]][5]
artist=self._store[paths[0]][6]
rect=widget.get_cell_rect(paths[0], None)[1]
x=rect.x+rect.width//2
y=rect.y+rect.height//2
pop=AlbumPopover(self._client, self._settings, album, artist, year, widget, x, y)
pop.popup()
def _on_item_activated(self, widget, path):
treeiter=self._store.get_iter(path)
@@ -2218,7 +2219,7 @@ class AlbumWindow(FocusFrame):
class Browser(Gtk.Paned):
__gsignals__={"search_focus_changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,))}
def __init__(self, client, settings, window):
def __init__(self, client, settings):
super().__init__(orientation=Gtk.Orientation.HORIZONTAL) # paned1
# adding vars
@@ -2243,7 +2244,7 @@ class Browser(Gtk.Paned):
self._genre_select=GenreSelect(self._client)
self._artist_window=ArtistWindow(self._client, self._settings, self._genre_select)
self._search_window=SearchWindow(self._client)
self._album_window=AlbumWindow(self._client, self._settings, self._artist_window, window)
self._album_window=AlbumWindow(self._client, self._settings, self._artist_window)
# connect
self.back_to_current_album_button.connect("clicked", self._back_to_current_album)
@@ -2499,13 +2500,12 @@ class AudioType(Gtk.Label):
self.clear()
class CoverEventBox(Gtk.EventBox):
def __init__(self, client, settings, window):
def __init__(self, client, settings):
super().__init__()
# adding vars
self._client=client
self._settings=settings
self._window=window
# connect
self._button_press_event=self.connect("button-press-event", self._on_button_press_event)
@@ -2526,9 +2526,8 @@ class CoverEventBox(Gtk.EventBox):
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
self._client.wrapped_call("album_to_playlist", album, artist, album_year, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
album_dialog=AlbumDialog(self._window, self._client, self._settings, album, artist, album_year)
album_dialog.open()
album_dialog.destroy()
pop=AlbumPopover(self._client, self._settings, album, artist, album_year, widget, int(event.x), int(event.y))
pop.popup()
def _on_mini_player(self, obj, typestring):
if obj.get_property("mini-player"):
@@ -2545,7 +2544,7 @@ class MainCover(Gtk.Frame):
provider=Gtk.CssProvider()
css=b"""* {background-color: @theme_base_color; border-width: 0px}"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
style_context.add_provider(provider, 600)
# adding vars
self._client=client
@@ -2593,6 +2592,7 @@ class PlaylistWindow(Gtk.Box):
self._playlist_version=None
self._icon_size=self._settings.get_int("icon-size-sec")
self._inserted_path=None # needed for drag and drop
self._button_event=(None, None)
# buttons
provider=Gtk.CssProvider()
@@ -2606,7 +2606,7 @@ class PlaylistWindow(Gtk.Box):
)
self._back_to_current_song_button.set_can_focus(False)
style_context=self._back_to_current_song_button.get_style_context()
style_context.add_provider(provider, 800)
style_context.add_provider(provider, 600)
self._clear_button=Gtk.Button(
image=AutoSizedIcon("edit-clear-symbolic", "icon-size-sec", self._settings),
tooltip_text=_("Clear playlist"),
@@ -2615,12 +2615,12 @@ class PlaylistWindow(Gtk.Box):
self._clear_button.set_can_focus(False)
style_context=self._clear_button.get_style_context()
style_context.add_class("destructive-action")
style_context.add_provider(provider, 800)
style_context.add_provider(provider, 600)
# treeview
# (track, disc, title, artist, album, duration, date, genre, file, weight)
self._store=Gtk.ListStore(str, str, str, str, str, str, str, str, str, Pango.Weight)
self._treeview=Gtk.TreeView(model=self._store, activate_on_single_click=True, reorderable=True, search_column=-1)
self._treeview=Gtk.TreeView(model=self._store, activate_on_single_click=True, reorderable=True, search_column=2)
self._selection=self._treeview.get_selection()
# columns
@@ -2673,8 +2673,9 @@ class PlaylistWindow(Gtk.Box):
# connect
self._treeview.connect("row-activated", self._on_row_activated)
self._key_press_event=self._treeview.connect("key-press-event", self._on_key_press_event)
self._treeview.connect("button-press-event", self._on_button_press_event)
self._treeview.connect("button-release-event", self._on_button_release_event)
self._treeview.connect("key-release-event", self._on_key_release_event)
self._treeview.connect("drag-begin", lambda *args: self._frame.disable())
self._treeview.connect("drag-end", lambda *args: self._frame.enable())
self._row_deleted=self._store.connect("row-deleted", self._on_row_deleted)
@@ -2750,8 +2751,26 @@ class PlaylistWindow(Gtk.Box):
except:
self._selection.unselect_all()
def _on_key_press_event(self, widget, event):
self._treeview.handler_block(self._key_press_event)
def _on_button_press_event(self, widget, event):
path_re=widget.get_path_at_pos(int(event.x), int(event.y))
if path_re is not None:
if event.type == Gdk.EventType.BUTTON_PRESS:
self._button_event=(event.button, path_re[0])
def _on_button_release_event(self, widget, event):
path_re=widget.get_path_at_pos(int(event.x), int(event.y))
if path_re is not None:
path=path_re[0]
if self._button_event == (event.button, path):
if event.button == 2 and event.type == Gdk.EventType.BUTTON_RELEASE:
self._store.remove(self._store.get_iter(path))
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_RELEASE:
song=self._client.wrapped_call("get_metadata", self._store[path][8])
pop=SongPopover(song, widget, int(event.x), int(event.y))
pop.popup()
self._button_event=(None, None)
def _on_key_release_event(self, widget, event):
if event.keyval == Gdk.keyval_from_name("Delete"):
treeview, treeiter=self._selection.get_selected()
if treeiter is not None:
@@ -2767,22 +2786,6 @@ class PlaylistWindow(Gtk.Box):
file_name=self._store[path][8]
pop=SongPopover(self._client.wrapped_call("get_metadata", file_name), widget, int(cell.x), int(cell.y))
pop.popup()
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:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
self._store.remove(self._store.get_iter(path))
except:
pass
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
try:
path=widget.get_path_at_pos(int(event.x), int(event.y))[0]
pop=SongPopover(self._client.wrapped_call("get_metadata", self._store[path][8]), widget, int(event.x), int(event.y))
pop.popup()
except:
pass
def _on_row_deleted(self, model, path): # sync treeview to mpd
if self._inserted_path is not None: # move
@@ -2880,17 +2883,16 @@ class PlaylistWindow(Gtk.Box):
self.set_property("visible", visibility)
class CoverPlaylistWindow(Gtk.Paned):
def __init__(self, client, settings, window):
def __init__(self, client, settings):
super().__init__()
# adding vars
self._client=client
self._settings=settings
self._window=window
# cover
main_cover=MainCover(self._client, self._settings)
self._cover_event_box=CoverEventBox(self._client, self._settings, self._window)
self._cover_event_box=CoverEventBox(self._client, self._settings)
# playlist
self._playlist_window=PlaylistWindow(self._client, self._settings)
@@ -3076,7 +3078,7 @@ class SeekBar(Gtk.Box):
provider=Gtk.CssProvider()
css=b"""scale fill { background-color: @theme_selected_bg_color; }"""
provider.load_from_data(css)
style_context.add_provider(provider, 800)
style_context.add_provider(provider, 600)
# connect
self._elapsed_event_box.connect("button-release-event", self._on_elapsed_button_release_event)
@@ -3557,8 +3559,8 @@ class MainWindow(Gtk.ApplicationWindow):
else:
icons={"open-menu-symbolic": AutoSizedIcon("open-menu-symbolic", "icon-size", self._settings)}
self._browser=Browser(self._client, self._settings, self)
self._cover_playlist_window=CoverPlaylistWindow(self._client, self._settings, self)
self._browser=Browser(self._client, self._settings)
self._cover_playlist_window=CoverPlaylistWindow(self._client, self._settings)
playback_control=PlaybackControl(self._client, self._settings)
seek_bar=SeekBar(self._client)
playback_options=PlaybackOptions(self._client, self._settings)
@@ -3599,6 +3601,7 @@ class MainWindow(Gtk.ApplicationWindow):
self._settings.connect("changed::profiles", self._refresh_profiles_menu)
self._settings.connect("changed::active-profile", self._on_active_profile_changed)
self._settings.connect_after("notify::mini-player", self._on_mini_player)
self._settings.connect_after("notify::cursor-watch", self._on_cursor_watch)
self._settings.connect("changed::playlist-right", self._on_playlist_pos_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
@@ -3746,6 +3749,13 @@ class MainWindow(Gtk.ApplicationWindow):
self._tmp_saved_size=None
self._tmp_saved_maximized=None
def _on_cursor_watch(self, obj, typestring):
if obj.get_property("cursor-watch"):
watch_cursor = Gdk.Cursor(Gdk.CursorType.WATCH)
self.get_window().set_cursor(watch_cursor)
else:
self.get_window().set_cursor(None)
def _on_playlist_pos_changed(self, *args):
if self._settings.get_boolean("playlist-right"):
self._cover_playlist_window.set_orientation(Gtk.Orientation.VERTICAL)

View File

@@ -4,7 +4,7 @@ import DistUtilsExtra.auto
DistUtilsExtra.auto.setup(
name='mpdevil',
version='0.9.6', # sync with bin/mpdevil
version='0.9.7', # sync with bin/mpdevil
author="Martin Wagner",
author_email="martin.wagner.dev@gmail.com",
description=('A simple music browser for MPD'),