[rhythmbox: 1/8] listenbrainz: Added ListenBrainz plugin



commit 9c0a8947a34639720b54647704fea836e89fb083
Author: Philipp Wolfer <phil parolu io>
Date:   Mon Jun 11 13:57:27 2018 +0200

    listenbrainz: Added ListenBrainz plugin
    
    Previously developed out-of-tree at https://github.com/phw/rhythmbox-plugin-listenbrainz
    
    Fixes #1574

 configure.ac                                |   1 +
 data/org.gnome.rhythmbox.gschema.xml        |   8 ++
 plugins/Makefile.am                         |   1 +
 plugins/listenbrainz/Makefile.am            |  23 ++++
 plugins/listenbrainz/client.py              | 173 ++++++++++++++++++++++++++++
 plugins/listenbrainz/listenbrainz.plugin.in |  10 ++
 plugins/listenbrainz/listenbrainz.py        | 169 +++++++++++++++++++++++++++
 plugins/listenbrainz/queue.py               | 106 +++++++++++++++++
 plugins/listenbrainz/settings.py            |  51 ++++++++
 plugins/listenbrainz/settings.ui            |  52 +++++++++
 po/POTFILES.in                              |   2 +
 11 files changed, 596 insertions(+)
---
diff --git a/configure.ac b/configure.ac
index cf696e945..313779b75 100644
--- a/configure.ac
+++ b/configure.ac
@@ -739,6 +739,7 @@ plugins/notification/Makefile
 plugins/grilo/Makefile
 plugins/soundcloud/Makefile
 plugins/webremote/Makefile
+plugins/listenbrainz/Makefile
 sample-plugins/Makefile
 sample-plugins/sample/Makefile
 sample-plugins/sample-python/Makefile
diff --git a/data/org.gnome.rhythmbox.gschema.xml b/data/org.gnome.rhythmbox.gschema.xml
index 95685a7d9..5fcb5f618 100644
--- a/data/org.gnome.rhythmbox.gschema.xml
+++ b/data/org.gnome.rhythmbox.gschema.xml
@@ -336,6 +336,14 @@
     <child name='source' schema='org.gnome.rhythmbox.plugins.iradio.source'/>
   </schema>
 
+  <schema id="org.gnome.rhythmbox.plugins.listenbrainz" path="/org/gnome/rhythmbox/plugins/listenbrainz/">
+      <key type="s" name="user-token">
+          <default>""</default>
+          <summary>ListenBrainz user token</summary>
+          <description></description>
+      </key>
+  </schema>
+
   <schema id="org.gnome.rhythmbox.plugins.lyrics" path="/org/gnome/rhythmbox/plugins/lyrics/">
     <key name="sites" type="as">
       <default>['lyrc.com.ar']</default>       <!-- do we have any that work? -->
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index fa83a4da6..842ac3431 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -15,6 +15,7 @@ if ENABLE_PYTHON
 SUBDIRS +=                                             \
        artsearch                                       \
        im-status                                       \
+       listenbrainz                                    \
        lyrics                                          \
        magnatune                                       \
        pythonconsole                                   \
