[gnome-music/wip/jfelder/songeditor-gtk4: 69/76] songeditordialog: Initial songs support




commit ee1254cc56aeba2c6d4a27416bac6c4f716b9009
Author: Sumaid Syed <sumaidsyed gmail com>
Date:   Sun Aug 4 23:02:28 2019 +0530

    songeditordialog: Initial songs support
    
    When a song is selected, it enables to display a dialog to edit its
    main tags (title, artist, album, etc). An online search (with
    chromaprint and acoustid sources) is automatically triggered to
    identify the song from the MusicBrainz database.
    
    An user can choose to use the online suggestions or not. If an online
    suggestion is used, the MusicBrainz tags are also stored.
    
    The grilo-plugins dependency needs to be bumped to 0.3.14 to have a
    proper grl-tracker3-source writeback support.
    
    Based on the patches by Jean Felder
    
    https://gitlab.gnome.org/GNOME/gnome-music/issues/293

 data/org.gnome.Music.gresource.xml     |   1 +
 data/style.css                         |   5 +
 data/ui/SongEditorDialog.ui            | 426 +++++++++++++++++++++++++++++++++
 gnomemusic/widgets/songeditordialog.py | 372 ++++++++++++++++++++++++++++
 meson.build                            |   2 +-
 po/POTFILES.in                         |   2 +
 6 files changed, 807 insertions(+), 1 deletion(-)
---
diff --git a/data/org.gnome.Music.gresource.xml b/data/org.gnome.Music.gresource.xml
index 91d10915c..26d483399 100644
--- a/data/org.gnome.Music.gresource.xml
+++ b/data/org.gnome.Music.gresource.xml
@@ -31,6 +31,7 @@
     <file preprocess="xml-stripblanks">ui/SelectionToolbar.ui</file>
     <file preprocess="xml-stripblanks">ui/SongListItem.ui</file>
     <file preprocess="xml-stripblanks">ui/SongsView.ui</file>
+    <file preprocess="xml-stripblanks">ui/SongEditorDialog.ui</file>
     <file preprocess="xml-stripblanks">ui/SongWidget.ui</file>
     <file preprocess="xml-stripblanks">ui/SongWidgetMenu.ui</file>
     <file preprocess="xml-stripblanks">ui/TagEditorNotification.ui</file>
diff --git a/data/style.css b/data/style.css
index f7bf1021e..ad183384f 100644
--- a/data/style.css
+++ b/data/style.css
@@ -35,6 +35,11 @@
     padding: 12px 0;
 }
 
