From 952440bb05a195a11b7c52ae577b7918f5020c92 Mon Sep 17 00:00:00 2001 From: Speng Reb Date: Thu, 21 May 2026 16:04:47 +0200 Subject: [PATCH] Add show API to bot lib --- README.md | 49 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- veretube_bot/_api.py | 66 +++++++++++++++++++++++++++++++++++++++ veretube_bot/async_bot.py | 18 +++++++++++ veretube_bot/bot.py | 29 +++++++++++++++++ 5 files changed, 163 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eecf325..66f6d2e 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,53 @@ bot.clear_playlist() # clear all (rank >= ADMIN) **Media types:** `yt` YouTube, `sc` SoundCloud, `tw` Twitch stream, `tc` Twitch clip, `vm` Vimeo, `dm` Dailymotion, `fi` direct file URL, `cu` custom embed. +## Shows + +Show endpoints manage scheduled playlist runs. They support bot Bearer auth and session auth. + +```python +shows = bot.list_shows() # rank >= MOD +show = bot.get_show(show_id) # rank >= MOD + +payload = { + "name": "Friday Prime", + "scheduled_for": "2026-05-22T19:00:00.000Z", + "timezone": "America/New_York", + "recurrence": "weekly", + "fill_mode": "replace", + "conflict_mode": "force", + "start_playback": True, + "playlist": [ + {"type": "yt", "id": "dQw4w9WgXcQ", "pos": "end"} + ], + "status": "scheduled", +} + +created = bot.create_show(payload) # rank >= MOD +updated = bot.update_show(show_id, payload) # rank >= MOD +bot.delete_show(show_id) # rank >= ADMIN + +bot.show_action(show_id, "pause") # rank >= MOD +bot.show_action(show_id, "resume") # rank >= MOD +bot.show_action(show_id, "schedule") # rank >= MOD +bot.show_action(show_id, "run") # rank >= ADMIN +bot.show_action(show_id, "cancel") # rank >= ADMIN +``` + +Create/update payload constraints: +- `timezone`: required non-empty IANA time zone region string from the IANA Time Zone Database (for example `Europe/Berlin`, `America/New_York`) +- `recurrence`: `none` | `daily` | `weekly` +- `fill_mode`: `append` | `replace` +- `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) + +Action payload schema: + +```json +{ "action": "run" } +``` + ## Moderation ```python @@ -216,6 +263,8 @@ Every REST endpoint is also accessible on `bot.api` if you need something not co playlist = bot.api.get_playlist() # { items, currentIndex, locked } bot.api.skip_to(uid) bot.api.set_user_rank("Alice", 2) +bot.api.list_shows() +bot.api.show_action(show_id, "run") ``` ## Error handling diff --git a/pyproject.toml b/pyproject.toml index 648feed..dc24f65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "veretube-bot" -version = "0.1.3" +version = "0.1.4" description = "Python bot library for veretube sync channels" readme = "README.md" license = "MIT" diff --git a/veretube_bot/_api.py b/veretube_bot/_api.py index 631dfc1..77fc950 100644 --- a/veretube_bot/_api.py +++ b/veretube_bot/_api.py @@ -119,3 +119,69 @@ class BotAPI: def update_settings(self, **kwargs) -> None: """Update one or more channel settings. Requires rank >= OWNER and channel active.""" self._request("PUT", "/settings", json=kwargs) + + # ── Shows ───────────────────────────────────────────────────────────────── + + def _validate_show_payload(self, payload: dict) -> None: + timezone = payload.get("timezone") + if not isinstance(timezone, str) or not timezone.strip(): + raise ValueError("timezone is required and must be a non-empty IANA timezone string") + + recurrence = payload.get("recurrence") + if recurrence is not None and recurrence not in {"none", "daily", "weekly"}: + raise ValueError("recurrence must be one of: none, daily, weekly") + + fill_mode = payload.get("fill_mode") + if fill_mode is not None and fill_mode not in {"append", "replace"}: + raise ValueError("fill_mode must be one of: append, replace") + + conflict_mode = payload.get("conflict_mode") + if conflict_mode is not None and conflict_mode not in {"force", "skip"}: + raise ValueError("conflict_mode must be one of: force, skip") + + status = payload.get("status") + 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") + + playlist = payload.get("playlist") + if not isinstance(playlist, list) or not playlist: + raise ValueError("playlist is required and must be a non-empty list") + for index, item in enumerate(playlist): + if not isinstance(item, dict): + raise ValueError(f"playlist[{index}] must be an object") + if not item.get("type") or not item.get("id"): + raise ValueError(f"playlist[{index}] must include non-empty type and id") + pos = item.get("pos") + if pos is not None and pos not in {"next", "end"}: + raise ValueError(f"playlist[{index}].pos must be one of: next, end") + + def list_shows(self) -> list: + """List channel shows. Requires rank >= MOD.""" + return self._request("GET", "/shows") + + 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}") + + def create_show(self, payload: dict) -> dict: + """Create a show. Requires rank >= MOD.""" + self._validate_show_payload(payload) + return self._request("POST", "/shows", json=payload) + + def update_show(self, show_id: int | str, payload: dict) -> dict: + """Update a show by id. Requires rank >= MOD.""" + self._validate_show_payload(payload) + return self._request("PUT", f"/shows/{show_id}", json=payload) + + def delete_show(self, show_id: int | str) -> None: + """Delete a show by id. Requires rank >= ADMIN.""" + self._request("DELETE", f"/shows/{show_id}") + + def show_action(self, show_id: int | str, action: str) -> dict: + """ + Run a show action. + pause/resume/schedule require rank >= MOD. run/cancel require rank >= ADMIN. + """ + if action not in {"pause", "resume", "schedule", "run", "cancel"}: + raise ValueError("action must be one of: pause, resume, schedule, run, cancel") + return self._request("POST", f"/shows/{show_id}/action", json={"action": action}) diff --git a/veretube_bot/async_bot.py b/veretube_bot/async_bot.py index fc26ba4..b00e5ed 100644 --- a/veretube_bot/async_bot.py +++ b/veretube_bot/async_bot.py @@ -246,3 +246,21 @@ class AsyncBot: async def clear_playlist(self): await asyncio.to_thread(self.api.clear_playlist) + + async def list_shows(self) -> list: + return await asyncio.to_thread(self.api.list_shows) + + async def get_show(self, show_id: int | str) -> dict: + return await asyncio.to_thread(self.api.get_show, show_id) + + async def create_show(self, payload: dict) -> dict: + return await asyncio.to_thread(self.api.create_show, payload) + + async def update_show(self, show_id: int | str, payload: dict) -> dict: + return await asyncio.to_thread(self.api.update_show, show_id, payload) + + async def delete_show(self, show_id: int | str): + await asyncio.to_thread(self.api.delete_show, show_id) + + async def show_action(self, show_id: int | str, action: str) -> dict: + return await asyncio.to_thread(self.api.show_action, show_id, action) diff --git a/veretube_bot/bot.py b/veretube_bot/bot.py index 2737935..424e431 100644 --- a/veretube_bot/bot.py +++ b/veretube_bot/bot.py @@ -324,6 +324,35 @@ class Bot: """Clear the entire playlist. Requires rank >= ADMIN.""" self.api.clear_playlist() + # ── Shows ───────────────────────────────────────────────────────────────── + + def list_shows(self) -> list: + """List scheduled shows. Requires rank >= MOD.""" + return self.api.list_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) + + def create_show(self, payload: dict) -> dict: + """Create a show. Requires rank >= MOD.""" + return self.api.create_show(payload) + + def update_show(self, show_id: int | str, payload: dict) -> dict: + """Update a show by id. Requires rank >= MOD.""" + return self.api.update_show(show_id, payload) + + def delete_show(self, show_id: int | str): + """Delete a show by id. Requires rank >= ADMIN.""" + self.api.delete_show(show_id) + + def show_action(self, show_id: int | str, action: str) -> dict: + """ + Run show action. + pause/resume/schedule require rank >= MOD. run/cancel require rank >= ADMIN. + """ + return self.api.show_action(show_id, action) + # ── Emotes ──────────────────────────────────────────────────────────────── def get_emotes(self) -> list: