v0.1.6
This commit is contained in:
parent
7f7c10d02c
commit
f435d2630e
5 changed files with 71 additions and 7 deletions
15
README.md
15
README.md
|
|
@ -182,11 +182,17 @@ Show endpoints manage scheduled playlist runs. They support bot Bearer auth and
|
||||||
```python
|
```python
|
||||||
shows = bot.list_shows() # rank >= MOD
|
shows = bot.list_shows() # rank >= MOD
|
||||||
public_shows = bot.list_public_shows() # read-only public schedule
|
public_shows = bot.list_public_shows() # read-only public schedule
|
||||||
|
resolved = bot.resolve_show_media([ # rank >= MOD, max 50 entries
|
||||||
|
{"type": "yt", "id": "dQw4w9WgXcQ"}
|
||||||
|
])
|
||||||
show = bot.get_show(show_id) # rank >= MOD
|
show = bot.get_show(show_id) # rank >= MOD
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"name": "Friday Prime",
|
"name": "Friday Prime",
|
||||||
|
"notes": "Opening block",
|
||||||
|
"color": "#337AB7",
|
||||||
"scheduled_for": "2026-05-22T19:00:00.000Z",
|
"scheduled_for": "2026-05-22T19:00:00.000Z",
|
||||||
|
"estimated_end_at": "2026-05-22T21:00:00.000Z",
|
||||||
"timezone": "America/New_York",
|
"timezone": "America/New_York",
|
||||||
"recurrence": "weekly",
|
"recurrence": "weekly",
|
||||||
"fill_mode": "replace",
|
"fill_mode": "replace",
|
||||||
|
|
@ -196,8 +202,6 @@ payload = {
|
||||||
{"type": "yt", "id": "dQw4w9WgXcQ", "pos": "end"}
|
{"type": "yt", "id": "dQw4w9WgXcQ", "pos": "end"}
|
||||||
],
|
],
|
||||||
"status": "scheduled",
|
"status": "scheduled",
|
||||||
"notes": "<p>Special guest this week.</p>",
|
|
||||||
"color": "#22AAEE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
created = bot.create_show(payload) # rank >= MOD
|
created = bot.create_show(payload) # rank >= MOD
|
||||||
|
|
@ -212,13 +216,16 @@ bot.show_action(show_id, "cancel") # rank >= ADMIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Create/update payload constraints:
|
Create/update payload constraints:
|
||||||
|
- `name`: required, 1-100 chars
|
||||||
|
- `scheduled_for`: required date string or unix timestamp (ms)
|
||||||
|
- `estimated_end_at`: optional date string/timestamp; must be later than `scheduled_for` when present
|
||||||
- `timezone`: required non-empty IANA time zone region string from the IANA Time Zone Database (for example `Europe/Berlin`, `America/New_York`)
|
- `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`
|
- `recurrence`: `none` | `daily` | `weekly`
|
||||||
- `fill_mode`: `append` | `replace`
|
- `fill_mode`: `append` | `replace`
|
||||||
- `conflict_mode`: `force` | `skip`
|
- `conflict_mode`: `force` | `skip`
|
||||||
- `playlist`: required non-empty array of media entries with `type`, `id`, and optional `pos` (`next` | `end`)
|
- `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)
|
- `status`: `draft` | `scheduled` | `paused` | `running` | `completed` | `failed` | `canceled` (`running` is accepted and normalized to `scheduled` on write)
|
||||||
- `notes`: optional `string | null` (rich HTML allowed; expected max input length 20000 chars before sanitize)
|
- `notes`: optional `string | null`, trimmed and capped to 20000 chars
|
||||||
- `color`: optional `string | null`, must match `^#[0-9A-Fa-f]{6}$` when provided
|
- `color`: optional `string | null`, must match `^#[0-9A-Fa-f]{6}$` when provided
|
||||||
|
|
||||||
Action payload schema:
|
Action payload schema:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "veretube-bot"
|
name = "veretube-bot"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
description = "Python bot library for veretube/sync v4.0 channels"
|
description = "Python bot library for veretube/sync v4.0 channels"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
@ -125,11 +126,47 @@ class BotAPI:
|
||||||
|
|
||||||
_SHOW_COLOR_RE = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
_SHOW_COLOR_RE = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _coerce_timestamp_ms(value):
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = value.strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
if text.endswith("Z"):
|
||||||
|
text = f"{text[:-1]}+00:00"
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(text).timestamp() * 1000.0
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
def _validate_show_payload(self, payload: dict) -> None:
|
def _validate_show_payload(self, payload: dict) -> None:
|
||||||
|
name = payload.get("name")
|
||||||
|
if not isinstance(name, str) or not name.strip():
|
||||||
|
raise ValueError("name is required and must be a non-empty string")
|
||||||
|
if len(name) > 100:
|
||||||
|
raise ValueError("name must be at most 100 characters")
|
||||||
|
|
||||||
timezone = payload.get("timezone")
|
timezone = payload.get("timezone")
|
||||||
if not isinstance(timezone, str) or not timezone.strip():
|
if not isinstance(timezone, str) or not timezone.strip():
|
||||||
raise ValueError("timezone is required and must be a non-empty IANA timezone string")
|
raise ValueError("timezone is required and must be a non-empty IANA timezone string")
|
||||||
|
|
||||||
|
scheduled_for = payload.get("scheduled_for")
|
||||||
|
if scheduled_for is None or (isinstance(scheduled_for, str) and not scheduled_for.strip()):
|
||||||
|
raise ValueError("scheduled_for is required and must be a date string or unix timestamp (ms)")
|
||||||
|
if not isinstance(scheduled_for, (str, int, float)):
|
||||||
|
raise ValueError("scheduled_for must be a date string or unix timestamp (ms)")
|
||||||
|
|
||||||
|
estimated_end_at = payload.get("estimated_end_at")
|
||||||
|
if estimated_end_at is not None and not isinstance(estimated_end_at, (str, int, float)):
|
||||||
|
raise ValueError("estimated_end_at must be a date string or unix timestamp (ms)")
|
||||||
|
start_ts = self._coerce_timestamp_ms(scheduled_for)
|
||||||
|
end_ts = self._coerce_timestamp_ms(estimated_end_at)
|
||||||
|
if estimated_end_at is not None and start_ts is not None and end_ts is not None and end_ts <= start_ts:
|
||||||
|
raise ValueError("estimated_end_at must be later than scheduled_for")
|
||||||
|
|
||||||
recurrence = payload.get("recurrence")
|
recurrence = payload.get("recurrence")
|
||||||
if recurrence is not None and recurrence not in {"none", "daily", "weekly"}:
|
if recurrence is not None and recurrence not in {"none", "daily", "weekly"}:
|
||||||
raise ValueError("recurrence must be one of: none, daily, weekly")
|
raise ValueError("recurrence must be one of: none, daily, weekly")
|
||||||
|
|
@ -143,8 +180,8 @@ class BotAPI:
|
||||||
raise ValueError("conflict_mode must be one of: force, skip")
|
raise ValueError("conflict_mode must be one of: force, skip")
|
||||||
|
|
||||||
status = payload.get("status")
|
status = payload.get("status")
|
||||||
if status is not None and status not in {"draft", "scheduled", "paused", "completed", "failed", "canceled"}:
|
if status is not None and status not in {"draft", "scheduled", "paused", "running", "completed", "failed", "canceled"}:
|
||||||
raise ValueError("status must be one of: draft, scheduled, paused, completed, failed, canceled")
|
raise ValueError("status must be one of: draft, scheduled, paused, running, completed, failed, canceled")
|
||||||
|
|
||||||
notes = payload.get("notes")
|
notes = payload.get("notes")
|
||||||
if notes is not None:
|
if notes is not None:
|
||||||
|
|
@ -180,6 +217,19 @@ class BotAPI:
|
||||||
"""List public show schedule. Read-only."""
|
"""List public show schedule. Read-only."""
|
||||||
return self._request("GET", "/shows/public")
|
return self._request("GET", "/shows/public")
|
||||||
|
|
||||||
|
def resolve_show_media(self, items: list[dict]) -> dict:
|
||||||
|
"""Resolve up to 50 media entries into display-ready titles. Requires rank >= MOD."""
|
||||||
|
if not isinstance(items, list) or not items:
|
||||||
|
raise ValueError("items is required and must be a non-empty list")
|
||||||
|
if len(items) > 50:
|
||||||
|
raise ValueError("items may contain at most 50 entries")
|
||||||
|
for index, item in enumerate(items):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValueError(f"items[{index}] must be an object")
|
||||||
|
if not item.get("type") or not item.get("id"):
|
||||||
|
raise ValueError(f"items[{index}] must include non-empty type and id")
|
||||||
|
return self._request("POST", "/shows/resolve-media", json={"items": items})
|
||||||
|
|
||||||
def get_show(self, show_id: int | str) -> dict:
|
def get_show(self, show_id: int | str) -> dict:
|
||||||
"""Get a single show by id. Requires rank >= MOD."""
|
"""Get a single show by id. Requires rank >= MOD."""
|
||||||
return self._request("GET", f"/shows/{show_id}")
|
return self._request("GET", f"/shows/{show_id}")
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,9 @@ class AsyncBot:
|
||||||
async def list_public_shows(self) -> list:
|
async def list_public_shows(self) -> list:
|
||||||
return await asyncio.to_thread(self.api.list_public_shows)
|
return await asyncio.to_thread(self.api.list_public_shows)
|
||||||
|
|
||||||
|
async def resolve_show_media(self, items: list[dict]) -> dict:
|
||||||
|
return await asyncio.to_thread(self.api.resolve_show_media, items)
|
||||||
|
|
||||||
async def get_show(self, show_id: int | str) -> dict:
|
async def get_show(self, show_id: int | str) -> dict:
|
||||||
return await asyncio.to_thread(self.api.get_show, show_id)
|
return await asyncio.to_thread(self.api.get_show, show_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,10 @@ class Bot:
|
||||||
"""List public scheduled shows. Read-only."""
|
"""List public scheduled shows. Read-only."""
|
||||||
return self.api.list_public_shows()
|
return self.api.list_public_shows()
|
||||||
|
|
||||||
|
def resolve_show_media(self, items: list[dict]) -> dict:
|
||||||
|
"""Resolve up to 50 media entries into display-ready titles. Requires rank >= MOD."""
|
||||||
|
return self.api.resolve_show_media(items)
|
||||||
|
|
||||||
def get_show(self, show_id: int | str) -> dict:
|
def get_show(self, show_id: int | str) -> dict:
|
||||||
"""Get a single show by id. Requires rank >= MOD."""
|
"""Get a single show by id. Requires rank >= MOD."""
|
||||||
return self.api.get_show(show_id)
|
return self.api.get_show(show_id)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue