kryten-webqueue 0.7.0__tar.gz → 0.7.2__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.0 → kryten_webqueue-0.7.2}/CHANGELOG.md +7 -0
  2. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/catalog/db.py +29 -0
  4. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/queue/shadow.py +9 -0
  5. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/static/css/main.css +58 -22
  6. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/queue/index.html +21 -14
  7. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/pyproject.toml +1 -1
  8. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/.github/workflows/python-publish.yml +0 -0
  9. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/.github/workflows/release.yml +0 -0
  10. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/.gitignore +0 -0
  11. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/README.md +0 -0
  12. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/config.example.json +0 -0
  13. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/deploy/kryten-webqueue.service +0 -0
  14. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/deploy/nginx-queue.conf +0 -0
  15. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/IMPLEMENTATION_SPEC.md +0 -0
  16. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/IMPL_API_GATE.md +0 -0
  17. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/IMPL_ECONOMY.md +0 -0
  18. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/IMPL_KRYTEN_PY.md +0 -0
  19. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/IMPL_ROBOT.md +0 -0
  20. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/PRE_PLAN_GAPS.md +0 -0
  21. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/docs/PRODUCT_PLAN.md +0 -0
  22. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/__init__.py +0 -0
  23. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/__main__.py +0 -0
  24. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/api_gate/__init__.py +0 -0
  25. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/api_gate/client.py +0 -0
  26. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/app.py +0 -0
  27. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/auth/__init__.py +0 -0
  28. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/auth/otp.py +0 -0
  29. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/auth/rate_limit.py +0 -0
  30. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/auth/session.py +0 -0
  31. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/catalog/__init__.py +0 -0
  32. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/catalog/images.py +0 -0
  33. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/catalog/sync.py +0 -0
  34. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/config.py +0 -0
  35. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/jobs/__init__.py +0 -0
  36. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/jobs/manager.py +0 -0
  37. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/playlists/__init__.py +0 -0
  38. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/playlists/fire.py +0 -0
  39. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/playlists/importer.py +0 -0
  40. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/playlists/scheduler.py +0 -0
  41. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/queue/__init__.py +0 -0
  42. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/queue/ordering.py +0 -0
  43. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/queue/poller.py +0 -0
  44. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/__init__.py +0 -0
  45. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/admin_jobs.py +0 -0
  46. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/admin_playlists.py +0 -0
  47. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/admin_queue.py +0 -0
  48. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/admin_schedules.py +0 -0
  49. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/auth.py +0 -0
  50. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/catalog.py +0 -0
  51. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/pages.py +0 -0
  52. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/queue.py +0 -0
  53. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/routes/user.py +0 -0
  54. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/static/js/main.js +0 -0
  55. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/admin/index.html +0 -0
  56. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/admin/playlists.html +0 -0
  57. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  58. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/admin/schedules.html +0 -0
  59. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/auth/login.html +0 -0
  60. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/base.html +0 -0
  61. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/catalog/browse.html +0 -0
  62. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  63. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  64. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/templates/user/dashboard.html +0 -0
  65. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/ws/__init__.py +0 -0
  66. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/ws/handler.py +0 -0
  67. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.2}/kryten_webqueue/ws/manager.py +0 -0
@@ -4,6 +4,13 @@ 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.2] - 2026-06-08
8
+
9
+ ### Changed
10
+
11
+ - **Now Playing card redesigned for readability.** The card is now a vertical stack: the title spans the full width across the top (with a divider), then a row pairs a 2:3 poster with the time display, the progress bar, and the **Remaining** time directly under the bar. Below that, the item **description** and **category/tag chips** are shown when available. The now-playing state is enriched server-side with the catalog description and category/tag names for the playing item.
12
+ - **"Hide Previous" now defaults to on** so the Queue page opens focused on what's still to come.
13
+
7
14
  ## [0.7.0] - 2026-06-08
8
15
 
9
16
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryten-webqueue
3
- Version: 0.7.0
3
+ Version: 0.7.2
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
@@ -397,6 +397,35 @@ class Database:
397
397
  lookup[data["manifest_url"]] = data
398
398
  return lookup
399
399
 