+/* Suggested Tags in Tag Editor Dialog */
+.suggestion-label {
+    color: @theme_selected_bg_color;
+}
+
 /* PlayerToolbar */
 
 .smooth-scale {
diff --git a/data/ui/SongEditorDialog.ui b/data/ui/SongEditorDialog.ui
new file mode 100644
index 000000000..919ae5dbb
--- /dev/null
+++ b/data/ui/SongEditorDialog.ui
@@ -0,0 +1,426 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="SongEditorDialog" parent="GtkDialog">
+    <property name="resizable">False</property>
+    <property name="modal">True</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="title" translatable="yes">Edit Details</property>
+    <child internal-child="content_area">
+      <object class="GtkBox" id="dialog_vbox">
+        <property name="orientation">vertical</property>
+        <child internal-child="action_area">
+          <object class="GtkBox">
+          </object>
+        </child>
+        <child>
+          <object class="GtkOverlay" id="overlay">
+            <child type="overlay">
+              <object class="NotificationsPopup" id="_notifications_popup">
+                <property name="halign">center</property>
+                <property name="transition_type">slide-down</property>
+                <property name="valign">start</property>
+              </object>
+            </child>
+            <property name="child">
+              <object class="GtkGrid" id="media_details">
+                <property name="halign">start</property>
+                <property name="margin-bottom">12</property>
+                <property name="margin-end">12</property>
+                <property name="margin-start">12</property>
+                <property name="margin-top">12</property>
+                <property name="row_spacing">5</property>
+                <child>
+                  <object class="GtkBox" id="coverart_box">
+                    <property name="vexpand">True</property>
+                    <property name="halign">start</property>
+                    <property name="margin-end">20</property>
+                    <child>
+                      <object class="ArtStack" id="_art_stack">
+                        <property name="valign">fill</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="album-cover"/>
+                    </style>
+                    <layout>
+                      <property name="row-span">4</property>
+                    </layout>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="location">
+                    <property name="justify">right</property>
+                    <property name="margin-end">20</property>
+                    <property name="label" translatable="yes">Location</property>
+                    <property name="xalign">1</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                    <layout>
+                      <property name="column">0</property>
+                      <property name="row">4</property>
+                    </layout>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="_url_label">
+                    <property name="visible">False</property>
+                    <property name="halign">start</property>
+                    <property name="xalign">0</property>
+                    <property name="max_width_chars">45</property>
+                    <property name="wrap">True</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                    <layout>
+                      <property name="column">1</property>
+                      <property name="row">4</property>
+                    </layout>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">12</property>
+                    <layout>
+                      <property name="column">1</property>
+                      <property name="row">1</property>
+                    </layout>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkBox">
+                            <child>
+                              <object class="GtkLabel" id="title">
+                                <property name="margin-end">36</property>
+                                <property name="label" translatable="yes">Title</property>
+                                <property name="xalign">1</property>
+                                <attributes>
+                                  <attribute name="weight" value="bold"/>
+                                </attributes>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="_title_suggestion">
+                                <property name="visible">False</property>
+                                <property name="halign">start</property>
+                                <property name="ellipsize">end</property>
+                                <property name="max_width_chars">30</property>
+                                <style>
+                                  <class name="suggestion-label"/>
+                                </style>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="_title_entry">
+                            <property name="width_chars">45</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkBox">
+                            <child>
+                              <object class="GtkLabel" id="album">
+                                <property name="margin-end">36</property>
+                                <property name="label" translatable="yes">Album</property>
+                                <property name="xalign">1</property>
+                                <attributes>
+                                  <attribute name="weight" value="bold"/>
+                                </attributes>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="_album_suggestion">
+                                <property name="visible">False</property>
+                                <property name="halign">start</property>
+                                <property name="ellipsize">end</property>
+                                <property name="max_width_chars">30</property>
+                                <style>
+                                  <class name="suggestion-label"/>
+                                </style>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="_album_entry">
+                            <property name="width_chars">45</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkBox">
+                            <child>
+                              <object class="GtkLabel" id="artist">
+                                <property name="margin-end">36</property>
+                                <property name="label" translatable="yes">Artist</property>
+                                <property name="xalign">1</property>
+                                <attributes>
+                                  <attribute name="weight" value="bold"/>
+                                </attributes>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="_artist_suggestion">
+                                <property name="visible">False</property>
+                                <property name="halign">start</property>
+                                <property name="ellipsize">end</property>
+                                <property name="max_width_chars">30</property>
+                                <style>
+                                  <class name="suggestion-label"/>
+                                </style>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="_artist_entry">
+                            <property name="width_chars">45</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="hexpand">True</property>
+                        <property name="homogeneous">True</property>
+                        <property name="spacing">12</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <property name="halign">start</property>
+                            <child>
+                              <object class="GtkBox">
+                                <child>
+                                  <object class="GtkLabel" id="track">
+                                    <property name="halign">start</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="label" translatable="yes">Song</property>
+                                    <attributes>
+                                      <attribute name="weight" value="bold"/>
+                                    </attributes>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel" id="_track_suggestion">
+                                    <property name="visible">False</property>
+                                    <property name="halign">start</property>
+                                    <style>
+                                      <class name="suggestion-label"/>
+                                    </style>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="_track_entry">
+                                <property name="width_chars">10</property>
+                                <property name="input_purpose">number</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <property name="halign">start</property>
+                            <child>
+                              <object class="GtkBox">
+                                <child>
+                                  <object class="GtkLabel" id="disc">
+                                    <property name="halign">start</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="label" translatable="yes">Disc</property>
+                                    <attributes>
+                                      <attribute name="weight" value="bold"/>
+                                    </attributes>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel" id="_disc_suggestion">
+                                    <property name="visible">False</property>
+                                    <property name="halign">start</property>
+                                    <style>
+                                      <class name="suggestion-label"/>
+                                    </style>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="_disc_entry">
+                                <property name="width_chars">10</property>
+                                <property name="input_purpose">number</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <property name="halign">start</property>
+                            <child>
+                              <object class="GtkBox">
+                                <child>
+                                  <object class="GtkLabel" id="year">
+                                    <property name="halign">start</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="label" translatable="yes">Year</property>
+                                    <attributes>
+                                      <attribute name="weight" value="bold"/>
+                                    </attributes>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel" id="_year_suggestion">
+                                    <property name="visible">False</property>
+                                    <property name="halign">start</property>
+                                    <style>
+                                      <class name="suggestion-label"/>
+                                    </style>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="_year_entry">
+                                <property name="width_chars">10</property>
+                                <property name="input_purpose">number</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="margin-bottom">20</property>
+            <property name="margin-start">20</property>
+            <child>
+              <object class="GtkBox">
+                <property name="valign">end</property>
+                <property name="spacing">6</property>
+                <property name="baseline_position">bottom</property>
+                <child>
+                  <object class="GtkSpinner" id="_spinner">
+                    <property name="halign">start</property>
+                    <property name="spinning">True</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="_spinner_label">
+                    <property name="label" translatable="yes">Fetching metadata...</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="halign">end</property>
+                <property name="hexpand">True</property>
+                <property name="spacing">6</property>
+                <property name="margin-end">10</property>
+                <child>
+                  <object class="GtkBox" id="_prev_next_box">
+                    <property name="homogeneous">True</property>
+                    <style>
+                      <class name="linked"/>
+                    </style>
+                    <child>
+                      <object class="GtkButton" id="_prev_button">
+                        <property name="valign">center</property>
+                        <property name="sensitive">False</property>
+                        <property name="tooltip_text" translatable="yes">Previous Suggestion</property>
+                        <property name="icon-name">go-previous-symbolic</property>
+                        <signal name="clicked" handler="_on_prev_button_clicked" swapped="no"/>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="_next_button">
+                        <property name="valign">center</property>
+                        <property name="sensitive">False</property>
+                        <property name="tooltip_text" translatable="yes">Next Suggestion</property>
+                        <property name="icon-name">go-next-symbolic</property>
+                        <signal name="clicked" handler="_on_next_button_clicked" swapped="no"/>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="_use_suggestion_button">
+                    <property name="label" translatable="yes">Use Suggestion</property>
+                    <property name="sensitive">False</property>
+                    <property name="receives_default">True</property>
+                    <signal name="clicked" handler="_on_use_suggestion_clicked" swapped="no"/>
+                    <style>
+                      <class name="suggested-action"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="_submit_button">
+                    <property name="label" translatable="yes">Submit</property>
+                    <property name="sensitive">False</property>
+                    <property name="receives_default">True</property>
+                    <signal name="clicked" handler="_on_submit_clicked" swapped="no"/>
+                    <style>
+                      <class name="suggested-action"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/gnomemusic/widgets/songeditordialog.py b/gnomemusic/widgets/songeditordialog.py
new file mode 100644
index 000000000..c52da0515
--- /dev/null
+++ b/gnomemusic/widgets/songeditordialog.py
@@ -0,0 +1,372 @@
+# Copyright 2022 The GNOME Music Developers
+#
+# GNOME Music 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; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music 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 GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+from __future__ import annotations
+from gettext import gettext as _, ngettext
+from itertools import chain
+from typing import Callable, Dict, List, NamedTuple, Optional
+import typing
+
+from gi.repository import Gtk, Gio, GObject, GLib, Grl
+
+from gnomemusic.utils import ArtSize
+from gnomemusic.widgets.artstack import ArtStack  # noqa: F401
+from gnomemusic.widgets.notificationspopup import TagEditorNotification
+if typing.TYPE_CHECKING:
+    from gnomemusic.application import Application
+    from gnomemusic.coregrilo import CoreGrilo
+    from gnomemusic.coresong import CoreSong
+    from gnomemusic.musiclogger import MusicLogger
+
+import gnomemusic.utils as utils
+
+
+@Gtk.Template(resource_path="/org/gnome/Music/ui/SongEditorDialog.ui")
+class SongEditorDialog(Gtk.Dialog):
+    """Song tag editor widget
+    A tag editor dialog box that allows storing metadata for music files
+    through editing entries manually or applying automatic fetched tags.
+    """
+
+    __gtype_name__ = "SongEditorDialog"
+
+    _art_stack = Gtk.Template.Child()
+    _next_button = Gtk.Template.Child()
+    _notifications_popup = Gtk.Template.Child()
+    _prev_button = Gtk.Template.Child()
+    _spinner = Gtk.Template.Child()
+    _spinner_label = Gtk.Template.Child()
+    _submit_button = Gtk.Template.Child()
+    _url_label = Gtk.Template.Child()
+    _use_suggestion_button = Gtk.Template.Child()
+
+    # tags entries and labels
+    _album_entry = Gtk.Template.Child()
+    _album_suggestion = Gtk.Template.Child()
+    _artist_entry = Gtk.Template.Child()
+    _artist_suggestion = Gtk.Template.Child()
+    _disc_entry = Gtk.Template.Child()
+    _disc_suggestion = Gtk.Template.Child()
+    _title_entry = Gtk.Template.Child()
+    _title_suggestion = Gtk.Template.Child()
+    _track_entry = Gtk.Template.Child()
+    _track_suggestion = Gtk.Template.Child()
+    _year_entry = Gtk.Template.Child()
+    _year_suggestion = Gtk.Template.Child()
+
+    __gsignals__ = {
+        "no-tags": (GObject.SignalFlags.RUN_FIRST, None, ()),
+    }
+
+    class Tag(NamedTuple):
+        name: str
+        getter: Callable[[Grl.Media], str]
+        setter: Callable[[Grl.Media, str], None]
+        grl_key: int
+
+    _tags: List[Tag] = [
+        Tag("album", utils.get_album_title, Grl.Media.set_album,
+            Grl.METADATA_KEY_ALBUM),
+        Tag("artist", utils.get_song_artist, Grl.Media.set_artist,
+            Grl.METADATA_KEY_ARTIST),
+        Tag("disc", utils.get_album_disc_nr, utils.set_album_disc_nr,
+            Grl.METADATA_KEY_ALBUM_DISC_NUMBER),
+        Tag("title", utils.get_media_title, Grl.Media.set_title,
+            Grl.METADATA_KEY_TITLE),
+        Tag("track", utils.get_media_track_nr, utils.set_media_track_nr,
+            Grl.METADATA_KEY_TRACK_NUMBER),
+        Tag("year", utils.get_media_year, utils.set_media_year,
+            Grl.METADATA_KEY_PUBLICATION_DATE)
+    ]
+
+    _internal_tags: List[Tag] = [
+        Tag("album-artist", Grl.Media.get_album_artist,
+            Grl.Media.set_album_artist, Grl.METADATA_KEY_ALBUM_ARTIST),
+        Tag("mb-recording-id", Grl.Media.get_mb_recording_id,
+            Grl.Media.set_mb_recording_id, Grl.METADATA_KEY_MB_RECORDING_ID),
+        Tag("mb-track-id", Grl.Media.get_mb_track_id,
+            Grl.Media.set_mb_track_id, Grl.METADATA_KEY_MB_TRACK_ID),
+        Tag("mb-artist-id", Grl.Media.get_mb_artist_id,
+            Grl.Media.set_mb_artist_id, Grl.METADATA_KEY_MB_ARTIST_ID),
+        Tag("mb-release-id", Grl.Media.get_mb_release_id,
+            Grl.Media.set_mb_release_id, Grl.METADATA_KEY_MB_RELEASE_ID),
+        Tag("mb-release-group-id", Grl.Media.get_mb_release_group_id,
+            Grl.Media.set_mb_release_group_id,
+            Grl.METADATA_KEY_MB_RELEASE_GROUP_ID)
+    ]
+
+    def __init__(
+            self, application: Application, selected_song: CoreSong) -> None:
+        """Initialize the tag editor
+
+        :param Application application: Application object
+        :param Coresong selected_song: The Song being edited
+        """
+        super().__init__()
+
+        self._coregrilo: CoreGrilo = application.props.coregrilo
+        self._log: MusicLogger = application.props.log
+
+        self._art_stack.props.size = ArtSize.LARGE
+        self._art_stack.props.coreobject = selected_song
+
+        music_dir: str = GLib.UserDirectory.DIRECTORY_MUSIC
+        self._music_directory: str = GLib.get_user_special_dir(music_dir)
+
+        self._coresong: CoreSong = selected_song
+        self._init_labels()
+
+        self._notification: Optional[TagEditorNotification] = None
+        self._notification_finished_id: int = 0
+        self._notification_undo_id: int = 0
+
+        self._previous_tags: Dict[str, str] = {}
+        self._suggestions: List[Grl.Media] = []
+        self._suggestion_idx: int = -1
+        self._chosen_suggestion_idx: int = -1
+        self._search_tags()
+
+    def _init_labels(self) -> None:
+        for tag in self._tags:
+            entry: Gtk.Entry = getattr(self, "_" + tag.name + "_entry")
+            value: str = tag.getter(self._coresong.props.media)
+            if value:
+                entry.props.text = value
+            entry.connect("notify::text", self._on_entries_changed)
+
+        file_: Gio.File = Gio.File.new_for_uri(self._coresong.props.url)
+        file_path: str = file_.get_path()
+        if file_path.startswith(self._music_directory):
+            baselength: int = len(self._music_directory) + 1
+            self._url_label.props.label = file_path[baselength:]
+            self._url_label.props.tooltip_text = file_path[baselength:]
+        else:
+            self._url_label.props.label = file_path
+            self._url_label.props.tooltip_text = file_path
+
+        self._url_label.props.has_tooltip = True
+        self._url_label.props.visible = True
+
+    def _start_spinner(self) -> None:
+        self._spinner.start()
+        self._spinner_label.props.label = _("Fetching metadata…")
+
+    def _stop_spinner(self) -> None:
+        self._spinner.stop()
+        label: str = _("No suggestions found")
+        if self._suggestions:
+            label = ngettext(
+                "{} suggestion found", "{} suggestions found",
+                len(self._suggestions)).format(len(self._suggestions))
+
+        self._spinner_label.props.label = label
+
+    def _search_tags(self) -> None:
+        self._start_spinner()
+        self._coresong.query_musicbrainz_tags(self._tags_found)
+
+    def _suggestion_sort_func(self, media: Grl.Media) -> GLib.DateTime:
+        creation_date: Optional[GLib.DateTime] = media.get_creation_date()
+        if creation_date:
+            return (creation_date.get_year(), media.get_album())
+
+        return (GLib.DateTime.new_now_utc().get_year(), media.get_album())
+
+    def _tags_found(self, media: Optional[Grl.Media], count: int = 0) -> None:
+        if not media:
+            self._log.warning(
+                "Unable to find tags for song {}".format(
+                    self._coresong.props.url))
+            self._stop_spinner()
+            self._create_notification(TagEditorNotification.Type.NONE)
+            return
+
+        self._suggestions.append(media)
+
+        if count == 0:
+            self._suggestions.sort(key=self._suggestion_sort_func)
+            self._stop_spinner()
+            self._suggestion_idx = 0
+            self._update_suggestion()
+            self._on_entries_changed()
+
+    def _update_suggestion(self) -> None:
+        media: Grl.Media = self._suggestions[self._suggestion_idx]
+        for tag in self._tags:
+            label: Grl.Label = getattr(self, "_" + tag.name + "_suggestion")
+            value: str = tag.getter(media)
+            if value:
+                label.props.label = value
+                label.props.visible = True
+                label.props.has_tooltip = True
+                label.props.tooltip_text = value
+
+        self._next_button.props.sensitive = (
+            self._suggestion_idx < len(self._suggestions) - 1)
+        self._prev_button.props.sensitive = (self._suggestion_idx > 0)
+
+    def _on_entries_changed(
+            self, widget: Optional[Gtk.Entry] = None,
+            param: Optional[GObject.GParamSpec] = None) -> None:
+        if self._suggestion_idx >= 0:
+            media: Grl.Media = self._suggestions[self._suggestion_idx]
+            self._use_suggestion_button.props.sensitive = False
+        self._submit_button.props.sensitive = False
+
+        for tag in self._tags:
+            entry: Gtk.Entry = getattr(self, "_" + tag.name + "_entry")
+            value: str = tag.getter(self._coresong.props.media)
+            typed_value: str = entry.props.text.strip()
+            if (typed_value
+                    and value != typed_value):
+                self._submit_button.props.sensitive = True
+            if self._suggestion_idx >= 0:
+                suggested_value = tag.getter(media)
+                if (typed_value
+                        and suggested_value
+                        and typed_value != suggested_value):
+                    self._use_suggestion_button.props.sensitive = True
+
+    @Gtk.Template.Callback()
+    def _on_next_button_clicked(self, widget: Gtk.Button) -> None:
+        self._suggestion_idx += 1
+        self._update_suggestion()
+        self._on_entries_changed()
+
+    @Gtk.Template.Callback()
+    def _on_prev_button_clicked(self, widget: Gtk.Button) -> None:
+        self._suggestion_idx -= 1
+        self._update_suggestion()
+        self._on_entries_changed()
+
+    @Gtk.Template.Callback()
+    def _on_use_suggestion_clicked(self, widget: Gtk.Button) -> None:
+        suggested_media: Grl.Media = self._suggestions[self._suggestion_idx]
+        self._previous_tags.clear()
+        for tag in self._tags:
+            entry: Gtk.Entry = getattr(self, "_" + tag.name + "_entry")
+            self._previous_tags[tag.name] = entry.props.text
+            suggested_value: str = tag.getter(suggested_media)
+            if suggested_value:
+                entry.props.text = suggested_value
+
+        self._chosen_suggestion_idx = self._suggestion_idx
+        self._create_notification(TagEditorNotification.Type.SONG)
+        self._on_entries_changed()
+
+    def _create_notification(
+            self, notification_type: TagEditorNotification.Type) -> None:
+        self._notification = TagEditorNotification(
+            self._notifications_popup, notification_type)
+        if notification_type != TagEditorNotification.Type.NONE:
+            self._notification_undo_id = self._notification.connect(
+                "undo-fill", self._undo_use_suggestion)
+
+        self._notification_finished_id = self._notification.connect(
+            "finished", self._delete_notification)
+
+    def _delete_notification(
+            self,
+            notification: Optional[TagEditorNotification] = None) -> None:
+        if not self._notification:
+            return
+
+        if self._notification_undo_id != 0:
+            self._notification.disconnect(self._notification_undo_id)
+            self._notification_undo_id = 0
+
+        self._notification.disconnect(self._notification_finished_id)
+        self._notification_finished_id = 0
+        self._notification = None
+
+    def _undo_use_suggestion(
+            self, notification: TagEditorNotification) -> None:
+        """Revert found_tags filling
+
+        :param TagEditorNotification notification: the notification
+        """
+        for tag in self._tags:
+            entry: Gtk.Entry = getattr(self, "_" + tag.name + "_entry")
+            entry.props.text = self._previous_tags[tag.name]
+
+        self._chosen_suggestion_idx = -1
+        self._delete_notification()
+
+    @Gtk.Template.Callback()
+    def _on_submit_clicked(self, widget: Gtk.Button) -> None:
+        self._delete_notification()
+
+        changed_tags_keys: List[int] = []
+        media_writeback: Grl.Media = Grl.Media.audio_new()
+
+        # If _chosen_suggestion_idx is greater than -1, an online
+        # suggestion has been chosen. Otherwise, the tags have been
+        # manually changed.
+        chosen_idx: int = self._chosen_suggestion_idx
+        if chosen_idx > -1:
+            media_chosen: Grl.Media = self._suggestions[chosen_idx]
+            for tag in chain(self._tags, self._internal_tags):
+                existing_tag_text: str = tag.getter(self._coresong.props.media)
+                new_tag_text: str = tag.getter(media_chosen)
+                if new_tag_text:
+                    tag.setter(media_writeback, new_tag_text)
+                    if new_tag_text != existing_tag_text:
+                        changed_tags_keys.append(tag.grl_key)
+        else:
+            for tag in self._tags:
+                existing_tag_text = tag.getter(self._coresong.props.media)
+                entry: Gtk.Entry = getattr(self, "_" + tag.name + "_entry")
+                new_tag_text = entry.props.text.strip()
+                if new_tag_text:
+                    tag.setter(media_writeback, new_tag_text)
+                    if new_tag_text != existing_tag_text:
+                        changed_tags_keys.append(tag.grl_key)
+
+        if changed_tags_keys:
+            media_writeback.set_source(self._coresong.props.media.get_source())
+            media_writeback.set_url(self._coresong.props.media.get_url())
+
+            # FIXME: (Grilo tracker3 plugin). If the musicbrainz
+            # release or release group tag is updated, the album key
+            # need to be added to the list of changed keys to update
+            # those tags.
+            # For similar reasons, the artist key also needs to be
+            # added if the musicbrainz artist tag is updated.
+            if ((Grl.METADATA_KEY_MB_RELEASE_ID in changed_tags_keys
+                 or Grl.METADATA_KEY_MB_RELEASE_GROUP_ID in changed_tags_keys)
+                    and Grl.METADATA_KEY_ALBUM not in changed_tags_keys):
+                changed_tags_keys.append(Grl.METADATA_KEY_ALBUM)
+
+            if (Grl.METADATA_KEY_MB_ARTIST_ID in changed_tags_keys
+                    and Grl.METADATA_KEY_ARTIST not in changed_tags_keys):
+                changed_tags_keys.append(Grl.METADATA_KEY_ARTIST)
+
+            self._log.debug(
+                "Updating tags for song {}".format(self._coresong.props.url))
+            self._coregrilo.writeback(media_writeback, changed_tags_keys)
+        else:
+            self._log.debug(
+                "No updated tag for song {}".format(self._coresong.props.url))
+
+        self.response(Gtk.ResponseType.ACCEPT)
diff --git a/meson.build b/meson.build
index 4ae5b135b..a86e352b5 100644
--- a/meson.build
+++ b/meson.build
@@ -53,7 +53,7 @@ dependency('pango', version: '>= 1.44.0')
 dependency('pygobject-3.0', version: '>= 3.36.1')
 dependency('py3cairo', version: '>= 1.14.0')
 dependency('grilo-0.3', version: '>= 0.3.13', fallback: ['grilo', 'libgrl_dep'])
-dependency('grilo-plugins-0.3', version: '>= 0.3.12', fallback: ['grilo-plugins', 'grilo_plugins_dep'])
+dependency('grilo-plugins-0.3', version: '>= 0.3.14', fallback: ['grilo-plugins', 'grilo_plugins_dep'])
 
 subdir('data/ui')
 subdir('data')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a6d47c7e9..4dfb12808 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -18,6 +18,7 @@ data/ui/SearchHeaderBar.ui
 data/ui/SearchView.ui
 data/ui/SelectionBarMenuButton.ui
 data/ui/SelectionToolbar.ui
+data/ui/SongEditorDialog.ui
 data/ui/SongWidgetMenu.ui
 data/ui/TagEditorNotification.ui
 gnomemusic/__init__.py
@@ -43,5 +44,6 @@ gnomemusic/widgets/lastfmdialog.py
 gnomemusic/widgets/playertoolbar.py
 gnomemusic/widgets/playlistcontrols.py
 gnomemusic/widgets/playlistdialog.py
+gnomemusic/widgets/songeditordialog.py
 gnomemusic/widgets/starhandlerwidget.py
 gnomemusic/window.py


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]