diff --git a/plugins/listenbrainz/Makefile.am b/plugins/listenbrainz/Makefile.am
new file mode 100644
index 000000000..92b0e007a
--- /dev/null
+++ b/plugins/listenbrainz/Makefile.am
@@ -0,0 +1,23 @@
+# ListenBrainz plugin
+plugindir = $(PLUGINDIR)/listenbrainz
+plugindatadir = $(PLUGINDATADIR)/listenbrainz
+
+plugin_PYTHON =                        \
+       client.py                       \
+       listenbrainz.py                 \
+       queue.py                        \
+       settings.py
+
+plugin_in_files = listenbrainz.plugin.in
+%.plugin: %.plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) 
$(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.plugin.in=.plugin)
+
+gtkbuilderdir = $(plugindatadir)
+gtkbuilder_DATA =                      \
+               settings.ui
+
+EXTRA_DIST = $(plugin_in_files) $(gtkbuilder_DATA)
+
+CLEANFILES = $(plugin_DATA)
+DISTCLEANFILES = $(plugin_DATA)
diff --git a/plugins/listenbrainz/client.py b/plugins/listenbrainz/client.py
new file mode 100644
index 000000000..b8f1be9ac
--- /dev/null
+++ b/plugins/listenbrainz/client.py
@@ -0,0 +1,173 @@
+# Copyright (c) 2018 Philipp Wolfer <ph wolfer gmail com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import json
+import logging
+import ssl
+import time
+from http.client import HTTPSConnection
+
+HOST_NAME = "api.listenbrainz.org"
+PATH_SUBMIT = "/1/submit-listens"
+SSL_CONTEXT = ssl.create_default_context()
+
+
+class Track:
+    """
+    Represents a single track to submit.
+
+    See https://listenbrainz.readthedocs.io/en/latest/dev/json.html
+    """
+    def __init__(self, artist_name, track_name,
+                 release_name=None, additional_info={}):
+        """
+        Create a new Track instance
+        @param artist_name as str
+        @param track_name as str
+        @param release_name as str
+        @param additional_info as dict
+        """
+        self.artist_name = artist_name
+        self.track_name = track_name
+        self.release_name = release_name
+        self.additional_info = additional_info
+
+    @staticmethod
+    def from_dict(data):
+        return Track(
+            data["artist_name"],
+            data["track_name"],
+            data.get("release_name", None),
+            data.get("additional_info", {})
+        )
+
+    def to_dict(self):
+        return {
+            "artist_name": self.artist_name,
+            "track_name": self.track_name,
+            "release_name": self.release_name,
+            "additional_info": self.additional_info
+        }
+
+    def __repr__(self):
+        return "Track(%s, %s)" % (self.artist_name, self.track_name)
+
+
+class ListenBrainzClient:
+    """
+    Submit listens to ListenBrainz.org.
+
+    See https://listenbrainz.readthedocs.io/en/latest/dev/api.html
+    """
+
+    def __init__(self, logger=logging.getLogger(__name__)):
+        self.__next_request_time = 0
+        self.user_token = None
+        self.logger = logger
+
+    def listen(self, listened_at, track):
+        """
+        Submit a listen for a track
+        @param listened_at as int
+        @param entry as Track
+        """
+        payload = _get_payload(track, listened_at)
+        return self._submit("single", [payload])
+
+    def playing_now(self, track):
+        """
+        Submit a playing now notification for a track
+        @param track as Track
+        """
+        payload = _get_payload(track)
+        return self._submit("playing_now", [payload])
+
+    def import_tracks(self, tracks):
+        """
+        Import a list of tracks as (listened_at, Track) pairs
+        @param track as [(int, Track)]
+        """
+        payload = _get_payload_many(tracks)
+        return self._submit("import", payload)
+
+    def _submit(self, listen_type, payload, retry=0):
+        self._wait_for_ratelimit()
+        self.logger.debug("ListenBrainz %s: %r", listen_type, payload)
+        data = {
+            "listen_type": listen_type,
+            "payload": payload
+        }
+        headers = {
+            "Authorization": "Token %s" % self.user_token,
+            "Content-Type": "application/json"
+        }
+        body = json.dumps(data)
+        conn = HTTPSConnection(HOST_NAME, context=SSL_CONTEXT)
+        conn.request("POST", PATH_SUBMIT, body, headers)
+        response = conn.getresponse()
+        response_text = response.read()
+        try:
+            response_data = json.loads(response_text)
+        except json.decoder.JSONDecodeError:
+            response_data = response_text
+
+        self._handle_ratelimit(response)
+        log_msg = "Response %s: %r" % (response.status, response_data)
+        if response.status == 429 and retry < 5:  # Too Many Requests
+            self.logger.warning(log_msg)
+            return self._submit(listen_type, payload, retry + 1)
+        elif response.status == 200:
+            self.logger.debug(log_msg)
+        else:
+            self.logger.error(log_msg)
+        return response
+
+    def _wait_for_ratelimit(self):
+        now = time.time()
+        if self.__next_request_time > now:
+            delay = self.__next_request_time - now
+            self.logger.debug("Rate limit applies, delay %d", delay)
+            time.sleep(delay)
+
+    def _handle_ratelimit(self, response):
+        remaining = int(response.getheader("X-RateLimit-Remaining", 0))
+        reset_in = int(response.getheader("X-RateLimit-Reset-In", 0))
+        self.logger.debug("X-RateLimit-Remaining: %i", remaining)
+        self.logger.debug("X-RateLimit-Reset-In: %i", reset_in)
+        if remaining == 0:
+            self.__next_request_time = time.time() + reset_in
+
+
+def _get_payload_many(tracks):
+    payload = []
+    for (listened_at, track) in tracks:
+        data = _get_payload(track, listened_at)
+        payload.append(data)
+    return payload
+
+
+def _get_payload(track, listened_at=None):
+    data = {
+        "track_metadata": track.to_dict()
+    }
+    if listened_at is not None:
+        data["listened_at"] = listened_at
+    return data
diff --git a/plugins/listenbrainz/listenbrainz.plugin.in b/plugins/listenbrainz/listenbrainz.plugin.in
new file mode 100644
index 000000000..8c48040c1
--- /dev/null
+++ b/plugins/listenbrainz/listenbrainz.plugin.in
@@ -0,0 +1,10 @@
+[Plugin]
+Loader=python3
+Module=listenbrainz
+Depends=rb
+IAge=2
+_Name=ListenBrainz
+_Description=Submit your listens to ListenBrainz
+Authors=Philipp Wolfer <ph wolfer gmail com>
+Copyright=Copyright © 2018 Philipp Wolfer
+Website=https://github.com/phw/rhythmbox-plugin-listenbrainz
diff --git a/plugins/listenbrainz/listenbrainz.py b/plugins/listenbrainz/listenbrainz.py
new file mode 100644
index 000000000..2ac2753ee
--- /dev/null
+++ b/plugins/listenbrainz/listenbrainz.py
@@ -0,0 +1,169 @@
+# Copyright (c) 2018 Philipp Wolfer <ph wolfer gmail com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import logging
+import sys
+import time
+from gi.repository import GObject
+from gi.repository import Peas
+from gi.repository import RB
+from client import ListenBrainzClient, Track
+from queue import ListenBrainzQueue
+from settings import ListenBrainzSettings, load_settings
+
+
+logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
+logger = logging.getLogger("listenbrainz")
+
+
+class ListenBrainzPlugin(GObject.Object, Peas.Activatable):
+    __gtype_name = 'ListenBrainzPlugin'
+    object = GObject.property(type=GObject.GObject)
+
+    def __init__(self):
+        GObject.Object.__init__(self)
+        self.settings = None
+        self.__client = None
+        self.__current_entry = None
+        self.__current_start_time = 0
+        self.__current_elapsed = 0
+
+    def do_activate(self):
+        logger.debug("activating ListenBrainz plugin")
+        self.settings = load_settings()
+        self.__client = ListenBrainzClient(logger=logger)
+        self.settings.connect("changed::user-token",
+                              self.on_user_token_changed)
+        self.on_user_token_changed(self.settings)
+        self.__current_entry = None
+        self.__current_start_time = 0
+        self.__current_elapsed = 0
+        self.__queue = ListenBrainzQueue(self.__client)
+        try:
+            self.__queue.load()
+        except Exception as e:
+            _handle_exception(e)
+        self.__queue.activate()
+        shell_player = self.object.props.shell_player
+        shell_player.connect("playing-song-changed",
+                             self.on_playing_song_changed)
+        shell_player.connect("elapsed-changed", self.on_elapsed_changed)
+
+    def do_deactivate(self):
+        logger.debug("deactivating ListenBrainz plugin")
+        shell_player = self.object.props.shell_player
+        shell_player.disconnect_by_func(self.on_playing_song_changed)
+        shell_player.disconnect_by_func(self.on_elapsed_changed)
+        self.settings.disconnect_by_func(self.on_user_token_changed)
+        self.__queue.deactivate()
+        self.__queue.submit_batch()
+        self.__queue.save()
+
+    def on_playing_song_changed(self, player, entry):
+        logger.debug("playing-song-changed: %r, %r", player, entry)
+
+        self._submit_current_entry()
+
+        self.__current_entry = entry
+        self.__current_elapsed = 0
+
+        if not _can_be_listened(entry):
+            self.__current_entry = None
+            return
+
+        self.__current_start_time = int(time.time())
+        track = _entry_to_track(entry)
+        try:
+            self.__client.playing_now(track)
+        except Exception as e:
+            _handle_exception(e)
+
+    def on_elapsed_changed(self, player, elapsed):
+        # logger.debug("elapsed-changed: %r, %i" % (player, elapsed))
+        if player.get_playing_entry() == self.__current_entry:
+            self.__current_elapsed += 1
+
+    def on_user_token_changed(self, settings, key="user-token"):
+        self.__client.user_token = settings.get_string("user-token")
+
+    def _submit_current_entry(self):
+        if self.__current_entry is not None:
+            duration = self.__current_entry.get_ulong(
+                            RB.RhythmDBPropType.DURATION)
+            elapsed = self.__current_elapsed
+            logger.debug("Elapsed: %s / %s", elapsed, duration)
+            if elapsed >= 240 or elapsed >= duration / 2:
+                track = _entry_to_track(self.__current_entry)
+                try:
+                    self.__queue.add(self.__current_start_time, track)
+                except Exception as e:
+                    _handle_exception(e)
+
+
+def _can_be_listened(entry):
+    if entry is None:
+        return False
+
+    entry_type = entry.get_entry_type()
+    category = entry_type.get_property("category")
+    title = entry.get_string(RB.RhythmDBPropType.TITLE)
+    error = entry.get_string(RB.RhythmDBPropType.PLAYBACK_ERROR)
+
+    if category != RB.RhythmDBEntryCategory.NORMAL:
+        logger.debug("Cannot submit %r: Category %s" %
+                     (title, category.value_name))
+        return False
+
+    if entry_type.get_name() != "song":
+        logger.debug("Cannot submit listen%r: Entry type %s" %
+                     (title, entry_type.get_name()))
+        return False
+
+    if error is not None:
+        logger.debug("Cannot submit %r: Playback error %s" %
+                     (title, error))
+        return False
+
+    return True
+
+
+def _handle_exception(e):
+    logger.error("ListenBrainz exception %s: %s", type(e).__name__, e)
+
+
+def _entry_to_track(entry):
+    artist = entry.get_string(RB.RhythmDBPropType.ARTIST)
+    title = entry.get_string(RB.RhythmDBPropType.TITLE)
+    album = entry.get_string(RB.RhythmDBPropType.ALBUM)
+    track_number = entry.get_ulong(RB.RhythmDBPropType.TRACK_NUMBER)
+    mb_track_id = entry.get_string(RB.RhythmDBPropType.MB_TRACKID)
+    mb_album_id = entry.get_string(RB.RhythmDBPropType.MB_ALBUMID)
+    mb_artist_id = entry.get_string(RB.RhythmDBPropType.MB_ARTISTID)
+    additional_info = {
+        "release_mbid": mb_album_id or None,
+        "recording_mbid": mb_track_id or None,
+        "artist_mbids": [mb_artist_id] if mb_artist_id else [],
+        "tracknumber": track_number or None
+    }
+    return Track(artist, title, album, additional_info)
+
+
+GObject.type_register(ListenBrainzSettings)
diff --git a/plugins/listenbrainz/queue.py b/plugins/listenbrainz/queue.py
new file mode 100644
index 000000000..6db4b7785
--- /dev/null
+++ b/plugins/listenbrainz/queue.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2018 Philipp Wolfer <ph wolfer gmail com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import json
+import logging
+import os
+from gi.repository import GLib
+from client import Track
+
+MAX_TRACKS_PER_IMPORT = 10
+
+logger = logging.getLogger("listenbrainz")
+
+
+class ListenBrainzQueue:
+
+    def __init__(self, client):
+        self.__client = client
+        self.__queue = []
+
+    def activate(self):
+        self.submit_batch()
+        self.__timeout_id = GLib.timeout_add_seconds(30, self.submit_batch)
+
+    def deactivate(self):
+        GLib.source_remove(self.__timeout_id)
+
+    def add(self, listened_at, track):
+        try:
+            # Try to submit immediatelly, and queue if it fails
+            response = self.__client.listen(listened_at, track)
+            if response.status in [401, 429] or response.status >= 500:
+                self._append(listened_at, track)
+        except Exception as e:
+            logger.error("ListenBrainz exception %s: %s", type(e).__name__, e)
+            self._append(listened_at, track)
+
+    def load(self):
+        cache_file = self.get_cache_file_path()
+        if os.path.exists(cache_file):
+            logger.debug("Loading queue from %s", cache_file)
+            self.__queue = json.load(open(cache_file), object_hook=from_json)
+
+    def save(self):
+        cache_file = self.get_cache_file_path()
+        cache_dir = os.path.dirname(cache_file)
+        if not os.path.exists(cache_dir):
+            os.makedirs(cache_dir)
+        logger.debug("Saving queue to %s", cache_file)
+        json.dump(self.__queue, open(cache_file, 'w'), cls=QueueEncoder)
+
+    def _append(self, listened_at, track):
+        logger.debug("Queuing for later submission %s: %s", listened_at, track)
+        self.__queue.append((listened_at, track))
+
+    def submit_batch(self):
+        if len(self.__queue) == 0:
+            return True
+        logger.debug("Submitting %d queued entries", len(self.__queue))
+        try:
+            tracks = self.__queue[0:MAX_TRACKS_PER_IMPORT]
+            response = self.__client.import_tracks(tracks)
+            if response.status != 200:
+                return True
+            if len(self.__queue) > MAX_TRACKS_PER_IMPORT:
+                self.__queue = self.__queue[MAX_TRACKS_PER_IMPORT:]
+            else:
+                self.__queue = []
+        except Exception as e:
+            logger.error("ListenBrainz exception %s: %s", type(e).__name__, e)
+        return True
+
+    def get_cache_file_path(self):
+        return os.path.join(GLib.get_user_cache_dir(), "rhythmbox",
+                            "listenbrainz-queue.json")
+
+
+class QueueEncoder(json.JSONEncoder):
+    def default(self, o):
+        if type(o) is Track:
+            return o.to_dict()
+        return super(json.JSONEncoder, self).default(o)
+
+
+def from_json(json_object):
+    if 'artist_name' in json_object:
+        return Track.from_dict(json_object)
+    return json_object
diff --git a/plugins/listenbrainz/settings.py b/plugins/listenbrainz/settings.py
new file mode 100644
index 000000000..28da52f8e
--- /dev/null
+++ b/plugins/listenbrainz/settings.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2018 Philipp Wolfer <ph wolfer gmail com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import rb
+from gi.repository import Gio
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import PeasGtk
+
+
+def load_settings():
+    return Gio.Settings.new("org.gnome.rhythmbox.plugins.listenbrainz")
+
+
+class ListenBrainzSettings(GObject.Object, PeasGtk.Configurable):
+    __gtype_name__ = 'ListenBrainzSettings'
+    object = GObject.property(type=GObject.Object)
+
+    user_token_entry = GObject.Property(type=Gtk.Entry, default=None)
+
+    def do_create_configure_widget(self):
+        self.settings = load_settings()
+
+        ui_file = rb.find_plugin_file(self, "settings.ui")
+        self.builder = Gtk.Builder()
+        self.builder.add_from_file(ui_file)
+
+        content = self.builder.get_object("listenbrainz-settings")
+
+        self.user_token_entry = self.builder.get_object("user-token")
+        self.settings.bind("user-token", self.user_token_entry, "text", 0)
+
+        return content
diff --git a/plugins/listenbrainz/settings.ui b/plugins/listenbrainz/settings.ui
new file mode 100644
index 000000000..9794a03fd
--- /dev/null
+++ b/plugins/listenbrainz/settings.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.2 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <object class="GtkGrid" id="listenbrainz-settings">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_start">12</property>
+    <property name="margin_end">12</property>
+    <property name="margin_top">12</property>
+    <property name="margin_bottom">12</property>
+    <property name="row_spacing">6</property>
+    <property name="column_spacing">6</property>
+    <child>
+      <object class="GtkEntry" id="user-token">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="width_chars">34</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="user-token-label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">User token:</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">To submit your listens to ListenBrainz, enter your 
ListenBrainz user token below. You can see your user token in your &lt;a 
href="https://listenbrainz.org/profile/"&gt;user profile&lt;/a&gt;.</property>
+        <property name="use_markup">True</property>
+        <property name="wrap">True</property>
+        <property name="max_width_chars">40</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index cb5a21ded..87091e81d 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -122,6 +122,8 @@ plugins/iradio/rb-station-properties-dialog.c
 [type: gettext/glade]plugins/iradio/station-properties.ui
 plugins/lirc/rb-lirc-plugin.c
 [type: gettext/ini]plugins/lirc/rblirc.plugin.in
+[type: gettext/ini]plugins/listenbrainz/listenbrainz.plugin.in
+[type: gettext/glade]plugins/listenbrainz/settings.ui
 plugins/lyrics/LyricsConfigureDialog.py
 [type: gettext/ini]plugins/lyrics/lyrics.plugin.in
 [type: gettext/glade]plugins/lyrics/lyrics-prefs.ui


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