400
+ async def get_item_facets(self, friendly_token: str) -> dict:
401
+ """Return description + category/tag names for a single catalog item.
402
+
403
+ Used to enrich the now-playing card. Returns empty values when the
404
+ token is unknown.
405
+ """
406
+ if not friendly_token:
407
+ return {"description": None, "categories": [], "tags": []}
408
+ row = await self._fetch_one(
409
+ "SELECT description FROM catalog WHERE friendly_token = ?", [friendly_token]
410
+ )
411
+ cats = await self._fetch_all(
412
+ "SELECT cat.name FROM categories cat "
413
+ "JOIN catalog_categories cc ON cc.category_id = cat.id "
414
+ "WHERE cc.friendly_token = ? ORDER BY cat.name",
415
+ [friendly_token],
416
+ )
417
+ tags = await self._fetch_all(
418
+ "SELECT t.name FROM tags t "
419
+ "JOIN catalog_tags ct ON ct.tag_id = t.id "
420
+ "WHERE ct.friendly_token = ? ORDER BY t.name",
421
+ [friendly_token],
422
+ )
423
+ return {
424
+ "description": (row or {}).get("description"),
425
+ "categories": [c["name"] for c in cats],
426
+ "tags": [t["name"] for t in tags],
427
+ }
428
+
400
429
  async def is_restricted(self, friendly_token: str) -> bool:
