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 @@ sort by year False - True + + + + + + sort by modification + False + sort_year 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