beetkeeper-plugin 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Final
|
|
5
|
+
|
|
6
|
+
from beets.importer import ImportTask # pants: no-infer-dep
|
|
7
|
+
from beets.library import Album, Item # pants: no-infer-dep
|
|
8
|
+
from beets.plugins import BeetsPlugin # pants: no-infer-dep
|
|
9
|
+
from beetsplug._utils.requests import ( # type: ignore[import-untyped]
|
|
10
|
+
RequestHandler, # pants: no-infer-dep
|
|
11
|
+
TimeoutAndRetrySession, # pants: no-infer-dep
|
|
12
|
+
)
|
|
13
|
+
from requests.auth import AuthBase
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from beets.importer import ImportSession # pants: no-infer-dep
|
|
17
|
+
from beets.library import Library # pants: no-infer-dep
|
|
18
|
+
from beets.plugins import EventType # pants: no-infer-dep
|
|
19
|
+
from requests import PreparedRequest, Response
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_LOGGER_NAME: Final[str] = __name__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BeetkeeperPlugin(BeetsPlugin):
|
|
26
|
+
"""
|
|
27
|
+
Plugin which registers a number of beets event listeners, all of which submit a POST request to the beetkeeper
|
|
28
|
+
server. This allows the server to collect event data without 'peeking' into the beets database.
|
|
29
|
+
https://beets.readthedocs.io/en/stable/dev/plugins/events.html"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, beetkeeper_server_url: str, raw_api_token: str | None = None):
|
|
32
|
+
"""
|
|
33
|
+
Args:
|
|
34
|
+
beetkeeper_server_url: The base url to reach the beetkeeper server when submitting event-based HTTP requests.
|
|
35
|
+
raw_api_token: Optional beetkeeper API authentication token string.
|
|
36
|
+
"""
|
|
37
|
+
self._client = _BeetKeeperClient(url=beetkeeper_server_url, api_token=_APIToken(value=raw_api_token or ""))
|
|
38
|
+
super().__init__("beetkeeper_listener")
|
|
39
|
+
self.register_listener("album_imported", self._on_album_import)
|
|
40
|
+
self.register_listener("album_removed", self._on_album_removed)
|
|
41
|
+
self.register_listener("import_task_files", self._on_import_task_files)
|
|
42
|
+
self.register_listener("item_imported", self._on_item_imported)
|
|
43
|
+
self.register_listener("item_removed", self._on_item_removed)
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def log(self) -> logging.Logger:
|
|
47
|
+
return logging.getLogger(_LOGGER_NAME)
|
|
48
|
+
|
|
49
|
+
def _on_album_import(self, lib: Library, album: Album) -> None:
|
|
50
|
+
self.log.debug("Run listener for 'album_imported' ...")
|
|
51
|
+
self._client.post(event_type="album_imported", event_element=album)
|
|
52
|
+
|
|
53
|
+
def _on_album_removed(self, lib: Library, album: Album) -> None:
|
|
54
|
+
self.log.debug("Run listener for 'album_removed' ...")
|
|
55
|
+
self._client.post(event_type="album_removed", event_element=album)
|
|
56
|
+
|
|
57
|
+
def _on_import_task_files(self, task: ImportTask, session: ImportSession) -> None:
|
|
58
|
+
self.log.debug("Run listener for 'import_task_files' ...")
|
|
59
|
+
self._client.post(event_type="import_task_files", event_element=task)
|
|
60
|
+
|
|
61
|
+
def _on_item_imported(self, lib: Library, item: Item) -> None:
|
|
62
|
+
self.log.debug("Run listener for 'item_imported' ...")
|
|
63
|
+
self._client.post(event_type="item_imported", event_element=item)
|
|
64
|
+
|
|
65
|
+
def _on_item_removed(self, item: Item) -> None:
|
|
66
|
+
self.log.debug("Run listener for 'item_removed' ...")
|
|
67
|
+
self._client.post(event_type="item_removed", event_element=item)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _BeetKeeperClient(RequestHandler):
|
|
71
|
+
"""
|
|
72
|
+
Custom `RequestHandler` for submitting beetkeeper API requests to the `/api/events` endpoint.
|
|
73
|
+
See code in `beets.importer.session.ImportSession` to understand why we can't run a long-lived async client.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
_base_path: ClassVar[str] = "/api/events"
|
|
77
|
+
|
|
78
|
+
def __init__(self, url: str, api_token: _APIToken):
|
|
79
|
+
self._url = url
|
|
80
|
+
self._api_token = api_token
|
|
81
|
+
super().__init__()
|
|
82
|
+
|
|
83
|
+
def create_session(self):
|
|
84
|
+
return TimeoutAndRetrySession(auth=_BkAuth(token=self._api_token), url=self._url)
|
|
85
|
+
|
|
86
|
+
def post(self, event_type: EventType, event_element: Album | Item | ImportTask | ImportSession) -> Response:
|
|
87
|
+
"""Submits a POST reques to the Beetkeeper server, with a request body containing relevant beets event info."""
|
|
88
|
+
return self.request(
|
|
89
|
+
method="post",
|
|
90
|
+
url=self._base_path + self._url_subpath(event_element=event_element),
|
|
91
|
+
json=_jsonify(event_type=event_type, event_element=event_element),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _url_subpath(self, event_element: Album | Item | ImportTask | ImportSession) -> str:
|
|
95
|
+
match event_element:
|
|
96
|
+
case Album():
|
|
97
|
+
return "/album"
|
|
98
|
+
case Item():
|
|
99
|
+
return "/track"
|
|
100
|
+
case _:
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _jsonify(event_type: EventType, event_element: Album | Item | ImportTask | ImportSession) -> dict[str, Any]:
|
|
105
|
+
"""Returns a JSON-serializable dictionary with the beets event details. Result is used as POST request body."""
|
|
106
|
+
if isinstance(event_element, (Album, Item)):
|
|
107
|
+
return dict(event_type=event_type, beets_field={k: v for k, v in event_element.items()})
|
|
108
|
+
elif isinstance(event_type, ImportTask):
|
|
109
|
+
jsonified_imported_items = []
|
|
110
|
+
if imported_items := event_element.imported_items():
|
|
111
|
+
jsonified_imported_items = [_jsonify(event_type=event_type, event_element=item) for item in imported_items]
|
|
112
|
+
return dict(
|
|
113
|
+
event_type=event_type, choice_flag=event_element.choice_flag, imported_items=jsonified_imported_items
|
|
114
|
+
)
|
|
115
|
+
return dict(event_type=event_type, event_element="unknown")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class _BkAuth(AuthBase):
|
|
119
|
+
"""https://requests.readthedocs.io/en/latest/user/authentication/"""
|
|
120
|
+
|
|
121
|
+
_token: _APIToken
|
|
122
|
+
|
|
123
|
+
def __init__(self, token: _APIToken):
|
|
124
|
+
self._token = token
|
|
125
|
+
|
|
126
|
+
def __call__(self, r: PreparedRequest):
|
|
127
|
+
r.headers["Authorization"] = self._token.header_value
|
|
128
|
+
return r
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class _APIToken:
|
|
133
|
+
"""Wrapper class for a BeetKeeper API token."""
|
|
134
|
+
|
|
135
|
+
value: str = field(repr=False) # mitigate logging the token accidentally.
|
|
136
|
+
|
|
137
|
+
def get_value(self) -> str:
|
|
138
|
+
return self.value
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def header_value(self) -> str:
|
|
142
|
+
return f"Bearer {self.value}"
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beetkeeper-plugin
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Beets plugin client to integrate with the beetkeeper server.
|
|
5
|
+
Author: Zach Gottesman
|
|
6
|
+
Maintainer: zach-overflow
|
|
7
|
+
License-Expression: AGPL-3.0-or-later
|
|
8
|
+
Project-URL: Documentation, https://github.com/zach-overflow/beetkeeper/README.md
|
|
9
|
+
Project-URL: Repository, https://github.com/zach-overflow/beetkeeper
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/zach-overflow/beetkeeper/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/zach-overflow/beetkeeper/releases
|
|
12
|
+
Keywords: beets,beets plugin,music tagging,music-library,music tags,self-hosted
|
|
13
|
+
Requires-Python: <3.15,>=3.14
|
|
14
|
+
Requires-Dist: beets>=2.11.0
|
|
15
|
+
Requires-Dist: requests>=2.34
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
beetkeeper_plugin/beetkeeper_plugin.py,sha256=sOzwqM4KWzLmuJQroUh2-d021rVTsDehDEF3AA62hPk,5991
|
|
2
|
+
beetkeeper_plugin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
beetkeeper_plugin-0.0.2.dist-info/METADATA,sha256=tq1vB3s0ZPIyAtmsAmd4OYu_tNf1FjI-HEpFX4GHPX8,692
|
|
4
|
+
beetkeeper_plugin-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
beetkeeper_plugin-0.0.2.dist-info/top_level.txt,sha256=tojtaChievoLWZADraQU7YBFqZ21vhJ6aSDEKyF89q8,18
|
|
6
|
+
beetkeeper_plugin-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
beetkeeper_plugin
|