401
430
  sql = """
402
431
  SELECT 1 FROM saved_playlist_items spi
@@ -217,6 +217,15 @@ class QueueShadow:
217
217
  ):
218
218
  np["uid"] = it.get("uid")
219
219
  break
220
+ # Attach description + category/tag names for the now-playing card.
221
+ if np.get("friendly_token"):
222
+ try:
223
+ facets = await db.get_item_facets(np["friendly_token"])
224
+ np.setdefault("description", facets.get("description"))
225
+ np["categories"] = facets.get("categories") or []
226
+ np["tags"] = facets.get("tags") or []
227
+ except Exception:
228
+ logger.debug("Failed to enrich now-playing facets", exc_info=True)
220
229
  state["now_playing"] = np
221
230
 
222
231
  return state
@@ -317,30 +317,41 @@ a:hover {
317
317
  .now-playing-card {
318
318
  background: var(--bg-card);
319
319
  border-radius: var(--radius);
320
- padding: 2rem;
320
+ padding: 1.5rem;
321
321
  display: flex;
322
- gap: 1.5rem;
323
- align-items: center;
322
+ flex-direction: column;
323
+ gap: 1rem;
324
324
  /* Highlight the currently-playing item: subtle accent ring + glow. */
325
325
  border: 1px solid var(--accent);
326
326
  box-shadow: 0 0 0 1px rgba(108, 92, 231, 0.25), 0 6px 18px rgba(108, 92, 231, 0.18);
327
327
  }
328
- .np-info {
329
- flex: 1;
330
- min-width: 0;
331
- }
332
- .np-info h3 {
333
- margin-bottom: 0.5rem;
328
+ /* Title spans the full width of the card. */
329
+ .np-title {
330
+ margin: 0;
334
331
  font-size: 1.4rem;
335
332
  line-height: 1.25;
336
333
  overflow-wrap: anywhere;
334
+ padding-bottom: 0.75rem;
335
+ border-bottom: 1px solid var(--border);
336
+ }
337
+ /* Image + time display sit side by side. */
338
+ .np-body {
339
+ display: flex;
340
+ gap: 1.25rem;
341
+ align-items: flex-start;
342
+ }
343
+ .np-info {
344
+ flex: 1;
345
+ min-width: 0;
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 0.5rem;
337
349
  }
338
350
  .np-progress {
339
351
  height: 6px;
340
352
  background: var(--border);
341
353
  border-radius: 3px;
342
354
  overflow: hidden;
343
- margin: 0.75rem 0;
344
355
  }
345
356
  .progress-bar {
346
357
  height: 100%;
@@ -348,8 +359,9 @@ a:hover {
348
359
  transition: width 1s linear;
349
360
  }
350
361
  .np-time {
351
- font-size: 0.9rem;
352
- color: var(--text-secondary);
362
+ font-size: 1rem;
363
+ color: var(--text-primary);
364
+ font-variant-numeric: tabular-nums;
353
365
  }
354
366
 
355
367
  .queue-list {
@@ -412,8 +424,8 @@ a:hover {
412
424
  display: block;
413
425
  }
414
426
  .np-cover {
415
- width: 128px;
416
- height: 128px;
427
+ width: 120px;
428
+ aspect-ratio: 2 / 3;
417
429
  flex-shrink: 0;
418
430
  border-radius: 8px;
419
431
  overflow: hidden;
@@ -477,20 +489,44 @@ a:hover {
477
489
  color: var(--text-secondary);
478
490
  }
479
491
  .np-meta {
492
+ font-size: 0.85rem;
493
+ color: var(--text-secondary);
494
+ }
495
+ .np-remaining {
480
496
  font-size: 0.9rem;
481
497
  color: var(--text-secondary);
482
- margin: 0.25rem 0;
498
+ font-variant-numeric: tabular-nums;
483
499
  }
484
- .np-times {
485
- display: flex;
486
- justify-content: space-between;
487
- gap: 1rem;
500
+ /* Description + category/tag chips sit below the image / time row. */
501
+ .np-description {
488
502
  font-size: 0.9rem;
489
- margin-top: 0.5rem;
490
- white-space: nowrap;
503
+ line-height: 1.5;
504
+ color: var(--text-secondary);
505
+ padding-top: 0.75rem;
506
+ border-top: 1px solid var(--border);
507
+ white-space: pre-line;
508
+ overflow-wrap: anywhere;
491
509
  }
492
- .np-remaining {
510
+ .np-chips {
511
+ display: flex;
512
+ flex-wrap: wrap;
513
+ gap: 0.4rem;
514
+ }
515
+ .np-chip {
516
+ font-size: 0.72rem;
517
+ padding: 0.2rem 0.55rem;
518
+ border-radius: 999px;
519
+ line-height: 1.3;
520
+ }
521
+ .np-chip-cat {
522
+ background: rgba(108, 92, 231, 0.22);
523
+ color: var(--text-primary);
524
+ border: 1px solid var(--accent);
525
+ }
526
+ .np-chip-tag {
527
+ background: var(--bg-elevated, #2a2a38);
493
528
  color: var(--text-secondary);
529
+ border: 1px solid var(--border);
494
530
  }
495
531
 
496
532
  /* WebSocket status */
@@ -15,7 +15,7 @@
15
15
  <div class="queue-section-header">
16
16
  <h2>Up Next</h2>
17
17
  <label class="toggle-switch" title="Hide items before the currently playing one">
18
- <input type="checkbox" id="hide-previous-toggle" onchange="toggleHidePrevious(this.checked)">
18
+ <input type="checkbox" id="hide-previous-toggle" onchange="toggleHidePrevious(this.checked)" checked>
19
19
  <span class="toggle-slider"></span>
20
20
  <span>Hide Previous</span>
21
21
  </label>
@@ -43,7 +43,7 @@
43
43
  <script>
44
44
  let ws = null;
45
45
  let reconnectTimer = null;
46
- let hidePrevious = false;
46
+ let hidePrevious = true;
47
47
  let lastState = null;
48
48
 
49
49
  function toggleHidePrevious(checked) {
@@ -101,21 +101,28 @@ function renderQueue(state) {
101
101
  // runtime (`duration` is a preformatted "HH:MM:SS" string).
102
102
  const total = Number(np.seconds ?? np.duration_sec ?? 0) || 0;
103
103
  const remaining = Math.max(0, total - elapsed);
104
+ const pct = total > 0 ? (elapsed / total * 100) : 0;
105
+ const cats = Array.isArray(np.categories) ? np.categories : [];
106
+ const tags = Array.isArray(np.tags) ? np.tags : [];
107
+ const chips = [
108
+ ...cats.map(c => `<span class="np-chip np-chip-cat">${escapeHtml(c)}</span>`),
109
+ ...tags.map(t => `<span class="np-chip np-chip-tag">${escapeHtml(t)}</span>`),
110
+ ].join('');
104
111
  npEl.innerHTML = `
105
- <div class="np-cover">${coverHtml(np)}</div>
106
- <div class="np-info">
107
- <h3>${escapeHtml(np.title || 'Unknown')}</h3>
108
- <div class="np-meta">
109
- ${np.paid_by ? `<span class="np-user">Queued by ${escapeHtml(np.paid_by)}</span>` : ''}
110
- </div>
111
- <div class="np-progress">
112
- <div class="progress-bar" style="width: ${total > 0 ? (elapsed / total * 100) : 0}%"></div>
113
- </div>
114
- <div class="np-times">
115
- <span class="np-time">${formatTime(elapsed)} / ${formatTime(total)}</span>
116
- <span class="np-remaining">${formatTime(remaining)} remaining</span>
112
+ <h3 class="np-title">${escapeHtml(np.title || 'Unknown')}</h3>
113
+ <div class="np-body">
114
+ <div class="np-cover">${coverHtml(np)}</div>
115
+ <div class="np-info">
116
+ ${np.paid_by ? `<div class="np-meta"><span class="np-user">Queued by ${escapeHtml(np.paid_by)}</span></div>` : ''}
117
+ <div class="np-time">${formatTime(elapsed)} / ${formatTime(total)}</div>
118
+ <div class="np-progress">
119
+ <div class="progress-bar" style="width: ${pct}%"></div>
120
+ </div>
121
+ <div class="np-remaining">${formatTime(remaining)} remaining</div>
117
122
  </div>
118
123
  </div>
124
+ ${np.description ? `<div class="np-description">${escapeHtml(np.description)}</div>` : ''}
125
+ ${chips ? `<div class="np-chips">${chips}</div>` : ''}
119
126
  `;
120
127
  } else {
121
128
  npEl.innerHTML = '<p class="empty-state">Nothing playing</p>';
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.7.0"
3
+ version = "0.7.2"
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"