kryten-webqueue 0.7.4__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.
Files changed (67) hide show
  1. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/CHANGELOG.md +11 -0
  2. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/db.py +91 -17
  4. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/catalog.py +8 -6
  5. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/pages.py +20 -10
  6. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/static/css/main.css +24 -1
  7. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/base.html +3 -0
  8. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/browse.html +10 -1
  9. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/item_detail.html +0 -1
  10. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/pyproject.toml +1 -1
  11. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/.github/workflows/python-publish.yml +0 -0
  12. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/.github/workflows/release.yml +0 -0
  13. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/.gitignore +0 -0
  14. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/README.md +0 -0
  15. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/config.example.json +0 -0
  16. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/deploy/kryten-webqueue.service +0 -0
  17. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/deploy/nginx-queue.conf +0 -0
  18. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/IMPLEMENTATION_SPEC.md +0 -0
  19. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/IMPL_API_GATE.md +0 -0
  20. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/IMPL_ECONOMY.md +0 -0
  21. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/IMPL_KRYTEN_PY.md +0 -0
  22. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/IMPL_ROBOT.md +0 -0
  23. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/PRE_PLAN_GAPS.md +0 -0
  24. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/docs/PRODUCT_PLAN.md +0 -0
  25. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/__init__.py +0 -0
  26. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/__main__.py +0 -0
  27. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/api_gate/__init__.py +0 -0
  28. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/api_gate/client.py +0 -0
  29. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/app.py +0 -0
  30. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/__init__.py +0 -0
  31. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/otp.py +0 -0
  32. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/rate_limit.py +0 -0
  33. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/auth/session.py +0 -0
  34. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/__init__.py +0 -0
  35. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/images.py +0 -0
  36. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/catalog/sync.py +0 -0
  37. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/config.py +0 -0
  38. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/jobs/__init__.py +0 -0
  39. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/jobs/manager.py +0 -0
  40. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/__init__.py +0 -0
  41. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/fire.py +0 -0
  42. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/importer.py +0 -0
  43. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/playlists/scheduler.py +0 -0
  44. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/__init__.py +0 -0
  45. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/ordering.py +0 -0
  46. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/poller.py +0 -0
  47. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/queue/shadow.py +0 -0
  48. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/__init__.py +0 -0
  49. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_jobs.py +0 -0
  50. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_playlists.py +0 -0
  51. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_queue.py +0 -0
  52. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/admin_schedules.py +0 -0
  53. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/auth.py +0 -0
  54. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/queue.py +0 -0
  55. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/routes/user.py +0 -0
  56. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/static/js/main.js +0 -0
  57. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/index.html +0 -0
  58. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/playlists.html +0 -0
  59. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  60. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/admin/schedules.html +0 -0
  61. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/auth/login.html +0 -0
  62. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  63. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/queue/index.html +0 -0
  64. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/templates/user/dashboard.html +0 -0
  65. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/ws/__init__.py +0 -0
  66. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/ws/handler.py +0 -0
  67. {kryten_webqueue-0.7.4 → kryten_webqueue-0.7.5}/kryten_webqueue/ws/manager.py +0 -0
@@ -4,6 +4,17 @@ 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
+
7
18
  ## [0.7.4] - 2026-06-08
8
19
 
9
20
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryten-webqueue
3
- Version: 0.7.4
3
+ Version: 0.7.5
4
4
  Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
5
5
  Author: grobertson
6
6
  License-Expression: MIT
@@ -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
- return await self._fetch_all(sql, [query_text, per_page, (page - 1) * per_page])
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
- row = await self._fetch_one(sql, [query_text])
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:
@@ -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
- return await self._fetch_all(
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
- return await self._fetch_all(
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
- [limit],
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."""
@@ -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
- items = await db.browse(category=category, tag=tag, page=page)
14
- categories = await db.get_categories()
15
- tags = await db.get_tags()
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
- items = await db.search(q, page=page)
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
- items = await db.browse(category=category, tag=tag, page=page)
50
- total = await db.browse_count(category=category, tag=tag)
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
- items = await db.search(q, page=page)
76
- total = await db.search_count(q)
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
 
@@ -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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
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;
@@ -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>
@@ -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 %}
@@ -22,7 +22,6 @@
22
22
 
23
23
  <div class="card-actions item-detail-actions">
24
24
  <button class="btn btn-queue" onclick="queueItem('{{ item.friendly_token }}')">Queue</button>
25
- <button class="btn btn-playnext" onclick="playNext('{{ item.friendly_token }}')">Play Next</button>
26
25
  {% if user.rank >= 3 %}
27
26
  <button class="btn btn-admin" onclick="queueAsAdmin('{{ item.friendly_token }}')">Queue as Admin</button>
28
27
  {% endif %}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.7.4"
3
+ version = "0.7.5"
4
4
  description = "Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube"
5
5
  readme = "README.md"
6
6
  license = "MIT"