1111
1212class 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"
0 commit comments