listenbrainz: Set the user API token
I added a "user-token" property to the ListenBrainz object, and watch for changes to query the liblistenbrainz client. I also set up an idle callback so we can set the "valid-token" property so the user can know if there is a problem. Implements: #69 ("Add ListenBrainz support") Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
14c153733d
commit
924f65fddd
|
@ -1,6 +1,7 @@
|
|||
# Copyright 2024 (c) Anna Schumaker.
|
||||
"""Our ListenBrainz custom GObject."""
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from . import thread
|
||||
from . import task
|
||||
|
||||
|
@ -8,12 +9,56 @@ from . import task
|
|||
class ListenBrainz(GObject.GObject):
|
||||
"""Our main ListenBrainz GObject."""
|
||||
|
||||
user_token = GObject.Property(type=str)
|
||||
valid_token = GObject.Property(type=bool, default=True)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the ListenBrainz GObject."""
|
||||
super().__init__()
|
||||
self._queue = task.Queue()
|
||||
self._thread = thread.Thread()
|
||||
|
||||
self._idle_id = None
|
||||
|
||||
self.connect("notify::user-token", self.__notify_user_token)
|
||||
|
||||
def __check_result(self) -> None:
|
||||
if (res := self._thread.get_result()) is not None:
|
||||
self.valid_token = res.valid
|
||||
|
||||
def __parse_task(self, op: str, *args) -> bool:
|
||||
match op:
|
||||
case "set-token":
|
||||
self._thread.set_user_token(*args)
|
||||
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
def __idle_work(self) -> bool:
|
||||
if self._thread.ready.is_set():
|
||||
self.__check_result()
|
||||
|
||||
if (task := self._queue.pop()) is None:
|
||||
self._idle_id = None
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
return self.__parse_task(*task)
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
def __idle_start(self) -> None:
|
||||
if self._idle_id is None:
|
||||
self._idle_id = GLib.idle_add(self.__idle_work)
|
||||
|
||||
def __notify_user_token(self, listenbrainz: GObject.GObject,
|
||||
param: GObject.ParamSpec) -> None:
|
||||
self._queue.push("set-token", self.user_token)
|
||||
self.__idle_start()
|
||||
|
||||
def __source_stop(self, srcid: str) -> None:
|
||||
if (id := getattr(self, srcid)) is not None:
|
||||
GLib.source_remove(id)
|
||||
setattr(self, srcid, None)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the ListenBrainz thread."""
|
||||
self.__source_stop("_idle_id")
|
||||
self._thread.stop()
|
||||
|
|
|
@ -4,3 +4,21 @@
|
|||
|
||||
class Queue:
|
||||
"""A queue for prioritizing ListenBrainz operations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the task Queue."""
|
||||
self._set_token = None
|
||||
|
||||
def clear(self, op: str) -> None:
|
||||
"""Clear a pending operation."""
|
||||
self._set_token = None
|
||||
|
||||
def push(self, op: str, *args) -> None:
|
||||
"""Push an operation onto the queue."""
|
||||
self._set_token = (op, *args)
|
||||
|
||||
def pop(self) -> tuple:
|
||||
"""Pop an operation off the queue."""
|
||||
res = self._set_token
|
||||
self._set_token = None
|
||||
return res
|
||||
|
|
|
@ -1,7 +1,45 @@
|
|||
# Copyright 2024 (c) Anna Schumaker.
|
||||
"""Our ListenBrainz client thread."""
|
||||
import liblistenbrainz
|
||||
from .. import thread
|
||||
|
||||
|
||||
class Thread(thread.Thread):
|
||||
"""Thread for submitting listens to ListenBrainz."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the ListenBrainz Thread object."""
|
||||
super().__init__()
|
||||
self._client = liblistenbrainz.client.ListenBrainz()
|
||||
|
||||
def __print(self, text: str) -> None:
|
||||
print(f"listenbrainz: {text}")
|
||||
|
||||
def __set_user_token(self, token: str) -> None:
|
||||
try:
|
||||
self._client.set_auth_token(token)
|
||||
self.set_result("set-token", token=token)
|
||||
except liblistenbrainz.errors.InvalidAuthTokenException:
|
||||
self.set_result("set-token", token=token, valid=False)
|
||||
|
||||
def do_run_task(self, task: thread.Data) -> None:
|
||||
"""Call a specific listenbrainz operation."""
|
||||
match task.op:
|
||||
case "set-token":
|
||||
self.__set_user_token(task.token)
|
||||
|
||||
def get_result(self, **kwargs) -> thread.Data:
|
||||
"""Get the result of a listenbrainz task."""
|
||||
if (res := super().get_result(**kwargs)) is not None:
|
||||
if not res.valid:
|
||||
self.__print("user token is invalid")
|
||||
return res
|
||||
|
||||
def set_result(self, op: str, *, valid: bool = True, **kwargs) -> None:
|
||||
"""Set the Thread result with a standard format for all ops."""
|
||||
super().set_result(op=op, valid=valid, **kwargs)
|
||||
|
||||
def set_user_token(self, token: str) -> None:
|
||||
"""Schedule setting the user token."""
|
||||
self.__print("setting user token")
|
||||
self.set_task(op="set-token", token=token)
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
# Copyright 2024 (c) Anna Schumaker.
|
||||
"""Tests our custom ListenBrainz GObject."""
|
||||
import emmental.listenbrainz
|
||||
import io
|
||||
import unittest
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
@unittest.mock.patch("gi.repository.GLib.source_remove")
|
||||
@unittest.mock.patch("gi.repository.GLib.idle_add", return_value=42)
|
||||
class TestListenBrainz(unittest.TestCase):
|
||||
"""ListenBrainz GObject test case."""
|
||||
|
||||
|
@ -12,19 +17,72 @@ class TestListenBrainz(unittest.TestCase):
|
|||
"""Set up common variables."""
|
||||
self.listenbrainz = emmental.listenbrainz.ListenBrainz()
|
||||
|
||||
def tearDown(self):
|
||||
@unittest.mock.patch("gi.repository.GLib.source_remove")
|
||||
def tearDown(self, mock_source_remove: unittest.mock.Mock):
|
||||
"""Clean up."""
|
||||
self.listenbrainz.stop()
|
||||
|
||||
def test_init(self):
|
||||
def test_init(self, mock_idle_add: unittest.mock.Mock,
|
||||
mock_source_remove: unittest.mock.Mock,
|
||||
mock_stdout: io.StringIO):
|
||||
"""Test that the ListenBrainz GObject was set up properly."""
|
||||
self.assertIsInstance(self.listenbrainz, GObject.GObject)
|
||||
self.assertIsInstance(self.listenbrainz._queue,
|
||||
emmental.listenbrainz.task.Queue)
|
||||
self.assertIsInstance(self.listenbrainz._thread,
|
||||
emmental.listenbrainz.thread.Thread)
|
||||
self.assertIsNone(self.listenbrainz._idle_id)
|
||||
|
||||
def test_stop(self):
|
||||
def test_stop(self, mock_idle_add: unittest.mock.Mock,
|
||||
mock_source_remove: unittest.mock.Mock,
|
||||
mock_stdout: io.StringIO):
|
||||
"""Test stopping the thread during shutdown."""
|
||||
self.listenbrainz._idle_id = 12345
|
||||
|
||||
self.listenbrainz.stop()
|
||||
self.assertFalse(self.listenbrainz._thread.is_alive())
|
||||
self.assertIsNone(self.listenbrainz._idle_id)
|
||||
mock_source_remove.assert_called_with(12345)
|
||||
|
||||
def test_set_user_token(self, mock_idle_add: unittest.mock.Mock,
|
||||
mock_source_remove: unittest.mock.Mock,
|
||||
mock_stdout: io.StringIO):
|
||||
"""Test setting the user-token property."""
|
||||
self.assertEqual(self.listenbrainz.user_token, "")
|
||||
self.assertTrue(self.listenbrainz.valid_token)
|
||||
|
||||
idle_work = self.listenbrainz._ListenBrainz__idle_work
|
||||
with unittest.mock.patch.object(self.listenbrainz._thread,
|
||||
"set_user_token") as mock_set_token:
|
||||
self.listenbrainz.user_token = "abc"
|
||||
self.assertEqual(self.listenbrainz._queue._set_token,
|
||||
("set-token", "abc"))
|
||||
self.assertEqual(self.listenbrainz._idle_id, 42)
|
||||
mock_idle_add.assert_called_with(idle_work)
|
||||
|
||||
self.assertEqual(idle_work(), GLib.SOURCE_CONTINUE)
|
||||
mock_set_token.assert_called_with("abc")
|
||||
|
||||
mock_idle_add.reset_mock()
|
||||
self.listenbrainz.user_token = "abcde"
|
||||
self.assertEqual(self.listenbrainz._queue._set_token,
|
||||
("set-token", "abcde"))
|
||||
self.assertEqual(self.listenbrainz._idle_id, 42)
|
||||
mock_idle_add.assert_not_called()
|
||||
|
||||
self.listenbrainz._thread.set_result(op="set-token", token="abc",
|
||||
valid=True)
|
||||
self.assertEqual(idle_work(), GLib.SOURCE_CONTINUE)
|
||||
mock_set_token.assert_called_with("abcde")
|
||||
|
||||
self.listenbrainz._thread.ready.clear()
|
||||
self.assertEqual(idle_work(), GLib.SOURCE_CONTINUE)
|
||||
|
||||
for valid in [False, True]:
|
||||
with self.subTest(valid=valid):
|
||||
self.listenbrainz._thread.set_result(op="set-token",
|
||||
token="abcde",
|
||||
valid=valid)
|
||||
self.assertEqual(idle_work(), GLib.SOURCE_REMOVE)
|
||||
self.assertEqual(self.listenbrainz.valid_token, valid)
|
||||
self.assertIsNone(self.listenbrainz._idle_id)
|
||||
|
|
|
@ -14,3 +14,19 @@ class TestTaskQueue(unittest.TestCase):
|
|||
def test_init(self):
|
||||
"""Test that the queue was set up properly."""
|
||||
self.assertIsNotNone(self.queue)
|
||||
|
||||
def test_push_set_token(self):
|
||||
"""Test calling push() with the 'set-token' operation."""
|
||||
self.assertIsNone(self.queue._set_token)
|
||||
|
||||
self.queue.push("set-token", "abcde")
|
||||
self.assertTupleEqual(self.queue._set_token, ("set-token", "abcde"))
|
||||
self.queue.push("set-token", "fghij")
|
||||
self.assertTupleEqual(self.queue._set_token, ("set-token", "fghij"))
|
||||
|
||||
self.assertTupleEqual(self.queue.pop(), ("set-token", "fghij"))
|
||||
self.assertIsNone(self.queue._set_token)
|
||||
|
||||
self.queue.push("set-token", "abcde")
|
||||
self.queue.clear("set-token")
|
||||
self.assertIsNone(self.queue._set_token)
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
# Copyright 2024 (c) Anna Schumaker.
|
||||
"""Tests our ListenBrainz client thread."""
|
||||
import emmental.listenbrainz.thread
|
||||
import io
|
||||
import liblistenbrainz
|
||||
import unittest
|
||||
|
||||
|
||||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
class TestThread(unittest.TestCase):
|
||||
"""ListenBrainz Thread test case."""
|
||||
|
||||
|
@ -15,6 +18,40 @@ class TestThread(unittest.TestCase):
|
|||
"""Clean up."""
|
||||
self.thread.stop()
|
||||
|
||||
def test_init(self):
|
||||
def test_init(self, mock_stdout: io.StringIO):
|
||||
"""Test that the ListenBrainz thread was initialized properly."""
|
||||
self.assertIsInstance(self.thread, emmental.thread.Thread)
|
||||
self.assertIsInstance(self.thread._client,
|
||||
liblistenbrainz.client.ListenBrainz)
|
||||
|
||||
def test_set_user_token(self, mock_stdout: io.StringIO):
|
||||
"""Test setting the user auth token."""
|
||||
with unittest.mock.patch.object(self.thread._client,
|
||||
"set_auth_token") as mock_set_auth:
|
||||
self.thread.set_user_token("abcde")
|
||||
self.assertFalse(self.thread.ready.is_set())
|
||||
self.assertEqual(self.thread._task,
|
||||
{"op": "set-token", "token": "abcde"})
|
||||
self.assertEqual(mock_stdout.getvalue(),
|
||||
"listenbrainz: setting user token\n")
|
||||
|
||||
self.thread.ready.wait()
|
||||
mock_set_auth.assert_called_with("abcde")
|
||||
self.assertEqual(self.thread.get_result(),
|
||||
{"op": "set-token", "token": "abcde",
|
||||
"valid": True})
|
||||
|
||||
def test_set_user_token_exceptions(self, mock_stdout: io.StringIO):
|
||||
"""Test exception handling when setting the user auth token."""
|
||||
with unittest.mock.patch.object(self.thread._client,
|
||||
"set_auth_token") as mock_set_auth:
|
||||
mock_set_auth.side_effect = \
|
||||
liblistenbrainz.errors.InvalidAuthTokenException()
|
||||
self.thread.set_user_token("abcde")
|
||||
self.thread.ready.wait()
|
||||
self.assertEqual(self.thread.get_result(),
|
||||
{"op": "set-token", "token": "abcde",
|
||||
"valid": False})
|
||||
self.assertEqual(mock_stdout.getvalue(),
|
||||
"listenbrainz: setting user token\n" +
|
||||
"listenbrainz: user token is invalid\n")
|
||||
|
|
Loading…
Reference in New Issue