Add show API to bot lib

This commit is contained in:
Speng Reb 2026-05-21 16:04:47 +02:00
parent 12d01f9d6d
commit 952440bb05
5 changed files with 163 additions and 1 deletions

View file

@ -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. **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 ## Moderation
```python ```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 } playlist = bot.api.get_playlist() # { items, currentIndex, locked }
bot.api.skip_to(uid) bot.api.skip_to(uid)
bot.api.set_user_rank("Alice", 2) bot.api.set_user_rank("Alice", 2)
bot.api.list_shows()
bot.api.show_action(show_id, "run")
``` ```
## Error handling ## Error handling

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "veretube-bot" name = "veretube-bot"
version = "0.1.3" version = "0.1.4"
description = "Python bot library for veretube sync channels" description = "Python bot library for veretube sync channels"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"

View file

@ -119,3 +119,69 @@ class BotAPI:
def update_settings(self, **kwargs) -> None: def update_settings(self, **kwargs) -> None:
"""Update one or more channel settings. Requires rank >= OWNER and channel active.""" """Update one or more channel settings. Requires rank >= OWNER and channel active."""
self._request("PUT", "/settings", json=kwargs) 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})

View file

@ -246,3 +246,21 @@ class AsyncBot:
async def clear_playlist(self): async def clear_playlist(self):
await asyncio.to_thread(self.api.clear_playlist) 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)

View file

@ -324,6 +324,35 @@ class Bot:
"""Clear the entire playlist. Requires rank >= ADMIN.""" """Clear the entire playlist. Requires rank >= ADMIN."""
self.api.clear_playlist() 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 ──────────────────────────────────────────────────────────────── # ── Emotes ────────────────────────────────────────────────────────────────
def get_emotes(self) -> list: def get_emotes(self) -> list: