1+ import json
12import re
23import threading
4+ from pathlib import Path
35from typing import Dict , List , Optional
46
57import requests
68from .models import AnimeResult , Episode
9+ from .storage import atomic_write_json
710
811# Default credentials - can be overridden with environment variables
912# This is for analytics and also api credentials fetching.
@@ -19,8 +22,59 @@ def _get_endpoint_config() -> tuple[str, str]:
1922
2023
2124class APICache :
25+ CACHE_FILENAME = "api_credentials.json"
26+
27+ def __init__ (self ):
28+ home_dir = Path .home ()
29+ db_dir = home_dir / ".ani-cli-arabic" / "database"
30+ db_dir .mkdir (parents = True , exist_ok = True )
31+ self .cache_file = db_dir / self .CACHE_FILENAME
32+
33+ @staticmethod
34+ def _default_keys () -> dict :
35+ return {
36+ 'ANI_CLI_AR_API_BASE' : '' ,
37+ 'ANI_CLI_AR_TOKEN' : '' ,
38+ 'THUMBNAILS_BASE_URL' : '' ,
39+ 'TRAILERS_BASE_URL' : ''
40+ }
41+
42+ @staticmethod
43+ def _normalize_keys (data : dict ) -> dict :
44+ defaults = APICache ._default_keys ()
45+ if not isinstance (data , dict ):
46+ return defaults
47+ return {key : str (data .get (key , defaults [key ]) or '' ) for key in defaults }
48+
49+ def _load_cached_keys (self ) -> Optional [dict ]:
50+ if not self .cache_file .exists ():
51+ return None
52+
53+ try :
54+ with open (self .cache_file , 'r' , encoding = 'utf-8' ) as cache_handle :
55+ cached = json .load (cache_handle )
56+
57+ normalized = self ._normalize_keys (cached )
58+ if normalized ['ANI_CLI_AR_API_BASE' ] and normalized ['ANI_CLI_AR_TOKEN' ]:
59+ return normalized
60+ except (json .JSONDecodeError , OSError , IOError , ValueError , TypeError ):
61+ return None
62+
63+ return None
64+
65+ def _save_cached_keys (self , keys : dict ) -> None :
66+ normalized = self ._normalize_keys (keys )
67+ if not normalized ['ANI_CLI_AR_API_BASE' ] or not normalized ['ANI_CLI_AR_TOKEN' ]:
68+ return
69+
70+ try :
71+ atomic_write_json (self .cache_file , normalized , indent = 2 , ensure_ascii = False )
72+ except OSError :
73+ pass
74+
2275 def _fetch_from_remote (self ) -> dict :
2376 endpoint_url , auth_secret = _get_endpoint_config ()
77+ cached = self ._load_cached_keys ()
2478
2579 try :
2680 response = requests .get (
@@ -32,22 +86,17 @@ def _fetch_from_remote(self) -> dict:
3286 timeout = 10
3387 )
3488
35- if response .status_code == 200 :
36- return response .json ()
37- else :
38- return {
39- 'ANI_CLI_AR_API_BASE' : '' ,
40- 'ANI_CLI_AR_TOKEN' : '' ,
41- 'THUMBNAILS_BASE_URL' : '' ,
42- 'TRAILERS_BASE_URL' : ''
43- }
44- except Exception :
45- return {
46- 'ANI_CLI_AR_API_BASE' : '' ,
47- 'ANI_CLI_AR_TOKEN' : '' ,
48- 'THUMBNAILS_BASE_URL' : '' ,
49- 'TRAILERS_BASE_URL' : ''
50- }
89+ response .raise_for_status ()
90+ remote_keys = self ._normalize_keys (response .json ())
91+ if remote_keys ['ANI_CLI_AR_API_BASE' ] and remote_keys ['ANI_CLI_AR_TOKEN' ]:
92+ self ._save_cached_keys (remote_keys )
93+ return remote_keys
94+ except (requests .RequestException , ValueError , TypeError ):
95+ pass
96+
97+ if cached :
98+ return cached
99+ return self ._default_keys ()
51100
52101 def get_keys (self ) -> dict :
53102 return self ._fetch_from_remote ()
@@ -78,15 +127,15 @@ def _ensure_creds():
78127
79128def get_api_base ():
80129 _ensure_creds ()
81- return _creds [ 'ANI_CLI_AR_API_BASE' ]
130+ return _creds . get ( 'ANI_CLI_AR_API_BASE' , '' )
82131
83132def get_api_token ():
84133 _ensure_creds ()
85- return _creds [ 'ANI_CLI_AR_TOKEN' ]
134+ return _creds . get ( 'ANI_CLI_AR_TOKEN' , '' )
86135
87136def get_thumbnails_base ():
88137 _ensure_creds ()
89- return _creds [ 'THUMBNAILS_BASE_URL' ]
138+ return _creds . get ( 'THUMBNAILS_BASE_URL' , '' )
90139
91140def get_trailers_base ():
92141 _ensure_creds ()
0 commit comments