mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-06 18:44:14 -05:00
Cache playlist based on type
This commit is contained in:
@@ -42,9 +42,9 @@ class OasisCloudClient:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the OasisCloudClient.
|
Initialize the OasisCloudClient.
|
||||||
|
|
||||||
Sets the optional aiohttp session and access token, records whether the client owns the session, and initializes caches and asyncio locks for playlists and software metadata.
|
Sets the optional aiohttp session and access token, records whether the client owns the session, and initializes caches and asyncio locks for playlists and software metadata.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
session (ClientSession | None): Optional aiohttp ClientSession to use. If None, the client will create and own a session later.
|
session (ClientSession | None): Optional aiohttp ClientSession to use. If None, the client will create and own a session later.
|
||||||
access_token (str | None): Optional initial access token for authenticated requests.
|
access_token (str | None): Optional initial access token for authenticated requests.
|
||||||
@@ -53,9 +53,11 @@ class OasisCloudClient:
|
|||||||
self._owns_session = session is None
|
self._owns_session = session is None
|
||||||
self._access_token = access_token
|
self._access_token = access_token
|
||||||
|
|
||||||
|
now_dt = now()
|
||||||
|
|
||||||
# playlists cache
|
# playlists cache
|
||||||
self.playlists: list[dict[str, Any]] = []
|
self._playlists_cache: dict[bool, list[dict[str, Any]]] = {False: [], True: []}
|
||||||
self._playlists_next_refresh = now()
|
self._playlists_next_refresh = {False: now_dt, True: now_dt}
|
||||||
self._playlists_lock = asyncio.Lock()
|
self._playlists_lock = asyncio.Lock()
|
||||||
|
|
||||||
self._playlist_details: dict[int, dict[str, str]] = {}
|
self._playlist_details: dict[int, dict[str, str]] = {}
|
||||||
@@ -65,11 +67,25 @@ class OasisCloudClient:
|
|||||||
self._software_next_refresh = now()
|
self._software_next_refresh = now()
|
||||||
self._software_lock = asyncio.Lock()
|
self._software_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playlists(self) -> list[dict]:
|
||||||
|
"""Return all cached playlists, deduplicated by ID."""
|
||||||
|
seen = set()
|
||||||
|
merged: list[dict] = []
|
||||||
|
|
||||||
|
for items in self._playlists_cache.values():
|
||||||
|
for pl in items:
|
||||||
|
if (pid := pl.get("id")) not in seen:
|
||||||
|
seen.add(pid)
|
||||||
|
merged.append(pl)
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self) -> ClientSession:
|
def session(self) -> ClientSession:
|
||||||
"""
|
"""
|
||||||
Get the active aiohttp ClientSession, creating and owning a new session if none exists or the existing session is closed.
|
Get the active aiohttp ClientSession, creating and owning a new session if none exists or the existing session is closed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ClientSession: The active aiohttp ClientSession; a new session is created and marked as owned by this client when necessary.
|
ClientSession: The active aiohttp ClientSession; a new session is created and marked as owned by this client when necessary.
|
||||||
"""
|
"""
|
||||||
@@ -81,7 +97,7 @@ class OasisCloudClient:
|
|||||||
async def async_close(self) -> None:
|
async def async_close(self) -> None:
|
||||||
"""
|
"""
|
||||||
Close the aiohttp ClientSession owned by this client if it exists and is open.
|
Close the aiohttp ClientSession owned by this client if it exists and is open.
|
||||||
|
|
||||||
This should be called during teardown when the client is responsible for the session.
|
This should be called during teardown when the client is responsible for the session.
|
||||||
"""
|
"""
|
||||||
if self._session and not self._session.closed and self._owns_session:
|
if self._session and not self._session.closed and self._owns_session:
|
||||||
@@ -91,7 +107,7 @@ class OasisCloudClient:
|
|||||||
def access_token(self) -> str | None:
|
def access_token(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
Access token used for authenticated requests or None if not set.
|
Access token used for authenticated requests or None if not set.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The current access token string, or `None` if no token is stored.
|
The current access token string, or `None` if no token is stored.
|
||||||
"""
|
"""
|
||||||
@@ -101,7 +117,7 @@ class OasisCloudClient:
|
|||||||
def access_token(self, value: str | None) -> None:
|
def access_token(self, value: str | None) -> None:
|
||||||
"""
|
"""
|
||||||
Set the access token used for authenticated requests.
|
Set the access token used for authenticated requests.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
value (str | None): The bearer token to store; pass None to clear the stored token.
|
value (str | None): The bearer token to store; pass None to clear the stored token.
|
||||||
"""
|
"""
|
||||||
@@ -110,7 +126,7 @@ class OasisCloudClient:
|
|||||||
async def async_login(self, email: str, password: str) -> None:
|
async def async_login(self, email: str, password: str) -> None:
|
||||||
"""
|
"""
|
||||||
Log in to the Oasis cloud and store the received access token on the client.
|
Log in to the Oasis cloud and store the received access token on the client.
|
||||||
|
|
||||||
Performs an authentication request using the provided credentials and saves the returned access token to self.access_token for use in subsequent authenticated requests.
|
Performs an authentication request using the provided credentials and saves the returned access token to self.access_token for use in subsequent authenticated requests.
|
||||||
"""
|
"""
|
||||||
response = await self._async_request(
|
response = await self._async_request(
|
||||||
@@ -125,7 +141,7 @@ class OasisCloudClient:
|
|||||||
async def async_logout(self) -> None:
|
async def async_logout(self) -> None:
|
||||||
"""
|
"""
|
||||||
End the current authenticated session with the Oasis cloud.
|
End the current authenticated session with the Oasis cloud.
|
||||||
|
|
||||||
Performs a logout request and clears the stored access token on success.
|
Performs a logout request and clears the stored access token on success.
|
||||||
"""
|
"""
|
||||||
await self._async_auth_request("GET", "api/auth/logout")
|
await self._async_auth_request("GET", "api/auth/logout")
|
||||||
@@ -134,10 +150,10 @@ class OasisCloudClient:
|
|||||||
async def async_get_user(self) -> dict:
|
async def async_get_user(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Return information about the currently authenticated user.
|
Return information about the currently authenticated user.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A mapping containing the user's details as returned by the cloud API.
|
dict: A mapping containing the user's details as returned by the cloud API.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
UnauthenticatedError: If no access token is available or the request is unauthorized.
|
UnauthenticatedError: If no access token is available or the request is unauthorized.
|
||||||
"""
|
"""
|
||||||
@@ -146,7 +162,7 @@ class OasisCloudClient:
|
|||||||
async def async_get_devices(self) -> list[dict[str, Any]]:
|
async def async_get_devices(self) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Retrieve the current user's devices from the cloud API.
|
Retrieve the current user's devices from the cloud API.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[dict[str, Any]]: A list of device objects as returned by the API.
|
list[dict[str, Any]]: A list of device objects as returned by the API.
|
||||||
"""
|
"""
|
||||||
@@ -157,12 +173,12 @@ class OasisCloudClient:
|
|||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Retrieve playlists from the Oasis cloud, optionally limited to the authenticated user's personal playlists.
|
Retrieve playlists from the Oasis cloud, optionally limited to the authenticated user's personal playlists.
|
||||||
|
|
||||||
The result is cached and will be refreshed according to PLAYLISTS_REFRESH_LIMITER to avoid frequent network requests.
|
The result is cached and will be refreshed according to PLAYLISTS_REFRESH_LIMITER to avoid frequent network requests.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
personal_only (bool): If True, return only playlists owned by the authenticated user; otherwise return all available playlists.
|
personal_only (bool): If True, return only playlists owned by the authenticated user; otherwise return all available playlists.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[dict[str, Any]]: A list of playlist objects represented as dictionaries; an empty list if no playlists are available.
|
list[dict[str, Any]]: A list of playlist objects represented as dictionaries; an empty list if no playlists are available.
|
||||||
"""
|
"""
|
||||||
@@ -171,20 +187,22 @@ class OasisCloudClient:
|
|||||||
def _is_cache_valid() -> bool:
|
def _is_cache_valid() -> bool:
|
||||||
"""
|
"""
|
||||||
Determine whether the playlists cache is still valid.
|
Determine whether the playlists cache is still valid.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`true` if the playlists cache contains data and the next refresh timestamp is later than the current time, `false` otherwise.
|
`true` if the playlists cache contains data and the next refresh timestamp is later than the current time, `false` otherwise.
|
||||||
"""
|
"""
|
||||||
return self._playlists_next_refresh > now_dt and bool(self.playlists)
|
cache = self._playlists_cache[personal_only]
|
||||||
|
next_refresh = self._playlists_next_refresh[personal_only]
|
||||||
|
return bool(cache) and next_refresh > now_dt
|
||||||
|
|
||||||
if _is_cache_valid():
|
if _is_cache_valid():
|
||||||
return self.playlists
|
return self._playlists_cache[personal_only]
|
||||||
|
|
||||||
async with self._playlists_lock:
|
async with self._playlists_lock:
|
||||||
# Double-check in case another task just refreshed it
|
# Double-check in case another task just refreshed it
|
||||||
now_dt = now()
|
now_dt = now()
|
||||||
if _is_cache_valid():
|
if _is_cache_valid():
|
||||||
return self.playlists
|
return self._playlists_cache[personal_only]
|
||||||
|
|
||||||
params = {"my_playlists": str(personal_only).lower()}
|
params = {"my_playlists": str(personal_only).lower()}
|
||||||
playlists = await self._async_auth_request(
|
playlists = await self._async_auth_request(
|
||||||
@@ -194,15 +212,17 @@ class OasisCloudClient:
|
|||||||
if not isinstance(playlists, list):
|
if not isinstance(playlists, list):
|
||||||
playlists = []
|
playlists = []
|
||||||
|
|
||||||
self.playlists = playlists
|
self._playlists_cache[personal_only] = playlists
|
||||||
self._playlists_next_refresh = now_dt + PLAYLISTS_REFRESH_LIMITER
|
self._playlists_next_refresh[personal_only] = (
|
||||||
|
now_dt + PLAYLISTS_REFRESH_LIMITER
|
||||||
|
)
|
||||||
|
|
||||||
return self.playlists
|
return playlists
|
||||||
|
|
||||||
async def async_get_track_info(self, track_id: int) -> dict[str, Any] | None:
|
async def async_get_track_info(self, track_id: int) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Retrieve information for a single track from the cloud.
|
Retrieve information for a single track from the cloud.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Track detail dictionary. If the track is not found (HTTP 404), returns a dict with keys `id` and `name` where `name` is "Unknown Title (#{id})". Returns `None` on other failures.
|
dict: Track detail dictionary. If the track is not found (HTTP 404), returns a dict with keys `id` and `name` where `name` is "Unknown Title (#{id})". Returns `None` on other failures.
|
||||||
"""
|
"""
|
||||||
@@ -220,10 +240,10 @@ class OasisCloudClient:
|
|||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Retrieve track details for the given track IDs, following pagination until all pages are fetched.
|
Retrieve track details for the given track IDs, following pagination until all pages are fetched.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
tracks (list[int] | None): Optional list of track IDs to request. If omitted or None, an empty list is sent to the API.
|
tracks (list[int] | None): Optional list of track IDs to request. If omitted or None, an empty list is sent to the API.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[dict[str, Any]]: A list of track detail dictionaries returned by the cloud, aggregated across all pages (may be empty).
|
list[dict[str, Any]]: A list of track detail dictionaries returned by the cloud, aggregated across all pages (may be empty).
|
||||||
"""
|
"""
|
||||||
@@ -245,19 +265,19 @@ class OasisCloudClient:
|
|||||||
) -> dict[str, int | str] | None:
|
) -> dict[str, int | str] | None:
|
||||||
"""
|
"""
|
||||||
Retrieve the latest software metadata from the cloud, using an internal cache to limit requests.
|
Retrieve the latest software metadata from the cloud, using an internal cache to limit requests.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
force_refresh (bool): If True, bypass the cache and fetch fresh metadata from the cloud.
|
force_refresh (bool): If True, bypass the cache and fetch fresh metadata from the cloud.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
details (dict[str, int | str] | None): A mapping of software metadata keys to integer or string values, or `None` if no metadata is available.
|
details (dict[str, int | str] | None): A mapping of software metadata keys to integer or string values, or `None` if no metadata is available.
|
||||||
"""
|
"""
|
||||||
now_dt = now()
|
now_dt = now()
|
||||||
|
|
||||||
def _is_cache_valid() -> bool:
|
def _is_cache_valid() -> bool:
|
||||||
"""
|
"""
|
||||||
Determine whether the cached software metadata should be used instead of fetching fresh data.
|
Determine whether the cached software metadata should be used instead of fetching fresh data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the software cache exists, has not expired, and a force refresh was not requested; False otherwise.
|
True if the software cache exists, has not expired, and a force refresh was not requested; False otherwise.
|
||||||
"""
|
"""
|
||||||
@@ -289,18 +309,18 @@ class OasisCloudClient:
|
|||||||
async def _async_auth_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
async def _async_auth_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Perform a cloud API request using the stored access token.
|
Perform a cloud API request using the stored access token.
|
||||||
|
|
||||||
If `url` is relative it will be joined with the module `BASE_URL`. The method will
|
If `url` is relative it will be joined with the module `BASE_URL`. The method will
|
||||||
inject an `Authorization: Bearer <token>` header into the request.
|
inject an `Authorization: Bearer <token>` header into the request.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
method (str): HTTP method (e.g., "GET", "POST").
|
method (str): HTTP method (e.g., "GET", "POST").
|
||||||
url (str): Absolute URL or path relative to `BASE_URL`.
|
url (str): Absolute URL or path relative to `BASE_URL`.
|
||||||
**kwargs: Passed through to the underlying request helper.
|
**kwargs: Passed through to the underlying request helper.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The parsed response value (JSON object, text, or None) as returned by the underlying request helper.
|
The parsed response value (JSON object, text, or None) as returned by the underlying request helper.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
UnauthenticatedError: If no access token is set.
|
UnauthenticatedError: If no access token is set.
|
||||||
"""
|
"""
|
||||||
@@ -320,7 +340,7 @@ class OasisCloudClient:
|
|||||||
async def _async_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
async def _async_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Perform a single HTTP request and return a normalized response value.
|
Perform a single HTTP request and return a normalized response value.
|
||||||
|
|
||||||
Performs the request using the client's session and:
|
Performs the request using the client's session and:
|
||||||
- If the response status is 200:
|
- If the response status is 200:
|
||||||
- returns parsed JSON for `application/json`.
|
- returns parsed JSON for `application/json`.
|
||||||
@@ -329,15 +349,15 @@ class OasisCloudClient:
|
|||||||
- returns `None` for other content types.
|
- returns `None` for other content types.
|
||||||
- If the response status is 401, raises UnauthenticatedError.
|
- If the response status is 401, raises UnauthenticatedError.
|
||||||
- For other non-200 statuses, re-raises the response's HTTP error.
|
- For other non-200 statuses, re-raises the response's HTTP error.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
method: HTTP method to use (e.g., "GET", "POST").
|
method: HTTP method to use (e.g., "GET", "POST").
|
||||||
url: Request URL or path.
|
url: Request URL or path.
|
||||||
**kwargs: Passed through to the session request (e.g., `params`, `json`, `headers`).
|
**kwargs: Passed through to the session request (e.g., `params`, `json`, `headers`).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The parsed JSON object, response text, or `None` depending on the response content type.
|
The parsed JSON object, response text, or `None` depending on the response content type.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
UnauthenticatedError: when the server indicates the client is unauthenticated (401) or a cloud login page is returned.
|
UnauthenticatedError: when the server indicates the client is unauthenticated (401) or a cloud login page is returned.
|
||||||
aiohttp.ClientResponseError: for other non-success HTTP statuses raised by `response.raise_for_status()`.
|
aiohttp.ClientResponseError: for other non-success HTTP statuses raised by `response.raise_for_status()`.
|
||||||
@@ -366,4 +386,4 @@ class OasisCloudClient:
|
|||||||
if response.status == 401:
|
if response.status == 401:
|
||||||
raise UnauthenticatedError("Unauthenticated")
|
raise UnauthenticatedError("Unauthenticated")
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|||||||
Reference in New Issue
Block a user