[rhythmbox: 1/8] listenbrainz: Added ListenBrainz plugin
- From: Jonathan Matthew <jmatthew src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [rhythmbox: 1/8] listenbrainz: Added ListenBrainz plugin
- Date: Sun, 1 Sep 2019 20:59:00 +0000 (UTC)
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 <a
href="https://listenbrainz.org/profile/">user profile</a>.</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]