Modernized codebase with tooling configuration and CI/CD workflows.
- Replaced legacy typing (Optional, List, Type, Union, Tuple) with PEP 604/585 equivalents. - Added pyproject.toml with configurations for hatch-vcs, mypy, ruff, and codespell. - Added CI workflows for formatting, linting, type checking, and spelling. - Added CD workflow for building and uploading plugin artifacts on push to master and version tags. - Added dependency auditing workflow with pip-audit. - Added comprehensive docstrings and inline comments across all modules. - Fixed User-Agent header using hardcoded version instead of actual plugin version. - Fixed grammar and terminology in log messages and comments. - Removed unreachable error handling branch in unsubscribe command.
This commit is contained in:
@@ -1,47 +1,68 @@
|
||||
# Copyright 2026 Logan Fick
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""HTTP client for querying Owncast instance APIs."""
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import aiohttp
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
from .models import StreamConfig, StreamState
|
||||
from .utils import OWNCAST_STATUS_PATH, OWNCAST_CONFIG_PATH, USER_AGENT
|
||||
from .utils import OWNCAST_CONFIG_PATH, OWNCAST_STATUS_PATH, user_agent
|
||||
|
||||
|
||||
class OwncastClient:
|
||||
"""HTTP client for communicating with Owncast instances."""
|
||||
|
||||
def __init__(self, logger):
|
||||
"""
|
||||
Initialize the Owncast client with an HTTP session.
|
||||
def __init__(self, logger: logging.Logger, version: str) -> None:
|
||||
"""Initialize the Owncast client with an HTTP session.
|
||||
|
||||
:param logger: Logger instance for debugging
|
||||
:param version: Plugin version string for the User-Agent header
|
||||
"""
|
||||
self.log = logger
|
||||
|
||||
# Set up HTTP session configuration
|
||||
headers = {"User-Agent": USER_AGENT}
|
||||
headers = {"User-Agent": user_agent(version)}
|
||||
cookie_jar = aiohttp.DummyCookieJar()
|
||||
connector = aiohttp.TCPConnector(
|
||||
use_dns_cache=False, limit=1000, limit_per_host=1, keepalive_timeout=120
|
||||
use_dns_cache=False,
|
||||
limit=1000,
|
||||
limit_per_host=1,
|
||||
keepalive_timeout=120,
|
||||
)
|
||||
timeout = aiohttp.ClientTimeout(sock_connect=5, sock_read=5)
|
||||
|
||||
self.session = aiohttp.ClientSession(
|
||||
headers=headers, cookie_jar=cookie_jar, timeout=timeout, connector=connector
|
||||
headers=headers,
|
||||
cookie_jar=cookie_jar,
|
||||
timeout=timeout,
|
||||
connector=connector,
|
||||
)
|
||||
|
||||
async def get_stream_state(self, domain: str) -> Optional[StreamState]:
|
||||
"""
|
||||
Get the current stream state for a given domain.
|
||||
HTTPS on port 443 is assumed, no other protocols or ports are supported.
|
||||
async def get_stream_state(self, domain: str) -> StreamState | None:
|
||||
"""Get the current stream state for a given domain.
|
||||
|
||||
HTTPS on port 443 is assumed, no other protocols or ports
|
||||
are supported.
|
||||
|
||||
:param domain: The domain (not URL) where the stream is hosted.
|
||||
:return: A StreamState with stream state if available, None if an error occurred.
|
||||
:return: A StreamState if available, None on error.
|
||||
"""
|
||||
self.log.debug(f"[{domain}] Fetching current stream state...")
|
||||
status_url = "https://" + domain + OWNCAST_STATUS_PATH
|
||||
@@ -60,20 +81,24 @@ class OwncastClient:
|
||||
# Check the response code is success
|
||||
if response.status != 200:
|
||||
self.log.warning(
|
||||
f"[{domain}] Response to request on {OWNCAST_STATUS_PATH} was not 200, got {response.status} instead."
|
||||
f"[{domain}] Response to request on "
|
||||
f"{OWNCAST_STATUS_PATH} was not 200, "
|
||||
f"got {response.status} instead."
|
||||
)
|
||||
return None
|
||||
|
||||
# Try and interpret the response as JSON
|
||||
# Try to interpret the response as JSON
|
||||
try:
|
||||
new_state = json.loads(await response.read())
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
f"[{domain}] Rejecting response to request on {OWNCAST_STATUS_PATH} as could not be interpreted as JSON: {e}"
|
||||
f"[{domain}] Rejecting response to request on "
|
||||
f"{OWNCAST_STATUS_PATH} as could not be "
|
||||
f"interpreted as JSON: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Validate the response to ensure it contains all the basic info needed to function
|
||||
# Validate the response contains all basic info needed
|
||||
required_fields = [
|
||||
"lastConnectTime",
|
||||
"lastDisconnectTime",
|
||||
@@ -83,19 +108,22 @@ class OwncastClient:
|
||||
for field in required_fields:
|
||||
if field not in new_state:
|
||||
self.log.warning(
|
||||
f"[{domain}] Rejecting response to request on {OWNCAST_STATUS_PATH} as it does not have {field} parameter."
|
||||
f"[{domain}] Rejecting response to request "
|
||||
f"on {OWNCAST_STATUS_PATH} as it does not "
|
||||
f"have {field} field."
|
||||
)
|
||||
return None
|
||||
|
||||
return StreamState.from_api_response(new_state, domain)
|
||||
|
||||
async def get_stream_config(self, domain: str) -> Optional[StreamConfig]:
|
||||
"""
|
||||
Get the current stream config for a given domain.
|
||||
HTTPS on port 443 is assumed, no other protocols or ports are supported.
|
||||
async def get_stream_config(self, domain: str) -> StreamConfig | None:
|
||||
"""Get the current stream config for a given domain.
|
||||
|
||||
HTTPS on port 443 is assumed, no other protocols or ports
|
||||
are supported.
|
||||
|
||||
:param domain: The domain (not URL) where the stream is hosted.
|
||||
:return: A StreamConfig with the stream's configuration, or None if fetch failed.
|
||||
:return: A StreamConfig, or None if fetch failed.
|
||||
"""
|
||||
self.log.debug(f"[{domain}] Fetching current stream config...")
|
||||
config_url = "https://" + domain + OWNCAST_CONFIG_PATH
|
||||
@@ -114,25 +142,28 @@ class OwncastClient:
|
||||
# Check the response code is success
|
||||
if response.status != 200:
|
||||
self.log.warning(
|
||||
f"[{domain}] Response to request on {OWNCAST_CONFIG_PATH} was not 200, got {response.status} instead."
|
||||
f"[{domain}] Response to request on "
|
||||
f"{OWNCAST_CONFIG_PATH} was not 200, "
|
||||
f"got {response.status} instead."
|
||||
)
|
||||
return None
|
||||
|
||||
# Try and interpret the response as JSON
|
||||
# Try to interpret the response as JSON
|
||||
try:
|
||||
config = json.loads(await response.read())
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
f"[{domain}] Rejecting response to request on {OWNCAST_CONFIG_PATH} as could not be interpreted as JSON: {e}"
|
||||
f"[{domain}] Rejecting response to request on "
|
||||
f"{OWNCAST_CONFIG_PATH} as could not be "
|
||||
f"interpreted as JSON: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Create StreamConfig with validated fields
|
||||
# Create StreamConfig from response (fields are truncated to max lengths)
|
||||
return StreamConfig.from_api_response(config)
|
||||
|
||||
async def validate_instance(self, domain: str) -> bool:
|
||||
"""
|
||||
Validate that a domain is a valid Owncast instance.
|
||||
"""Validate that a domain is a valid Owncast instance.
|
||||
|
||||
:param domain: The domain to validate
|
||||
:return: True if valid Owncast instance, False otherwise
|
||||
|
||||
Reference in New Issue
Block a user