kryten-webqueue 0.7.3__tar.gz → 0.7.5__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.7.3 → kryten_webqueue-0.7.5}/CHANGELOG.md +17 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/PKG-INFO +1 -1
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/db.py +93 -19
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/shadow.py +1 -1
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/catalog.py +8 -6
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/pages.py +23 -10
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/static/css/main.css +94 -1
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/base.html +3 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/browse.html +10 -1
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/item_detail.html +27 -6
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/pyproject.toml +1 -1
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/.gitignore +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/README.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/config.example.json +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/ws/manager.py +0 -0
|
@@ -4,6 +4,23 @@ 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
|
+
## [0.7.5] - 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Hidden categories/tags with an admin reveal toggle.** Items in the categories `Z Channel Promos`, `Z Event Movies`, `Weekday Z Promos` and the tags `grindhousebumper`, `commercialsforbumpers`, `bumpers`, `channelz`, `grindhousetrailer`, `publicaccess`, `religioustv` are now excluded from the catalog browse/search results and from the category/tag facet dropdowns. Admins (rank ≥ 3) see a notice — "Certain items are hidden from results. Show hidden items?" — that toggles them back into view via `?show_hidden=1` (ignored for non-admins).
|
|
12
|
+
- **Modern, readability-focused typography.** The UI now loads Inter (body/controls) and Sora (headings) from Google Fonts, with antialiasing and `optimizeLegibility` enabled.
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
- **"Play Next" buttons** are no longer shown on the catalog browse cards or the item detail page (the underlying play-next queue mechanism is unchanged).
|
|
17
|
+
|
|
18
|
+
## [0.7.4] - 2026-06-08
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Catalog item detail page redesigned** to a two-column layout: the poster and action buttons (Queue / Play Next / Queue as Admin) stack in a sticky left column, while the right column shows the title, a divider, the formatted description, and **Category** / **Tags** facet rows. Category and tag chips link back into a filtered catalog browse. Categories/tags are sourced from the catalog join tables (populated by sync since 0.7.0).
|
|
23
|
+
|
|
7
24
|
## [0.7.3] - 2026-06-08
|
|
8
25
|
|
|
9
26
|
### Fixed
|
|
@@ -12,6 +12,47 @@ def _slugify(text: str) -> str:
|
|
|
12
12
|
return s.strip("-") or "untitled"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
# Categories and tags whose items are hidden from the public catalog (dropdowns
|
|
16
|
+
# and search results). Admins can opt to reveal them. Matched by exact name.
|
|
17
|
+
HIDDEN_CATEGORY_NAMES = [
|
|
18
|
+
"Z Channel Promos",
|
|
19
|
+
"Z Event Movies",
|
|
20
|
+
"Weekday Z Promos",
|
|
21
|
+
]
|
|
22
|
+
HIDDEN_TAG_NAMES = [
|
|
23
|
+
"grindhousebumper",
|
|
24
|
+
"commercialsforbumpers",
|
|
25
|
+
"bumpers",
|
|
26
|
+
"channelz",
|
|
27
|
+
"grindhousetrailer",
|
|
28
|
+
"publicaccess",
|
|
29
|
+
"religioustv",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _hidden_exclusion(alias: str = "c") -> tuple[str, list]:
|
|
34
|
+
"""SQL fragment (+ params) excluding items in hidden categories/tags.
|
|
35
|
+
|
|
36
|
+
The fragment is prefixed with ``AND`` so it can be appended to an existing
|
|
37
|
+
WHERE clause that references the catalog row under ``alias``.
|
|
38
|
+
"""
|
|
39
|
+
cat_ph = ",".join("?" * len(HIDDEN_CATEGORY_NAMES))
|
|
40
|
+
tag_ph = ",".join("?" * len(HIDDEN_TAG_NAMES))
|
|
41
|
+
sql = f"""
|
|
42
|
+
AND {alias}.friendly_token NOT IN (
|
|
43
|
+
SELECT cc.friendly_token FROM catalog_categories cc
|
|
44
|
+
JOIN categories cat ON cc.category_id = cat.id
|
|
45
|
+
WHERE cat.name IN ({cat_ph})
|
|
46
|
+
)
|
|
47
|
+
AND {alias}.friendly_token NOT IN (
|
|
48
|
+
SELECT ct.friendly_token FROM catalog_tags ct
|
|
49
|
+
JOIN tags t ON ct.tag_id = t.id
|
|
50
|
+
WHERE t.name IN ({tag_ph})
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
return sql, [*HIDDEN_CATEGORY_NAMES, *HIDDEN_TAG_NAMES]
|
|
54
|
+
|
|
55
|
+
|
|
15
56
|
MIGRATIONS = [
|
|
16
57
|
# v1: Migration tracking table
|
|
17
58
|
"""
|
|
@@ -245,7 +286,7 @@ class Database:
|
|
|
245
286
|
|
|
246
287
|
# --- Catalog ---
|
|
247
288
|
|
|
248
|
-
async def browse(self, *, category: str | None = None, tag: str | None = None, page: int = 1, per_page: int = 24) -> list[dict]:
|
|
289
|
+
async def browse(self, *, category: str | None = None, tag: str | None = None, page: int = 1, per_page: int = 24, show_hidden: bool = False) -> list[dict]:
|
|
249
290
|
query = """
|
|
250
291
|
SELECT c.friendly_token, c.title, c.duration_sec, c.cover_art_path, c.thumbnail_url, c.manifest_url
|
|
251
292
|
FROM catalog c
|
|
@@ -256,6 +297,10 @@ class Database:
|
|
|
256
297
|
)
|
|
257
298
|
"""
|
|
258
299
|
params: list = []
|
|
300
|
+
if not show_hidden:
|
|
301
|
+
excl_sql, excl_params = _hidden_exclusion("c")
|
|
302
|
+
query += excl_sql
|
|
303
|
+
params.extend(excl_params)
|
|
259
304
|
if category:
|
|
260
305
|
query += """
|
|
261
306
|
AND c.friendly_token IN (
|
|
@@ -296,7 +341,7 @@ class Database:
|
|
|
296
341
|
params.extend([per_page, (page - 1) * per_page])
|
|
297
342
|
return await self._fetch_all(query, params)
|
|
298
343
|
|
|
299
|
-
async def browse_count(self, *, category: str | None = None, tag: str | None = None) -> int:
|
|
344
|
+
async def browse_count(self, *, category: str | None = None, tag: str | None = None, show_hidden: bool = False) -> int:
|
|
300
345
|
query = """
|
|
301
346
|
SELECT COUNT(*) as cnt FROM catalog c
|
|
302
347
|
WHERE c.friendly_token NOT IN (
|
|
@@ -306,6 +351,10 @@ class Database:
|
|
|
306
351
|
)
|
|
307
352
|
"""
|
|
308
353
|
params: list = []
|
|
354
|
+
if not show_hidden:
|
|
355
|
+
excl_sql, excl_params = _hidden_exclusion("c")
|
|
356
|
+
query += excl_sql
|
|
357
|
+
params.extend(excl_params)
|
|
309
358
|
if category:
|
|
310
359
|
query += """
|
|
311
360
|
AND c.friendly_token IN (
|
|
@@ -327,7 +376,7 @@ class Database:
|
|
|
327
376
|
row = await self._fetch_one(query, params)
|
|
328
377
|
return row["cnt"] if row else 0
|
|
329
378
|
|
|
330
|
-
async def search(self, query_text: str, *, page: int = 1, per_page: int = 24) -> list[dict]:
|
|
379
|
+
async def search(self, query_text: str, *, page: int = 1, per_page: int = 24, show_hidden: bool = False) -> list[dict]:
|
|
331
380
|
sql = """
|
|
332
381
|
SELECT c.friendly_token, c.title, c.duration_sec, c.cover_art_path, c.thumbnail_url, c.manifest_url,
|
|
333
382
|
rank AS relevance
|
|
@@ -339,12 +388,20 @@ class Database:
|
|
|
339
388
|
JOIN saved_playlists sp ON spi.playlist_id = sp.id
|
|
340
389
|
WHERE sp.is_immutable = 1 AND spi.media_type = 'cm'
|
|
341
390
|
)
|
|
391
|
+
"""
|
|
392
|
+
params: list = [query_text]
|
|
393
|
+
if not show_hidden:
|
|
394
|
+
excl_sql, excl_params = _hidden_exclusion("c")
|
|
395
|
+
sql += excl_sql
|
|
396
|
+
params.extend(excl_params)
|
|
397
|
+
sql += """
|
|
342
398
|
ORDER BY rank
|
|
343
399
|
LIMIT ? OFFSET ?
|
|
344
400
|
"""
|
|
345
|
-
|
|
401
|
+
params.extend([per_page, (page - 1) * per_page])
|
|
402
|
+
return await self._fetch_all(sql, params)
|
|
346
403
|
|
|
347
|
-
async def search_count(self, query_text: str) -> int:
|
|
404
|
+
async def search_count(self, query_text: str, *, show_hidden: bool = False) -> int:
|
|
348
405
|
sql = """
|
|
349
406
|
SELECT COUNT(*) as cnt
|
|
350
407
|
FROM catalog_fts fts
|
|
@@ -356,7 +413,12 @@ class Database:
|
|
|
356
413
|
WHERE sp.is_immutable = 1 AND spi.media_type = 'cm'
|
|
357
414
|
)
|
|
358
415
|
"""
|
|
359
|
-
|
|
416
|
+
params: list = [query_text]
|
|
417
|
+
if not show_hidden:
|
|
418
|
+
excl_sql, excl_params = _hidden_exclusion("c")
|
|
419
|
+
sql += excl_sql
|
|
420
|
+
params.extend(excl_params)
|
|
421
|
+
row = await self._fetch_one(sql, params)
|
|
360
422
|
return row["cnt"] if row else 0
|
|
361
423
|
|
|
362
424
|
async def get_item(self, friendly_token: str) -> dict | None:
|
|
@@ -409,7 +471,7 @@ class Database:
|
|
|
409
471
|
"SELECT description FROM catalog WHERE friendly_token = ?", [friendly_token]
|
|
410
472
|
)
|
|
411
473
|
cats = await self._fetch_all(
|
|
412
|
-
"SELECT cat.name FROM categories cat "
|
|
474
|
+
"SELECT cat.name, cat.slug FROM categories cat "
|
|
413
475
|
"JOIN catalog_categories cc ON cc.category_id = cat.id "
|
|
414
476
|
"WHERE cc.friendly_token = ? ORDER BY cat.name",
|
|
415
477
|
[friendly_token],
|
|
@@ -422,7 +484,7 @@ class Database:
|
|
|
422
484
|
)
|
|
423
485
|
return {
|
|
424
486
|
"description": (row or {}).get("description"),
|
|
425
|
-
"categories": [c["name"] for c in cats],
|
|
487
|
+
"categories": [{"name": c["name"], "slug": c["slug"]} for c in cats],
|
|
426
488
|
"tags": [t["name"] for t in tags],
|
|
427
489
|
}
|
|
428
490
|
|
|
@@ -438,31 +500,43 @@ class Database:
|
|
|
438
500
|
row = await self._fetch_one(sql, [friendly_token])
|
|
439
501
|
return row is not None
|
|
440
502
|
|
|
441
|
-
async def get_categories(self) -> list[dict]:
|
|
503
|
+
async def get_categories(self, *, show_hidden: bool = False) -> list[dict]:
|
|
442
504
|
"""Distinct categories that have at least one catalog item, for facets."""
|
|
443
|
-
|
|
444
|
-
"""
|
|
505
|
+
sql = """
|
|
445
506
|
SELECT c.id, c.name, c.slug, COUNT(cc.friendly_token) AS cnt
|
|
446
507
|
FROM categories c
|
|
447
508
|
JOIN catalog_categories cc ON cc.category_id = c.id
|
|
509
|
+
"""
|
|
510
|
+
params: list = []
|
|
511
|
+
if not show_hidden:
|
|
512
|
+
ph = ",".join("?" * len(HIDDEN_CATEGORY_NAMES))
|
|
513
|
+
sql += f" WHERE c.name NOT IN ({ph})"
|
|
514
|
+
params.extend(HIDDEN_CATEGORY_NAMES)
|
|
515
|
+
sql += """
|
|
448
516
|
GROUP BY c.id, c.name, c.slug
|
|
449
517
|
ORDER BY c.name
|
|
450
|
-
|
|
451
|
-
)
|
|
518
|
+
"""
|
|
519
|
+
return await self._fetch_all(sql, params)
|
|
452
520
|
|
|
453
|
-
async def get_tags(self, *, limit: int = 100) -> list[dict]:
|
|
521
|
+
async def get_tags(self, *, limit: int = 100, show_hidden: bool = False) -> list[dict]:
|
|
454
522
|
"""Most-used tags that have at least one catalog item, for facets."""
|
|
455
|
-
|
|
456
|
-
"""
|
|
523
|
+
sql = """
|
|
457
524
|
SELECT t.id, t.name, COUNT(ct.friendly_token) AS cnt
|
|
458
525
|
FROM tags t
|
|
459
526
|
JOIN catalog_tags ct ON ct.tag_id = t.id
|
|
527
|
+
"""
|
|
528
|
+
params: list = []
|
|
529
|
+
if not show_hidden:
|
|
530
|
+
ph = ",".join("?" * len(HIDDEN_TAG_NAMES))
|
|
531
|
+
sql += f" WHERE t.name NOT IN ({ph})"
|
|
532
|
+
params.extend(HIDDEN_TAG_NAMES)
|
|
533
|
+
sql += """
|
|
460
534
|
GROUP BY t.id, t.name
|
|
461
535
|
ORDER BY cnt DESC, t.name ASC
|
|
462
536
|
LIMIT ?
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
537
|
+
"""
|
|
538
|
+
params.append(limit)
|
|
539
|
+
return await self._fetch_all(sql, params)
|
|
466
540
|
|
|
467
541
|
async def upsert_category(self, name: str) -> int:
|
|
468
542
|
"""Insert a category by name (deriving a unique slug) and return its id."""
|
|
@@ -222,7 +222,7 @@ class QueueShadow:
|
|
|
222
222
|
try:
|
|
223
223
|
facets = await db.get_item_facets(np["friendly_token"])
|
|
224
224
|
np.setdefault("description", facets.get("description"))
|
|
225
|
-
np["categories"] = facets.get("categories") or []
|
|
225
|
+
np["categories"] = [c["name"] for c in (facets.get("categories") or [])]
|
|
226
226
|
np["tags"] = facets.get("tags") or []
|
|
227
227
|
except Exception:
|
|
228
228
|
logger.debug("Failed to enrich now-playing facets", exc_info=True)
|
|
@@ -7,23 +7,25 @@ router = APIRouter(prefix="/catalog", tags=["catalog"])
|
|
|
7
7
|
|
|
8
8
|
@router.get("/browse")
|
|
9
9
|
async def browse(request: Request, category: str | None = None, tag: str | None = None,
|
|
10
|
-
page: int = 1, user: dict = Depends(get_current_user)):
|
|
10
|
+
page: int = 1, show_hidden: int = 0, user: dict = Depends(get_current_user)):
|
|
11
11
|
"""Browse catalog with optional category/tag filter."""
|
|
12
12
|
db = request.app.state.db
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
show_hidden = bool(show_hidden) and (user.get("rank") or 0) >= 3
|
|
14
|
+
items = await db.browse(category=category, tag=tag, page=page, show_hidden=show_hidden)
|
|
15
|
+
categories = await db.get_categories(show_hidden=show_hidden)
|
|
16
|
+
tags = await db.get_tags(show_hidden=show_hidden)
|
|
16
17
|
return {"items": items, "categories": categories, "tags": tags, "page": page}
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@router.get("/search")
|
|
20
|
-
async def search(request: Request, q: str = "", page: int = 1,
|
|
21
|
+
async def search(request: Request, q: str = "", page: int = 1, show_hidden: int = 0,
|
|
21
22
|
user: dict = Depends(get_current_user)):
|
|
22
23
|
"""Full-text search of catalog."""
|
|
23
24
|
if not q.strip():
|
|
24
25
|
raise HTTPException(400, "Query required")
|
|
25
26
|
db = request.app.state.db
|
|
26
|
-
|
|
27
|
+
show_hidden = bool(show_hidden) and (user.get("rank") or 0) >= 3
|
|
28
|
+
items = await db.search(q, page=page, show_hidden=show_hidden)
|
|
27
29
|
return {"items": items, "query": q, "page": page}
|
|
28
30
|
|
|
29
31
|
|
|
@@ -41,16 +41,19 @@ async def login_page(request: Request):
|
|
|
41
41
|
|
|
42
42
|
@router.get("/catalog/browse", response_class=HTMLResponse)
|
|
43
43
|
async def catalog_browse_page(request: Request, category: str | None = None,
|
|
44
|
-
tag: str | None = None, page: int = 1
|
|
44
|
+
tag: str | None = None, page: int = 1,
|
|
45
|
+
show_hidden: int = 0):
|
|
45
46
|
user = _get_user_or_none(request)
|
|
46
47
|
if not user:
|
|
47
48
|
return RedirectResponse("/auth/login")
|
|
48
49
|
db = request.app.state.db
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
is_admin = (user.get("rank") or 0) >= 3
|
|
51
|
+
show_hidden = bool(show_hidden) and is_admin
|
|
52
|
+
items = await db.browse(category=category, tag=tag, page=page, show_hidden=show_hidden)
|
|
53
|
+
total = await db.browse_count(category=category, tag=tag, show_hidden=show_hidden)
|
|
51
54
|
total_pages = max(1, (total + 23) // 24)
|
|
52
|
-
categories = await db.get_categories()
|
|
53
|
-
tags = await db.get_tags()
|
|
55
|
+
categories = await db.get_categories(show_hidden=show_hidden)
|
|
56
|
+
tags = await db.get_tags(show_hidden=show_hidden)
|
|
54
57
|
return templates.TemplateResponse(request, "catalog/browse.html", {
|
|
55
58
|
"user": user,
|
|
56
59
|
"items": items,
|
|
@@ -61,22 +64,27 @@ async def catalog_browse_page(request: Request, category: str | None = None,
|
|
|
61
64
|
"active_category": category,
|
|
62
65
|
"active_tag": tag,
|
|
63
66
|
"query": None,
|
|
67
|
+
"is_admin": is_admin,
|
|
68
|
+
"show_hidden": show_hidden,
|
|
64
69
|
})
|
|
65
70
|
|
|
66
71
|
|
|
67
72
|
@router.get("/catalog/search", response_class=HTMLResponse)
|
|
68
|
-
async def catalog_search_page(request: Request, q: str = "", page: int = 1
|
|
73
|
+
async def catalog_search_page(request: Request, q: str = "", page: int = 1,
|
|
74
|
+
show_hidden: int = 0):
|
|
69
75
|
user = _get_user_or_none(request)
|
|
70
76
|
if not user:
|
|
71
77
|
return RedirectResponse("/auth/login")
|
|
72
78
|
if not q.strip():
|
|
73
79
|
return RedirectResponse("/catalog/browse")
|
|
74
80
|
db = request.app.state.db
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
is_admin = (user.get("rank") or 0) >= 3
|
|
82
|
+
show_hidden = bool(show_hidden) and is_admin
|
|
83
|
+
items = await db.search(q, page=page, show_hidden=show_hidden)
|
|
84
|
+
total = await db.search_count(q, show_hidden=show_hidden)
|
|
77
85
|
total_pages = max(1, (total + 23) // 24)
|
|
78
|
-
categories = await db.get_categories()
|
|
79
|
-
tags = await db.get_tags()
|
|
86
|
+
categories = await db.get_categories(show_hidden=show_hidden)
|
|
87
|
+
tags = await db.get_tags(show_hidden=show_hidden)
|
|
80
88
|
return templates.TemplateResponse(request, "catalog/browse.html", {
|
|
81
89
|
"user": user,
|
|
82
90
|
"items": items,
|
|
@@ -87,6 +95,8 @@ async def catalog_search_page(request: Request, q: str = "", page: int = 1):
|
|
|
87
95
|
"active_category": None,
|
|
88
96
|
"active_tag": None,
|
|
89
97
|
"query": q,
|
|
98
|
+
"is_admin": is_admin,
|
|
99
|
+
"show_hidden": show_hidden,
|
|
90
100
|
})
|
|
91
101
|
|
|
92
102
|
|
|
@@ -109,9 +119,12 @@ async def catalog_item_page(request: Request, friendly_token: str):
|
|
|
109
119
|
return templates.TemplateResponse(
|
|
110
120
|
request, "catalog/item_not_found.html", {"user": user}, status_code=404
|
|
111
121
|
)
|
|
122
|
+
facets = await db.get_item_facets(friendly_token)
|
|
112
123
|
return templates.TemplateResponse(request, "catalog/item_detail.html", {
|
|
113
124
|
"user": user,
|
|
114
125
|
"item": item,
|
|
126
|
+
"categories": facets.get("categories") or [],
|
|
127
|
+
"tags": facets.get("tags") or [],
|
|
115
128
|
})
|
|
116
129
|
|
|
117
130
|
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
--radius: 8px;
|
|
17
17
|
--shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
18
18
|
--nav-height: 4rem;
|
|
19
|
+
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
20
|
+
--font-heading: 'Sora', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
* {
|
|
@@ -30,11 +32,20 @@ html {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
body {
|
|
33
|
-
font-family: -
|
|
35
|
+
font-family: var(--font-body);
|
|
34
36
|
background: var(--bg-primary);
|
|
35
37
|
color: var(--text-primary);
|
|
36
38
|
line-height: 1.6;
|
|
37
39
|
min-height: 100vh;
|
|
40
|
+
-webkit-font-smoothing: antialiased;
|
|
41
|
+
-moz-osx-font-smoothing: grayscale;
|
|
42
|
+
text-rendering: optimizeLegibility;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h1, h2, h3, h4, h5, h6,
|
|
46
|
+
.nav-brand a {
|
|
47
|
+
font-family: var(--font-heading);
|
|
48
|
+
letter-spacing: -0.01em;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
a {
|
|
@@ -127,6 +138,18 @@ a:hover {
|
|
|
127
138
|
align-items: center;
|
|
128
139
|
flex-wrap: wrap;
|
|
129
140
|
}
|
|
141
|
+
.admin-hidden-notice {
|
|
142
|
+
margin-top: 0.85rem;
|
|
143
|
+
font-size: 0.85rem;
|
|
144
|
+
color: var(--text-secondary);
|
|
145
|
+
display: flex;
|
|
146
|
+
gap: 0.5rem;
|
|
147
|
+
align-items: center;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
}
|
|
150
|
+
.admin-hidden-notice a {
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
130
153
|
.search-form {
|
|
131
154
|
display: flex;
|
|
132
155
|
gap: 0.5rem;
|
|
@@ -507,12 +530,82 @@ a:hover {
|
|
|
507
530
|
white-space: pre-line;
|
|
508
531
|
overflow-wrap: anywhere;
|
|
509
532
|
}
|
|
533
|
+
/* Item detail: poster + buttons on the left, title/description/facets on right. */
|
|
534
|
+
.item-detail-layout {
|
|
535
|
+
display: grid;
|
|
536
|
+
grid-template-columns: 300px minmax(0, 1fr);
|
|
537
|
+
gap: 2rem;
|
|
538
|
+
align-items: start;
|
|
539
|
+
}
|
|
540
|
+
@media (max-width: 700px) {
|
|
541
|
+
.item-detail-layout {
|
|
542
|
+
grid-template-columns: 1fr;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
.item-detail-poster {
|
|
546
|
+
display: flex;
|
|
547
|
+
flex-direction: column;
|
|
548
|
+
gap: 1rem;
|
|
549
|
+
position: sticky;
|
|
550
|
+
top: calc(var(--nav-height) + 1rem);
|
|
551
|
+
}
|
|
552
|
+
.item-detail-poster img,
|
|
553
|
+
.item-detail-poster .card-poster-placeholder {
|
|
554
|
+
width: 100%;
|
|
555
|
+
aspect-ratio: 2 / 3;
|
|
556
|
+
object-fit: cover;
|
|
557
|
+
border-radius: var(--radius);
|
|
558
|
+
display: block;
|
|
559
|
+
}
|
|
560
|
+
.item-detail-actions {
|
|
561
|
+
display: flex;
|
|
562
|
+
flex-direction: column;
|
|
563
|
+
gap: 0.5rem;
|
|
564
|
+
}
|
|
565
|
+
.item-detail-actions .btn {
|
|
566
|
+
width: 100%;
|
|
567
|
+
}
|
|
568
|
+
.item-detail-info {
|
|
569
|
+
min-width: 0;
|
|
570
|
+
}
|
|
571
|
+
.item-detail-title {
|
|
572
|
+
margin: 0;
|
|
573
|
+
line-height: 1.2;
|
|
574
|
+
overflow-wrap: anywhere;
|
|
575
|
+
}
|
|
576
|
+
.item-detail-meta {
|
|
577
|
+
margin-top: 0.35rem;
|
|
578
|
+
font-size: 0.85rem;
|
|
579
|
+
color: var(--text-secondary);
|
|
580
|
+
}
|
|
510
581
|
/* MediaCMS descriptions are newline-delimited plain text; preserve the line
|
|
511
582
|
breaks (Synopsis / Tagline / Cast & Crew sections) instead of collapsing them. */
|
|
512
583
|
.item-detail-description {
|
|
513
584
|
white-space: pre-line;
|
|
514
585
|
overflow-wrap: anywhere;
|
|
515
586
|
line-height: 1.6;
|
|
587
|
+
margin-top: 0.75rem;
|
|
588
|
+
padding-top: 0.75rem;
|
|
589
|
+
border-top: 1px solid var(--border);
|
|
590
|
+
}
|
|
591
|
+
.item-detail-facet {
|
|
592
|
+
margin-top: 1rem;
|
|
593
|
+
}
|
|
594
|
+
.item-detail-facet-label {
|
|
595
|
+
display: block;
|
|
596
|
+
font-size: 0.75rem;
|
|
597
|
+
text-transform: uppercase;
|
|
598
|
+
letter-spacing: 0.05em;
|
|
599
|
+
color: var(--text-secondary);
|
|
600
|
+
margin-bottom: 0.4rem;
|
|
601
|
+
}
|
|
602
|
+
.item-detail-chips {
|
|
603
|
+
display: flex;
|
|
604
|
+
flex-wrap: wrap;
|
|
605
|
+
gap: 0.4rem;
|
|
606
|
+
}
|
|
607
|
+
a.np-chip {
|
|
608
|
+
text-decoration: none;
|
|
516
609
|
}
|
|
517
610
|
.np-chips {
|
|
518
611
|
display: flex;
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>{% block title %}Queue{% endblock %} — Channel-Z</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Sora:wght@600;700&display=swap">
|
|
7
10
|
<link rel="stylesheet" href="/static/css/main.css">
|
|
8
11
|
{% block head %}{% endblock %}
|
|
9
12
|
</head>
|
{kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
</select>
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
28
|
+
{% if is_admin %}
|
|
29
|
+
<div class="admin-hidden-notice">
|
|
30
|
+
<span>Certain items are hidden from results.</span>
|
|
31
|
+
{% if show_hidden %}
|
|
32
|
+
<a href="{{ request.url.remove_query_params('show_hidden') }}">Hide them again</a>
|
|
33
|
+
{% else %}
|
|
34
|
+
<a href="{{ request.url.include_query_params(show_hidden=1) }}">Show hidden items?</a>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
{% endif %}
|
|
28
38
|
</div>
|
|
29
39
|
|
|
30
40
|
<div class="catalog-grid">
|
|
@@ -55,7 +65,6 @@
|
|
|
55
65
|
</div>
|
|
56
66
|
<div class="card-actions">
|
|
57
67
|
<button class="btn btn-sm btn-queue" onclick="queueItem('{{ item.friendly_token }}')">Queue</button>
|
|
58
|
-
<button class="btn btn-sm btn-playnext" onclick="playNext('{{ item.friendly_token }}')">Play Next</button>
|
|
59
68
|
{% if user.rank >= 3 %}
|
|
60
69
|
<button class="btn btn-sm btn-admin" onclick="queueAsAdmin('{{ item.friendly_token }}')">Queue as Admin</button>
|
|
61
70
|
{% endif %}
|
{kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
{% else %}
|
|
20
20
|
<div class="card-poster-placeholder"><span>{{ item.title[:1] }}</span></div>
|
|
21
21
|
{% endif %}
|
|
22
|
+
|
|
23
|
+
<div class="card-actions item-detail-actions">
|
|
24
|
+
<button class="btn btn-queue" onclick="queueItem('{{ item.friendly_token }}')">Queue</button>
|
|
25
|
+
{% if user.rank >= 3 %}
|
|
26
|
+
<button class="btn btn-admin" onclick="queueAsAdmin('{{ item.friendly_token }}')">Queue as Admin</button>
|
|
27
|
+
{% endif %}
|
|
28
|
+
</div>
|
|
22
29
|
</div>
|
|
23
30
|
|
|
24
31
|
<div class="item-detail-info">
|
|
@@ -35,13 +42,27 @@
|
|
|
35
42
|
<p class="item-detail-description empty-state">No description available.</p>
|
|
36
43
|
{% endif %}
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
{% if categories %}
|
|
46
|
+
<div class="item-detail-facet">
|
|
47
|
+
<span class="item-detail-facet-label">Category</span>
|
|
48
|
+
<div class="item-detail-chips">
|
|
49
|
+
{% for cat in categories %}
|
|
50
|
+
<a class="np-chip np-chip-cat" href="/catalog/browse?category={{ cat.slug }}">{{ cat.name }}</a>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</div>
|
|
44
53
|
</div>
|
|
54
|
+
{% endif %}
|
|
55
|
+
|
|
56
|
+
{% if tags %}
|
|
57
|
+
<div class="item-detail-facet">
|
|
58
|
+
<span class="item-detail-facet-label">Tags</span>
|
|
59
|
+
<div class="item-detail-chips">
|
|
60
|
+
{% for tag in tags %}
|
|
61
|
+
<a class="np-chip np-chip-tag" href="/catalog/browse?tag={{ tag | urlencode }}">{{ tag }}</a>
|
|
62
|
+
{% endfor %}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{% endif %}
|
|
45
66
|
</div>
|
|
46
67
|
</div>
|
|
47
68
|
</div>
|
|
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
|
|
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.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.7.3 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|