diff --git a/README.md b/README.md index 66f6d2e..bd51b49 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ Show endpoints manage scheduled playlist runs. They support bot Bearer auth and ```python shows = bot.list_shows() # rank >= MOD +public_shows = bot.list_public_shows() # read-only public schedule show = bot.get_show(show_id) # rank >= MOD payload = { @@ -195,6 +196,8 @@ payload = { {"type": "yt", "id": "dQw4w9WgXcQ", "pos": "end"} ], "status": "scheduled", + "notes": "
Special guest this week.
", + "color": "#22AAEE", } created = bot.create_show(payload) # rank >= MOD @@ -215,6 +218,8 @@ Create/update payload constraints: - `conflict_mode`: `force` | `skip` - `playlist`: required non-empty array of media entries with `type`, `id`, and optional `pos` (`next` | `end`) - `status`: `draft` | `scheduled` | `paused` | `completed` | `failed` | `canceled` (`running` is internal) +- `notes`: optional `string | null` (rich HTML allowed; expected max input length 20000 chars before sanitize) +- `color`: optional `string | null`, must match `^#[0-9A-Fa-f]{6}$` when provided Action payload schema: diff --git a/pyproject.toml b/pyproject.toml index dc24f65..9b3d2ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "veretube-bot" -version = "0.1.4" -description = "Python bot library for veretube sync channels" +version = "0.1.5" +description = "Python bot library for veretube/sync v4.0 channels" readme = "README.md" license = "MIT" requires-python = ">=3.10" diff --git a/veretube_bot/_api.py b/veretube_bot/_api.py index 77fc950..685dade 100644 --- a/veretube_bot/_api.py +++ b/veretube_bot/_api.py @@ -1,4 +1,5 @@ import urllib.parse +import re import requests @@ -122,6 +123,8 @@ class BotAPI: # ── Shows ───────────────────────────────────────────────────────────────── + _SHOW_COLOR_RE = re.compile(r"^#[0-9A-Fa-f]{6}$") + def _validate_show_payload(self, payload: dict) -> None: timezone = payload.get("timezone") if not isinstance(timezone, str) or not timezone.strip(): @@ -143,6 +146,20 @@ class BotAPI: if status is not None and status not in {"draft", "scheduled", "paused", "completed", "failed", "canceled"}: raise ValueError("status must be one of: draft, scheduled, paused, completed, failed, canceled") + notes = payload.get("notes") + if notes is not None: + if not isinstance(notes, str): + raise ValueError("notes must be a string or null") + if len(notes) > 20000: + raise ValueError("notes must be at most 20000 characters") + + color = payload.get("color") + if color is not None: + if not isinstance(color, str): + raise ValueError("color must be a string or null") + if not self._SHOW_COLOR_RE.fullmatch(color): + raise ValueError("color must match /^#[0-9A-Fa-f]{6}$/") + playlist = payload.get("playlist") if not isinstance(playlist, list) or not playlist: raise ValueError("playlist is required and must be a non-empty list") @@ -159,6 +176,10 @@ class BotAPI: """List channel shows. Requires rank >= MOD.""" return self._request("GET", "/shows") + def list_public_shows(self) -> list: + """List public show schedule. Read-only.""" + return self._request("GET", "/shows/public") + def get_show(self, show_id: int | str) -> dict: """Get a single show by id. Requires rank >= MOD.""" return self._request("GET", f"/shows/{show_id}") diff --git a/veretube_bot/async_bot.py b/veretube_bot/async_bot.py index b00e5ed..f20f610 100644 --- a/veretube_bot/async_bot.py +++ b/veretube_bot/async_bot.py @@ -250,6 +250,9 @@ class AsyncBot: async def list_shows(self) -> list: return await asyncio.to_thread(self.api.list_shows) + async def list_public_shows(self) -> list: + return await asyncio.to_thread(self.api.list_public_shows) + async def get_show(self, show_id: int | str) -> dict: return await asyncio.to_thread(self.api.get_show, show_id) diff --git a/veretube_bot/bot.py b/veretube_bot/bot.py index 424e431..c1b2e59 100644 --- a/veretube_bot/bot.py +++ b/veretube_bot/bot.py @@ -330,6 +330,10 @@ class Bot: """List scheduled shows. Requires rank >= MOD.""" return self.api.list_shows() + def list_public_shows(self) -> list: + """List public scheduled shows. Read-only.""" + return self.api.list_public_shows() + def get_show(self, show_id: int | str) -> dict: """Get a single show by id. Requires rank >= MOD.""" return self.api.get_show(show_id)