From f62764a2b22af215c4bdabe05e414586f3ffa81e Mon Sep 17 00:00:00 2001 From: Logan Fick Date: Wed, 7 Jan 2026 11:33:55 -0500 Subject: [PATCH] Fixed unreachable streams reporting stale online status indefinitely. --- owncastsentry/commands.py | 12 ++++++++---- owncastsentry/models.py | 22 +++++++++++++++++++--- owncastsentry/utils.py | 3 +++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/owncastsentry/commands.py b/owncastsentry/commands.py index 9cc2e60..5034f9c 100644 --- a/owncastsentry/commands.py +++ b/owncastsentry/commands.py @@ -11,6 +11,7 @@ from mautrix.types import TextMessageEventContent, MessageType from .owncast_client import OwncastClient from .database import StreamRepository, SubscriptionRepository +from .models import StreamStatus from .utils import domainify, sanitize_for_markdown @@ -209,18 +210,21 @@ class CommandHandler: body_text += f"- **{safe_stream_name}** \n" # Add title if stream is online (as a sub-bullet) - if stream_state.online and stream_state.title: + if stream_state.status == StreamStatus.ONLINE and stream_state.title: safe_title = sanitize_for_markdown(stream_state.title) body_text += f" - Title: {safe_title} \n" # Determine status and duration (as a sub-bullet) - if stream_state.online: + if stream_state.status == StreamStatus.ONLINE: # Stream is online - use last_connect_time if stream_state.last_connect_time: duration = self._format_duration(stream_state.last_connect_time) body_text += f" - Status: Online for {duration} \n" else: body_text += f" - Status: Online \n" + elif stream_state.status == StreamStatus.UNKNOWN: + # Stream status is unknown - instance unreachable + body_text += f" - Status: Unknown (instance unreachable) \n" else: # Stream is offline - use last_disconnect_time if stream_state.last_disconnect_time: @@ -255,11 +259,11 @@ class CommandHandler: await evt.reply("This room is not subscribed to any Owncast instances.\n\nTo subscribe to an Owncast instance, use `!subscribe `", markdown=True) return - # Filter for only live streams + # Filter for only live streams (exclude unknown status) live_streams = [] for domain in subscribed_domains: stream_state = await self.stream_repo.get_by_domain(domain) - if stream_state and stream_state.online: + if stream_state and stream_state.status == StreamStatus.ONLINE: live_streams.append((domain, stream_state)) # Check if there are no live streams diff --git a/owncastsentry/models.py b/owncastsentry/models.py index 9a91b60..60d0cf4 100644 --- a/owncastsentry/models.py +++ b/owncastsentry/models.py @@ -5,9 +5,18 @@ # 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. from dataclasses import dataclass +from enum import Enum from typing import Optional, List +class StreamStatus(Enum): + """Represents the status of a stream.""" + + ONLINE = "online" + OFFLINE = "offline" + UNKNOWN = "unknown" + + @dataclass class StreamState: """Represents the state of an Owncast stream.""" @@ -20,9 +29,16 @@ class StreamState: failure_counter: int = 0 @property - def online(self) -> bool: - """Returns True if the stream is currently online.""" - return self.last_connect_time is not None + def status(self) -> StreamStatus: + """Returns the stream status considering failure_counter.""" + from .utils import UNKNOWN_STATUS_THRESHOLD + + if self.failure_counter > UNKNOWN_STATUS_THRESHOLD: + return StreamStatus.UNKNOWN + elif self.last_connect_time is not None: + return StreamStatus.ONLINE + else: + return StreamStatus.OFFLINE @classmethod def from_api_response(cls, response: dict, domain: str) -> "StreamState": diff --git a/owncastsentry/utils.py b/owncastsentry/utils.py index 09bf49f..a94acd8 100644 --- a/owncastsentry/utils.py +++ b/owncastsentry/utils.py @@ -33,6 +33,9 @@ TEMPORARY_OFFLINE_NOTIFICATION_COOLDOWN = 7 * 60 # 7 minutes in seconds CLEANUP_WARNING_THRESHOLD = 83 * 24 * 60 # 119,520 cycles = 83 days CLEANUP_DELETE_THRESHOLD = 90 * 24 * 60 # 129,600 cycles = 90 days +# Failure counter threshold for treating stream status as "unknown" +UNKNOWN_STATUS_THRESHOLD = 15 + # Maximum field lengths based on Owncast's configuration # Source: https://github.com/owncast/owncast/blob/master/web/utils/config-constants.tsx MAX_INSTANCE_TITLE_LENGTH = 255 # Server Name (line 81)