kryten-webqueue 0.21.0__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.21.0 → kryten_webqueue-0.22.0}/CHANGELOG.md +5 -1
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/api_gate/client.py +6 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/user.py +29 -1
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/static/css/main.css +4 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/user/dashboard.html +75 -1
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/pyproject.toml +1 -1
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/.gitignore +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/README.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/config.example.json +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/tests/test_search_facets.py +0 -0
|
@@ -4,7 +4,11 @@ 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.
|
|
8
12
|
|
|
9
13
|
## [0.21.0] — 2026-06-17
|
|
10
14
|
|
|
@@ -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,
|
|
@@ -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.21.0 → 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
|
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → 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.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.21.0 → kryten_webqueue-0.22.0}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.21.0 → 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
|