kryten-webqueue 0.20.1__tar.gz → 0.22.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/CHANGELOG.md +14 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/api_gate/client.py +6 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/app.py +4 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/user.py +29 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/static/css/main.css +4 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/index.html +1 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/user/dashboard.html +75 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/pyproject.toml +1 -1
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/.gitignore +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/README.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/config.example.json +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/tests/test_search_facets.py +0 -0
|
@@ -4,7 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
## [
|
|
7
|
+
## [0.22.0] - 2026-06-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Shoutout on the Vanity Items tab.** The dashboard's Vanity tab gains a third item — **Shoutout** — alongside Greeting and Chat color, each now with a short call-to-action. Sending a shoutout posts the user's message to public chat (`📢 <user>: …`) via the new `POST /user/vanity/shoutout` route, which proxies the api-gate `POST /economy/vanity/shoutout` endpoint. The message is the user's own input (validated server-side: trimmed, non-empty, max 200 chars); the cost/availability come from the account summary; the username is always taken from the authenticated session.
|
|
12
|
+
|
|
13
|
+
## [0.21.0] — 2026-06-17
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Catalog sync no longer runs on startup.** The background sync loop now waits a full interval before its first run instead of syncing immediately when the process starts, so a restart won't kick off a sync. Admins can still trigger it on demand with the "Sync Catalog" button.
|
|
18
|
+
- **Friendlier job buttons.** Jobs that open a parameter dialog before running now show **"Begin…"** instead of "Run…", signalling that a dialog (with a Cancel) comes next rather than an immediate action. One-click jobs keep the plain **"Run"** label.
|
|
19
|
+
|
|
20
|
+
[0.21.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.21.0
|
|
8
21
|
|
|
9
22
|
## [0.20.1] — 2026-06-17
|
|
10
23
|
|
|
@@ -108,6 +108,12 @@ class ApiGateClient:
|
|
|
108
108
|
"value": value,
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
async def set_vanity_shoutout(self, username: str, value: str) -> dict:
|
|
112
|
+
return await self.post("/economy/vanity/shoutout", json={
|
|
113
|
+
"username": username,
|
|
114
|
+
"value": value,
|
|
115
|
+
})
|
|
116
|
+
|
|
111
117
|
async def queue_preview(self, username: str, duration_sec: int, tier: str = "queue") -> dict:
|
|
112
118
|
return await self.post("/economy/queue-preview", json={
|
|
113
119
|
"username": username,
|
|
@@ -163,12 +163,15 @@ async def lifespan(app: FastAPI):
|
|
|
163
163
|
# Background workers
|
|
164
164
|
async def _catalog_sync_loop():
|
|
165
165
|
interval = config.catalog_sync_interval_hours * 3600
|
|
166
|
+
# Sync on the interval, NOT immediately on startup — a restart should
|
|
167
|
+
# not trigger a catalog sync. Admins can run it on demand from the
|
|
168
|
+
# "Sync Catalog" button.
|
|
166
169
|
while True:
|
|
170
|
+
await asyncio.sleep(interval)
|
|
167
171
|
try:
|
|
168
172
|
await catalog_sync.sync()
|
|
169
173
|
except Exception as e:
|
|
170
174
|
logger.exception(f"Catalog sync error: {type(e).__name__}: {e}")
|
|
171
|
-
await asyncio.sleep(interval)
|
|
172
175
|
|
|
173
176
|
async def _otp_cleanup_loop():
|
|
174
177
|
while True:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from fastapi import APIRouter, Request, Depends, HTTPException
|
|
2
|
-
from pydantic import BaseModel
|
|
2
|
+
from pydantic import BaseModel, field_validator
|
|
3
3
|
|
|
4
4
|
from ..auth.session import get_current_user
|
|
5
5
|
|
|
@@ -28,6 +28,22 @@ class ColorUpdate(BaseModel):
|
|
|
28
28
|
value: str
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
class ShoutoutRequest(BaseModel):
|
|
32
|
+
# Mirrors the economy's shoutout limits so a bypassed UI still gets a
|
|
33
|
+
# consistent, early rejection instead of forwarding arbitrary-length text.
|
|
34
|
+
value: str
|
|
35
|
+
|
|
36
|
+
@field_validator("value")
|
|
37
|
+
@classmethod
|
|
38
|
+
def _validate_value(cls, v: str) -> str:
|
|
39
|
+
v = v.strip()
|
|
40
|
+
if not v:
|
|
41
|
+
raise ValueError("Shoutout message is required.")
|
|
42
|
+
if len(v) > 200:
|
|
43
|
+
raise ValueError("Message too long (max 200 characters).")
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
|
|
31
47
|
@router.post("/vanity/greeting")
|
|
32
48
|
async def set_vanity_greeting(
|
|
33
49
|
body: GreetingUpdate, request: Request, user: dict = Depends(get_current_user)
|
|
@@ -52,6 +68,18 @@ async def set_vanity_color(
|
|
|
52
68
|
raise HTTPException(status_code=400, detail=_economy_error(exc)) from exc
|
|
53
69
|
|
|
54
70
|
|
|
71
|
+
@router.post("/vanity/shoutout")
|
|
72
|
+
async def set_vanity_shoutout(
|
|
73
|
+
body: ShoutoutRequest, request: Request, user: dict = Depends(get_current_user)
|
|
74
|
+
):
|
|
75
|
+
"""Purchase a shoutout — the bot posts the message to public chat."""
|
|
76
|
+
api_gate = request.app.state.api_gate
|
|
77
|
+
try:
|
|
78
|
+
return await api_gate.set_vanity_shoutout(user["username"], body.value)
|
|
79
|
+
except Exception as exc: # noqa: BLE001
|
|
80
|
+
raise HTTPException(status_code=400, detail=_economy_error(exc)) from exc
|
|
81
|
+
|
|
82
|
+
|
|
55
83
|
def _economy_error(exc: Exception) -> str:
|
|
56
84
|
"""Extract a human-readable message from an api-gate HTTP error."""
|
|
57
85
|
import httpx
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/index.html
RENAMED
|
@@ -199,7 +199,7 @@ async function loadJobs() {
|
|
|
199
199
|
<span class="job-label">${escapeHtml(j.label)}${hasParams ? ' <span class="job-badge">params</span>' : ''}</span>
|
|
200
200
|
${summary}
|
|
201
201
|
<button class="btn btn-sm" onclick="startJob('${j.name}')" ${j.running ? 'disabled' : ''}>
|
|
202
|
-
${j.running ? 'Running…' : (hasParams ? '
|
|
202
|
+
${j.running ? 'Running…' : (hasParams ? 'Begin…' : 'Run')}
|
|
203
203
|
</button>
|
|
204
204
|
</div>`;
|
|
205
205
|
}).join('')
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
@@ -48,13 +48,14 @@
|
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<div class="tab-panel" id="tab-vanity" role="tabpanel" hidden>
|
|
51
|
-
<p class="muted">Personalize how the bot greets you
|
|
51
|
+
<p class="muted">Personalize how the bot greets you, your chat color, and shout yourself out. Each update is a one-time purchase.</p>
|
|
52
52
|
<div class="vanity-item">
|
|
53
53
|
<div class="vanity-row">
|
|
54
54
|
<span class="vanity-label">Greeting</span>
|
|
55
55
|
<button class="btn btn-sm" id="edit-greeting" disabled>Edit</button>
|
|
56
56
|
</div>
|
|
57
57
|
<div class="vanity-value" id="vanity-greeting">—</div>
|
|
58
|
+
<div class="vanity-cta muted" id="cta-greeting">The bot welcomes you by name when you join.</div>
|
|
58
59
|
</div>
|
|
59
60
|
<div class="vanity-item">
|
|
60
61
|
<div class="vanity-row">
|
|
@@ -62,6 +63,15 @@
|
|
|
62
63
|
<button class="btn btn-sm" id="edit-color" disabled>Edit</button>
|
|
63
64
|
</div>
|
|
64
65
|
<div class="vanity-value" id="vanity-color">—</div>
|
|
66
|
+
<div class="vanity-cta muted" id="cta-color">Stand out — pick a custom color for your chat messages.</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="vanity-item">
|
|
69
|
+
<div class="vanity-row">
|
|
70
|
+
<span class="vanity-label">Shoutout</span>
|
|
71
|
+
<button class="btn btn-sm" id="buy-shoutout" disabled>Send</button>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="vanity-value" id="vanity-shoutout">—</div>
|
|
74
|
+
<div class="vanity-cta muted" id="cta-shoutout">Have the bot post your message to the whole channel.</div>
|
|
65
75
|
</div>
|
|
66
76
|
</div>
|
|
67
77
|
</div>
|
|
@@ -223,6 +233,20 @@ function renderAccount(a) {
|
|
|
223
233
|
cBtn.disabled = enabled.custom_color === false;
|
|
224
234
|
gBtn.title = costs.custom_greeting ? `${fmt(costs.custom_greeting, sym)} per update` : '';
|
|
225
235
|
cBtn.title = costs.custom_color ? `${fmt(costs.custom_color, sym)} per update` : '';
|
|
236
|
+
|
|
237
|
+
const shoutEl = document.getElementById('vanity-shoutout');
|
|
238
|
+
const shoutCost = costs.shoutout;
|
|
239
|
+
const shoutAvailable = enabled.shoutout !== false && shoutCost != null;
|
|
240
|
+
if (shoutAvailable) {
|
|
241
|
+
shoutEl.textContent = `Post a one-off message to chat for ${fmt(shoutCost, sym)}.`;
|
|
242
|
+
shoutEl.classList.remove('vanity-unset');
|
|
243
|
+
} else {
|
|
244
|
+
shoutEl.textContent = 'Unavailable';
|
|
245
|
+
shoutEl.classList.add('vanity-unset');
|
|
246
|
+
}
|
|
247
|
+
const sBtn = document.getElementById('buy-shoutout');
|
|
248
|
+
sBtn.disabled = !shoutAvailable;
|
|
249
|
+
sBtn.title = shoutAvailable ? `${fmt(shoutCost, sym)} per shoutout` : '';
|
|
226
250
|
}
|
|
227
251
|
|
|
228
252
|
// ── Vanity edit dialogs ────────────────────────────────────────
|
|
@@ -328,6 +352,55 @@ async function saveColor() {
|
|
|
328
352
|
}
|
|
329
353
|
}
|
|
330
354
|
|
|
355
|
+
// ── Shoutout dialog ────────────────────────────────────────────
|
|
356
|
+
function buyShoutout() {
|
|
357
|
+
if (!accountState) return;
|
|
358
|
+
const sym = accountState.currency_symbol || 'Z';
|
|
359
|
+
const costs = accountState.vanity_costs || {};
|
|
360
|
+
const enabled = accountState.vanity_enabled || {};
|
|
361
|
+
const cost = costs.shoutout;
|
|
362
|
+
if (enabled.shoutout === false || cost == null) {
|
|
363
|
+
showToast('Shoutouts are currently unavailable', 'error');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
showModal(`
|
|
367
|
+
<h3>Shoutout</h3>
|
|
368
|
+
<p class="muted">The bot posts your message to public chat as <strong>📢 ${escapeHtml(accountState.username || 'you')}: …</strong>. Cost: ${fmt(cost, sym)} each.</p>
|
|
369
|
+
<label class="field"><span>Message (max 200 characters)</span>
|
|
370
|
+
<textarea id="shoutout-input" maxlength="200" rows="3" placeholder="Say something to the whole channel"></textarea></label>
|
|
371
|
+
<div class="modal-actions">
|
|
372
|
+
<button class="btn" onclick="closeModal()">Cancel</button>
|
|
373
|
+
<button class="btn btn-primary" id="shoutout-save">Send shoutout</button>
|
|
374
|
+
</div>`);
|
|
375
|
+
document.getElementById('shoutout-save').addEventListener('click', saveShoutout);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function saveShoutout() {
|
|
379
|
+
const value = document.getElementById('shoutout-input').value.trim();
|
|
380
|
+
if (!value) { showToast('Message cannot be empty', 'error'); return; }
|
|
381
|
+
const btn = document.getElementById('shoutout-save');
|
|
382
|
+
btn.disabled = true;
|
|
383
|
+
let resp;
|
|
384
|
+
try {
|
|
385
|
+
resp = await fetch('/user/vanity/shoutout', {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: {'Content-Type': 'application/json'},
|
|
388
|
+
body: JSON.stringify({value}),
|
|
389
|
+
});
|
|
390
|
+
} catch (e) {
|
|
391
|
+
showToast('Network error', 'error'); btn.disabled = false; return;
|
|
392
|
+
}
|
|
393
|
+
const data = await resp.json().catch(() => ({}));
|
|
394
|
+
if (resp.ok) {
|
|
395
|
+
showToast('Shoutout sent', 'success');
|
|
396
|
+
closeModal();
|
|
397
|
+
loadAccount();
|
|
398
|
+
} else {
|
|
399
|
+
showToast(data.detail || 'Shoutout failed', 'error');
|
|
400
|
+
btn.disabled = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
331
404
|
// ── Queue history (MIDDLE column, paginated) ───────────────────
|
|
332
405
|
const QUEUE_LIMIT = 20;
|
|
333
406
|
let queueOffset = 0;
|
|
@@ -440,6 +513,7 @@ function closeModal() {
|
|
|
440
513
|
// ── Init ───────────────────────────────────────────────────────
|
|
441
514
|
document.getElementById('edit-greeting').addEventListener('click', editGreeting);
|
|
442
515
|
document.getElementById('edit-color').addEventListener('click', editColor);
|
|
516
|
+
document.getElementById('buy-shoutout').addEventListener('click', buyShoutout);
|
|
443
517
|
document.querySelectorAll('.tx-toggle-btn').forEach(b => b.addEventListener('click', () => {
|
|
444
518
|
document.querySelectorAll('.tx-toggle-btn').forEach(x => x.classList.remove('active'));
|
|
445
519
|
b.classList.add('active');
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.20.1 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|