diff --git a/README.md b/README.md
index 070cd52..083b151 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ License: [GPL](http://www.gnu.org/licenses/gpl.html) v3
Dependencies:
* [Python](http://www.python.org) 3
+* [python-dateutil](https://pypi.org/project/python-dateutil/)
* [GTK](http://www.gtk.org) 3 (>= 3.22) ([python-gobject](https://live.gnome.org/PyGObject))
* [Avahi](http://www.avahi.org) (optional)
* [python-keyring](http://pypi.python.org/pypi/keyring) (optional)
diff --git a/data/ui/library-panel.ui b/data/ui/library-panel.ui
index c459cd0..3ac79a7 100644
--- a/data/ui/library-panel.ui
+++ b/data/ui/library-panel.ui
@@ -68,7 +68,14 @@
+
+
+
diff --git a/data/xyz.suruatoel.mcg.gschema.xml b/data/xyz.suruatoel.mcg.gschema.xml
index 934564c..9f50878 100644
--- a/data/xyz.suruatoel.mcg.gschema.xml
+++ b/data/xyz.suruatoel.mcg.gschema.xml
@@ -4,6 +4,7 @@
+
diff --git a/meson.build b/meson.build
index d644865..5a6d445 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
project('mcg',
- version: '3.1',
+ version: '3.2.1',
meson_version: '>= 0.59.0',
default_options: [
'warning_level=2',
diff --git a/po/de.mo b/po/de.mo
index da025a5..abfee53 100644
Binary files a/po/de.mo and b/po/de.mo differ
diff --git a/po/de.po b/po/de.po
index 107757f..58ecec7 100644
--- a/po/de.po
+++ b/po/de.po
@@ -2,17 +2,17 @@ msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-09-05 15:08+0200\n"
-"PO-Revision-Date: 2020-10-24 14:41+0200\n"
+"POT-Creation-Date: 2023-01-08 19:06+0100\n"
+"PO-Revision-Date: 2023-01-08 19:07+0100\n"
"Last-Translator: coderkun \n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.4.1\n"
-"X-Poedit-Basepath: ../../..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.2.2\n"
+"X-Poedit-Basepath: ../../..\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: data/xyz.suruatoel.mcg.gschema.xml:11
@@ -216,15 +216,19 @@ msgstr "nach Titel"
msgid "sort by year"
msgstr "nach Jahr"
-#: data/ui/library-toolbar.ui:169 data/ui/shortcuts-dialog.ui:115
+#: data/ui/library-toolbar.ui:134
+msgid "sort by modification"
+msgstr "nach Änderungsdatum"
+
+#: data/ui/library-toolbar.ui:185 data/ui/shortcuts-dialog.ui:115
msgid "Search the library"
msgstr "Die Bibliothek durchsuchen"
-#: data/ui/library-toolbar.ui:192 data/ui/playlist-toolbar.ui:15
+#: data/ui/library-toolbar.ui:208 data/ui/playlist-toolbar.ui:15
msgid "Select multiple albums"
msgstr "Mehrere Alben auswählen"
-#: data/ui/library-toolbar.ui:214
+#: data/ui/library-toolbar.ui:230
msgid "Settings and actions"
msgstr "Einstellungen und Aktionen"
@@ -353,11 +357,11 @@ msgstr "Zu MPD verbinden"
msgid "Adjust the volume"
msgstr "Die Lautstärke anpassen"
-#: src/librarypanel.py:419
+#: src/librarypanel.py:421
msgid "Loading albums"
msgstr "Alben werden geladen"
-#: src/librarypanel.py:519
+#: src/librarypanel.py:521
msgid "Loading images"
msgstr "Bilder werden geladen"
diff --git a/po/en.mo b/po/en.mo
index 39a611b..8fa584c 100644
Binary files a/po/en.mo and b/po/en.mo differ
diff --git a/po/en.po b/po/en.po
index 9d0eba8..ae94e2b 100644
--- a/po/en.po
+++ b/po/en.po
@@ -2,17 +2,17 @@ msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-09-05 15:08+0200\n"
-"PO-Revision-Date: 2020-10-24 14:40+0200\n"
+"POT-Creation-Date: 2023-01-08 19:06+0100\n"
+"PO-Revision-Date: 2023-01-08 19:07+0100\n"
"Last-Translator: coderkun \n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.4.1\n"
-"X-Poedit-Basepath: ../../..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.2.2\n"
+"X-Poedit-Basepath: ../../..\n"
"X-Poedit-SearchPath-0: mcg\n"
"X-Poedit-SearchPath-1: data/ui\n"
@@ -217,15 +217,19 @@ msgstr "by Title"
msgid "sort by year"
msgstr "by Year"
-#: data/ui/library-toolbar.ui:169 data/ui/shortcuts-dialog.ui:115
+#: data/ui/library-toolbar.ui:134
+msgid "sort by modification"
+msgstr "by Modification Date"
+
+#: data/ui/library-toolbar.ui:185 data/ui/shortcuts-dialog.ui:115
msgid "Search the library"
msgstr "Search the library"
-#: data/ui/library-toolbar.ui:192 data/ui/playlist-toolbar.ui:15
+#: data/ui/library-toolbar.ui:208 data/ui/playlist-toolbar.ui:15
msgid "Select multiple albums"
msgstr "Select multiple albums"
-#: data/ui/library-toolbar.ui:214
+#: data/ui/library-toolbar.ui:230
msgid "Settings and actions"
msgstr "Settings and actions"
@@ -354,11 +358,11 @@ msgstr "Connect to MPD"
msgid "Adjust the volume"
msgstr "Adjust the volume"
-#: src/librarypanel.py:419
+#: src/librarypanel.py:421
msgid "Loading albums"
msgstr "Loading albums"
-#: src/librarypanel.py:519
+#: src/librarypanel.py:521
msgid "Loading images"
msgstr "Loading images"
diff --git a/po/mcg.pot b/po/mcg.pot
index 19fd383..c409fd6 100644
--- a/po/mcg.pot
+++ b/po/mcg.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: mcg\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-09-05 15:08+0200\n"
+"POT-Creation-Date: 2023-01-08 19:06+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -215,15 +215,19 @@ msgstr ""
msgid "sort by year"
msgstr ""
-#: data/ui/library-toolbar.ui:169 data/ui/shortcuts-dialog.ui:115
+#: data/ui/library-toolbar.ui:134
+msgid "sort by modification"
+msgstr ""
+
+#: data/ui/library-toolbar.ui:185 data/ui/shortcuts-dialog.ui:115
msgid "Search the library"
msgstr ""
-#: data/ui/library-toolbar.ui:192 data/ui/playlist-toolbar.ui:15
+#: data/ui/library-toolbar.ui:208 data/ui/playlist-toolbar.ui:15
msgid "Select multiple albums"
msgstr ""
-#: data/ui/library-toolbar.ui:214
+#: data/ui/library-toolbar.ui:230
msgid "Settings and actions"
msgstr ""
@@ -352,11 +356,11 @@ msgstr ""
msgid "Adjust the volume"
msgstr ""
-#: src/librarypanel.py:419
+#: src/librarypanel.py:421
msgid "Loading albums"
msgstr ""
-#: src/librarypanel.py:519
+#: src/librarypanel.py:521
msgid "Loading images"
msgstr ""
diff --git a/src/application.py b/src/application.py
index 946ef9a..5665b55 100644
--- a/src/application.py
+++ b/src/application.py
@@ -61,7 +61,7 @@ class Application(Gtk.Application):
self._info_dialog = Adw.AboutDialog()
self._info_dialog.set_application_icon("xyz.suruatoel.mcg")
self._info_dialog.set_application_name("CoverGrid")
- self._info_dialog.set_version("3.1")
+ self._info_dialog.set_version("3.2.1")
self._info_dialog.set_comments("CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.")
self._info_dialog.set_website("https://www.suruatoel.xyz/codes/mcg")
self._info_dialog.set_license_type(Gtk.License.GPL_3_0)
diff --git a/src/client.py b/src/client.py
index ac5efa2..43de37c 100644
--- a/src/client.py
+++ b/src/client.py
@@ -3,6 +3,7 @@
import concurrent.futures
import configparser
+import dateutil.parser
import logging
import os
import queue
@@ -649,57 +650,25 @@ class Client(Base):
def _get_albumart(self, album):
- data = None
if album in self._albums:
album = self._albums[album]
- if not album.get_tracks():
- return None
self._logger.debug("get albumart for album \"%s\"", album.get_title())
- size = 1
- offset = 0
- index = 0
- # Read data until size is reached
- try:
- while offset < size:
- self._write('albumart', args=[album.get_tracks()[0].get_file(), offset])
+ # Use "albumart" command
+ if album.get_tracks():
+ try:
+ return (album, self._read_binary('albumart', album.get_tracks()[0].get_file(), False))
+ except CommandException as e:
+ # The "albumart" command throws an exception if not found
+ if e.get_error_number() != Client.PROTOCOL_ERROR_NOEXISTS:
+ raise e
+ # If no albumart can be found, use "readpicture" command
+ for track in album.get_tracks():
+ data = self._read_binary('readpicture', track.get_file(), True)
+ if data:
+ return (album, data)
- # Read first line which tells us whether there is an albumart
- line = self._read_line()
- if line.startswith(Client.PROTOCOL_ERROR):
- error = line[len(Client.PROTOCOL_ERROR):].strip()
- self._logger.debug("command failed: %r", error)
- raise CommandException(error)
- # First line is the file size
- size = int(self._parse_dict([line])['size'])
- self._logger.debug("size: %d", size)
- # Second line is the count of bytes read
- binary = int(self._parse_dict([self._read_line()])['binary'])
- self._logger.debug("binary: %d", binary)
-
- # Create new data array on the first iteration
- if not data:
- data = bytearray(size)
- # Create a view for the current chunk of data
- data_view = memoryview(data)[offset:offset+binary]
- # Read actual bytes
- self._read_bytes(data_view, binary)
- offset += binary
- # Read line break to complete previous repsonse
- self._read_line()
- # Read command completion
- end = self._read_line()
- if not end.startswith(Client.PROTOCOL_COMPLETION):
- self._logger.debug("albumart not completed")
- data = None
- break
- except CommandException as e:
- # If no albumart can be found, do not throw an exception
- if e.get_error_number() == Client.PROTOCOL_ERROR_NOEXISTS:
- data = None
- else:
- raise e
- return (album, data)
+ return (album, None)
def _get_custom(self, name):
@@ -843,6 +812,55 @@ class Client(Base):
return None
+ def _read_binary(self, command, filename, has_mimetype):
+ data = None
+ size = 1
+ offset = 0
+ index = 0
+
+ # Read data until size is reached
+ while offset < size:
+ self._write(command, args=[filename, offset])
+
+ # Read first line
+ line = self._read_line()
+ # Check first line for error
+ if line.startswith(Client.PROTOCOL_ERROR):
+ error = line[len(Client.PROTOCOL_ERROR):].strip()
+ self._logger.debug("command failed: %r", error)
+ raise CommandException(error)
+ # Check first line for completion
+ if line.startswith(Client.PROTOCOL_COMPLETION):
+ break
+ # First line is the file size
+ size = int(self._parse_dict([line])['size'])
+ self._logger.debug("size: %d", size)
+ # For some commands the second line is the mimetype
+ if has_mimetype:
+ mimetype = self._parse_dict([self._read_line()])['type']
+ # Next line is the count of bytes read
+ binary = int(self._parse_dict([self._read_line()])['binary'])
+ self._logger.debug("binary: %d", binary)
+
+ # Create new data array on the first iteration
+ if not data:
+ data = bytearray(size)
+ # Create a view for the current chunk of data
+ data_view = memoryview(data)[offset:offset+binary]
+ # Read actual bytes
+ self._read_bytes(data_view, binary)
+ offset += binary
+ # Read line break to complete previous repsonse
+ self._read_line()
+ # Read command completion
+ end = self._read_line()
+ if not end.startswith(Client.PROTOCOL_COMPLETION):
+ self._logger.debug("albumart not completed")
+ data = None
+ break
+ return data
+
+
def _read_bytes(self, buf, nbytes):
self._logger.debug("reading bytes")
# Use already buffered data
@@ -937,6 +955,8 @@ class Client(Base):
track.set_date(song['date'])
if 'albumartist' in song:
track.set_albumartists(song['albumartist'])
+ if 'last-modified' in song:
+ track.set_last_modified(song['last-modified'])
return track
@@ -998,6 +1018,7 @@ class MCGAlbum:
self._host = host
self._tracks = []
self._length = 0
+ self._last_modified = None
self._id = Utils.generate_id(title)
@@ -1057,6 +1078,9 @@ class MCGAlbum:
path = os.path.dirname(track.get_file())
if path not in self._pathes:
self._pathes.append(path)
+ if track.get_last_modified():
+ if not self._last_modified or track.get_last_modified() > self._last_modified:
+ self._last_modified = track.get_last_modified()
def get_tracks(self):
@@ -1067,6 +1091,10 @@ class MCGAlbum:
return self._length
+ def get_last_modified(self):
+ return self._last_modified
+
+
def filter(self, filter_string):
if len(filter_string) == 0:
return True
@@ -1102,6 +1130,8 @@ class MCGAlbum:
value_function = "get_title"
elif criterion == SortOrder.YEAR:
value_function = "get_date"
+ elif criterion == SortOrder.MODIFIED:
+ value_function = "get_last_modified"
reverseMultiplier = -1 if reverse else 1
@@ -1139,6 +1169,7 @@ class MCGTrack:
self._track = None
self._length = 0
self._date = None
+ self._last_modified = None
def __eq__(self, other):
@@ -1210,6 +1241,18 @@ class MCGTrack:
return self._file
+ def set_last_modified(self, date_string):
+ if date_string:
+ try:
+ self._last_modified = dateutil.parser.isoparse(date_string)
+ except ValueError as e:
+ self._logger.debug("Invalid date format: %s", date_string)
+
+
+ def get_last_modified(self):
+ return self._last_modified
+
+
class MCGPlaylistTrack(MCGTrack):
diff --git a/src/librarypanel.py b/src/librarypanel.py
index 5201b5c..5feec0c 100644
--- a/src/librarypanel.py
+++ b/src/librarypanel.py
@@ -52,6 +52,7 @@ class LibraryPanel(Adw.Bin):
sort_artist = Gtk.Template.Child()
sort_title = Gtk.Template.Child()
sort_year = Gtk.Template.Child()
+ sort_modified = Gtk.Template.Child()
grid_scale = Gtk.Template.Child()
# Filter/search bar
filter_bar = Gtk.Template.Child()
@@ -111,7 +112,8 @@ class LibraryPanel(Adw.Bin):
self._toolbar_sort_buttons = {
SortOrder.ARTIST: self.sort_artist,
SortOrder.TITLE: self.sort_title,
- SortOrder.YEAR: self.sort_year
+ SortOrder.YEAR: self.sort_year,
+ SortOrder.MODIFIED: self.sort_modified
}
# Button controller for grid scale
diff --git a/src/utils.py b/src/utils.py
index c25b66d..3f2ef3a 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -86,6 +86,7 @@ class SortOrder:
ARTIST = 0
TITLE = 1
YEAR = 2
+ MODIFIED = 3