Skip to content

Commit fbde3fd

Browse files
authored
[Fix] ABSClient dynamic config: allow web UI settings to take effect without restart (#133)
Resolved configuration caching bug where ABSClient cached base_url and token at instantiation, preventing web UI settings changes from taking effect until container restart. Changes: - Converted base_url, token, and headers to @Property decorators (dynamic from os.environ) - Added _update_session_headers() helper called before all API requests - Added URL scheme validation to warn about missing http:// or https:// - Matches KoSyncClient pattern for consistency Impact: - Web UI ABS settings changes now take effect immediately (no restart needed) - Fixes "Invalid URL" and 401 auth errors after settings updates - No breaking changes - all existing code continues to work Also includes: - Hardcover token cleanup (strip whitespace and "Bearer " prefix)
1 parent 753a59d commit fbde3fd

File tree

2 files changed

+47
-6
lines changed

2 files changed

+47
-6
lines changed

src/api/api_clients.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,33 @@
1111

1212
class ABSClient:
1313
def __init__(self):
14-
# Kept your variable names (ABS_SERVER / ABS_KEY)
15-
self.base_url = os.environ.get("ABS_SERVER", "").rstrip('/')
16-
self.token = os.environ.get("ABS_KEY")
17-
self.headers = {"Authorization": f"Bearer {self.token}"}
14+
# Configuration is now dynamic via properties (no caching)
1815
self.session = requests.Session()
19-
self.session.headers.update(self.headers)
2016
self.timeout = 30
2117

18+
@property
19+
def base_url(self):
20+
"""Dynamic base_url from environment (no caching)."""
21+
url = os.environ.get("ABS_SERVER", "").rstrip('/')
22+
# Validate URL scheme to help catch configuration errors
23+
if url and not url.startswith(('http://', 'https://')):
24+
logger.warning(f"⚠️ ABS_SERVER missing http:// or https:// scheme: {url}")
25+
return url
26+
27+
@property
28+
def token(self):
29+
"""Dynamic token from environment (no caching)."""
30+
return os.environ.get("ABS_KEY")
31+
32+
@property
33+
def headers(self):
34+
"""Dynamic headers with current token."""
35+
return {"Authorization": f"Bearer {self.token}"}
36+
37+
def _update_session_headers(self):
38+
"""Update session headers with current token (called before requests)."""
39+
self.session.headers.update(self.headers)
40+
2241
def is_configured(self):
2342
"""Check if ABS is configured with URL and token."""
2443
return bool(self.base_url and self.token)
@@ -29,6 +48,7 @@ def check_connection(self):
2948
logger.warning("⚠️ Audiobookshelf not configured (skipping)")
3049
return False
3150

51+
self._update_session_headers()
3252
url = f"{self.base_url}/api/me"
3353
try:
3454
r = self.session.get(url, timeout=self.timeout)
@@ -60,6 +80,7 @@ def check_connection(self):
6080

6181
def get_all_audiobooks(self):
6282
if not self.is_configured(): return []
83+
self._update_session_headers()
6384
lib_url = f"{self.base_url}/api/libraries"
6485
try:
6586
r = self.session.get(lib_url, timeout=self.timeout)
@@ -76,6 +97,7 @@ def get_all_audiobooks(self):
7697

7798
def get_audiobooks_for_lib(self, lib: str):
7899
if not self.is_configured(): return []
100+
self._update_session_headers()
79101
items_url = f"{self.base_url}/api/libraries/{lib}/items"
80102
params = {"mediaType": "audiobook"}
81103
r_items = self.session.get(items_url, params=params, timeout=self.timeout)
@@ -86,6 +108,7 @@ def get_audiobooks_for_lib(self, lib: str):
86108

87109
def get_audio_files(self, item_id):
88110
if not self.is_configured(): return []
111+
self._update_session_headers()
89112
url = f"{self.base_url}/api/items/{item_id}"
90113
try:
91114
r = self.session.get(url, timeout=self.timeout)
@@ -112,6 +135,7 @@ def get_audio_files(self, item_id):
112135
def get_ebook_files(self, item_id):
113136
"""Get ebook files for an item (from libraryFiles)."""
114137
if not self.is_configured(): return []
138+
self._update_session_headers()
115139
url = f"{self.base_url}/api/items/{item_id}"
116140
try:
117141
r = self.session.get(url, timeout=self.timeout)
@@ -139,6 +163,7 @@ def get_ebook_files(self, item_id):
139163
def search_ebooks(self, query):
140164
"""Search for ebooks across all book libraries."""
141165
if not self.is_configured(): return []
166+
self._update_session_headers()
142167
results = []
143168
try:
144169
# Get all libraries first
@@ -203,6 +228,7 @@ def search_ebooks(self, query):
203228

204229
def download_file(self, stream_url, output_path):
205230
"""Download file from stream_url to output_path."""
231+
self._update_session_headers()
206232
try:
207233
logger.info(f"⬇️ ABS: Downloading file from {stream_url}...")
208234
with self.session.get(stream_url, stream=True, timeout=120) as r:
@@ -221,6 +247,7 @@ def download_file(self, stream_url, output_path):
221247

222248
def get_item_details(self, item_id):
223249
if not self.is_configured(): return None
250+
self._update_session_headers()
224251
url = f"{self.base_url}/api/items/{item_id}"
225252
try:
226253
r = self.session.get(url, timeout=self.timeout)
@@ -231,6 +258,7 @@ def get_item_details(self, item_id):
231258

232259
def get_progress(self, item_id):
233260
if not self.is_configured(): return None
261+
self._update_session_headers()
234262
url = f"{self.base_url}/api/me/progress/{item_id}"
235263
try:
236264
r = self.session.get(url, timeout=self.timeout)
@@ -254,6 +282,7 @@ def update_ebook_progress(self, item_id, progress, location):
254282
logger.error("❌ Ebook location is required for progress updates")
255283
return False
256284

285+
self._update_session_headers()
257286
# Ensure we use a float for the progress
258287
progress = float(progress)
259288
url = f"{self.base_url}/api/me/progress/{item_id}"
@@ -300,6 +329,7 @@ def update_progress_using_payload(self, abs_id, payload: dict):
300329
logger.error(f"❌ Failed to create ABS session for item '{abs_id}'")
301330
return {"success": False, "code": None, "reason": f"Failed to create ABS session for item {abs_id}"}
302331

332+
self._update_session_headers()
303333
try:
304334
url = f"{self.base_url}/api/session/{session_id}/sync"
305335
r = self.session.post(url, json=payload, timeout=self.timeout)
@@ -319,6 +349,7 @@ def update_progress_using_payload(self, abs_id, payload: dict):
319349

320350
def get_all_progress_raw(self):
321351
"""Fetch all user progress in one API call."""
352+
self._update_session_headers()
322353
# Try specific progress endpoint first
323354
url = f"{self.base_url}/api/me/progress"
324355
try:
@@ -357,6 +388,7 @@ def get_all_progress_raw(self):
357388

358389
def get_in_progress(self, min_progress=0.01):
359390
"""Fetch in-progress items, optimized to avoid redundant detail fetches if possible."""
391+
self._update_session_headers()
360392
url = f"{self.base_url}/api/me/progress"
361393
try:
362394
r = self.session.get(url, timeout=self.timeout)
@@ -398,6 +430,7 @@ def get_in_progress(self, min_progress=0.01):
398430

399431
def create_session(self, abs_id):
400432
"""Create a new ABS session for the given abs_id (item id). Returns session_id or None."""
433+
self._update_session_headers()
401434
play_url = f"{self.base_url}/api/items/{abs_id}/play"
402435
play_payload = {
403436
"deviceInfo": {
@@ -427,6 +460,7 @@ def create_session(self, abs_id):
427460
return None
428461

429462
def close_session(self, session_id):
463+
self._update_session_headers()
430464
try:
431465
close_url = f"{self.base_url}/api/session/{session_id}/close"
432466
self.session.post(close_url, timeout=5)
@@ -437,7 +471,8 @@ def add_to_collection(self, item_id, collection_name=None):
437471
"""Add an audiobook to a collection, creating the collection if it doesn't exist."""
438472
if not collection_name:
439473
collection_name = os.environ.get("ABS_COLLECTION_NAME", "abs-kosync")
440-
474+
475+
self._update_session_headers()
441476
try:
442477
collections_url = f"{self.base_url}/api/collections"
443478
r = self.session.get(collections_url)
@@ -478,6 +513,7 @@ def add_to_collection(self, item_id, collection_name=None):
478513

479514
def remove_from_collection(self, item_id, collection_name="abs-kosync"):
480515
"""Remove an audiobook from a collection."""
516+
self._update_session_headers()
481517
try:
482518
# Get collection by name
483519
collections_url = f"{self.base_url}/api/collections"

src/api/hardcover_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ def __init__(self):
2828
self.token = os.environ.get("HARDCOVER_TOKEN")
2929
self.user_id = None
3030

31+
if self.token:
32+
self.token = self.token.strip()
33+
if self.token.lower().startswith("bearer "):
34+
self.token = self.token[7:].strip()
35+
3136
if not self.token:
3237
logger.info("HARDCOVER_TOKEN not set")
3338
return

0 commit comments

Comments
 (0)