Enforced timestamps for online streams.
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 5s
CI / Tests (push) Successful in 34s
CI / Type Checking (push) Successful in 12s
CI / Spelling (push) Successful in 10s

This commit is contained in:
2026-05-25 21:49:21 -04:00
parent 85d3adf775
commit aeab5e1320
3 changed files with 271 additions and 11 deletions
+7 -10
View File
@@ -15,7 +15,7 @@
"""Command handlers for OwncastSentry bot commands."""
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast
from .types import (
AlreadySubscribedError,
@@ -187,11 +187,11 @@ class CommandHandler:
# Determine status and duration (as a sub-bullet)
match stream_state.status:
case StreamStatus.ONLINE if stream_state.status_since:
duration = _format_duration(stream_state.status_since, now)
parts.append(f" - Status: Online for {duration} \n")
case StreamStatus.ONLINE:
parts.append(" - Status: Online \n")
duration = _format_duration(
cast("str", stream_state.status_since), now
)
parts.append(f" - Status: Online for {duration} \n")
case StreamStatus.UNKNOWN:
parts.append(" - Status: Unknown (instance unreachable) \n")
case StreamStatus.OFFLINE if stream_state.status_since:
@@ -259,11 +259,8 @@ class CommandHandler:
parts.append(f" - Title: {safe_title} \n")
# Add status with duration
if stream_state.status_since:
duration = _format_duration(stream_state.status_since, now)
parts.append(f" - Online for {duration} \n")
else:
parts.append(" - Online \n")
duration = _format_duration(cast("str", stream_state.status_since), now)
parts.append(f" - Online for {duration} \n")
# Add stream link
parts.append(f" - Link: https://{domain}\n\n")
+114
View File
@@ -313,6 +313,120 @@ async def upgrade_v5(conn: Connection) -> None:
)
@upgrade_table.register( # type: ignore[arg-type, call-arg, untyped-decorator]
description="Require timestamps for online streams"
)
async def upgrade_v6(conn: Connection) -> None:
"""Upgrade database schema to version 6 format.
Rebuilds the stream table with a constraint that requires online streams
to have a status timestamp. Legacy rows that are marked online without a
timestamp are treated as offline because their live start time is unknown.
:param conn: A connection to run the v6 database migration on.
"""
await conn.execute(
"""CREATE TABLE "streams_new" (
"domain" TEXT NOT NULL CHECK(length(trim("domain")) > 0),
"name" TEXT,
"title" TEXT,
"online" INTEGER NOT NULL DEFAULT 0 CHECK("online" IN (0, 1)),
"status_since" TEXT,
"failure_counter" INTEGER NOT NULL DEFAULT 0
CHECK("failure_counter" >= 0),
CHECK("online" = 0 OR "status_since" IS NOT NULL),
PRIMARY KEY("domain")
)"""
)
await conn.execute(
"""INSERT INTO streams_new (
domain, name, title, online, status_since, failure_counter
)
SELECT domain,
name,
title,
CASE
WHEN online = 1 AND status_since IS NOT NULL THEN 1
ELSE 0
END,
status_since,
failure_counter
FROM streams"""
)
await conn.execute(
"""CREATE TABLE "subscriptions_v6_copy" (
"stream_domain" TEXT NOT NULL,
"room_id" TEXT NOT NULL,
PRIMARY KEY("room_id", "stream_domain")
)"""
)
await conn.execute(
"""INSERT INTO subscriptions_v6_copy (stream_domain, room_id)
SELECT stream_domain, room_id
FROM subscriptions
WHERE EXISTS (
SELECT 1
FROM streams_new
WHERE streams_new.domain = subscriptions.stream_domain
)"""
)
await conn.execute('DROP TRIGGER IF EXISTS "create_stream_for_subscription"')
await conn.execute('DROP TRIGGER IF EXISTS "delete_unsubscribed_stream"')
await conn.execute('DROP INDEX IF EXISTS "subscriptions_stream_domain_idx"')
await conn.execute("DROP TABLE subscriptions")
await conn.execute("DROP TABLE streams")
await conn.execute("ALTER TABLE streams_new RENAME TO streams")
await conn.execute(
"""CREATE TABLE "subscriptions_new" (
"stream_domain" TEXT NOT NULL
CHECK(length(trim("stream_domain")) > 0),
"room_id" TEXT NOT NULL CHECK(length(trim("room_id")) > 0),
PRIMARY KEY("room_id", "stream_domain"),
FOREIGN KEY("stream_domain")
REFERENCES "streams"("domain")
ON DELETE CASCADE
)"""
)
await conn.execute(
"""INSERT INTO subscriptions_new (stream_domain, room_id)
SELECT stream_domain, room_id
FROM subscriptions_v6_copy"""
)
await conn.execute("DROP TABLE subscriptions_v6_copy")
await conn.execute("ALTER TABLE subscriptions_new RENAME TO subscriptions")
await conn.execute(
"""CREATE INDEX "subscriptions_stream_domain_idx"
ON "subscriptions"("stream_domain")"""
)
await conn.execute(
"""CREATE TRIGGER "create_stream_for_subscription"
BEFORE INSERT ON "subscriptions"
BEGIN
INSERT INTO "streams" ("domain")
VALUES (NEW."stream_domain")
ON CONFLICT("domain") DO NOTHING;
END"""
)
await conn.execute(
"""CREATE TRIGGER "delete_unsubscribed_stream"
AFTER DELETE ON "subscriptions"
WHEN NOT EXISTS (
SELECT 1
FROM "subscriptions"
WHERE "stream_domain" = OLD."stream_domain"
)
BEGIN
DELETE FROM "streams"
WHERE "domain" = OLD."stream_domain";
END"""
)
def get_upgrade_table() -> UpgradeTable:
"""Return the repository upgrade table with registered migrations."""
return upgrade_table