kryten-webqueue 0.7.0__tar.gz → 0.7.1__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.1}/CHANGELOG.md +7 -0
  2. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/catalog/db.py +26 -1
  4. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/queue/shadow.py +11 -0
  5. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/static/css/main.css +62 -17
  6. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/queue/index.html +27 -14
  7. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/pyproject.toml +1 -1
  8. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/.github/workflows/python-publish.yml +0 -0
  9. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/.github/workflows/release.yml +0 -0
  10. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/.gitignore +0 -0
  11. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/README.md +0 -0
  12. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/config.example.json +0 -0
  13. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/deploy/kryten-webqueue.service +0 -0
  14. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/deploy/nginx-queue.conf +0 -0
  15. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
  16. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/IMPL_API_GATE.md +0 -0
  17. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/IMPL_ECONOMY.md +0 -0
  18. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/IMPL_KRYTEN_PY.md +0 -0
  19. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/IMPL_ROBOT.md +0 -0
  20. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/PRE_PLAN_GAPS.md +0 -0
  21. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/docs/PRODUCT_PLAN.md +0 -0
  22. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/__init__.py +0 -0
  23. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/__main__.py +0 -0
  24. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/api_gate/__init__.py +0 -0
  25. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/api_gate/client.py +0 -0
  26. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/app.py +0 -0
  27. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/auth/__init__.py +0 -0
  28. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/auth/otp.py +0 -0
  29. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/auth/rate_limit.py +0 -0
  30. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/auth/session.py +0 -0
  31. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/catalog/__init__.py +0 -0
  32. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/catalog/images.py +0 -0
  33. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/catalog/sync.py +0 -0
  34. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/config.py +0 -0
  35. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/jobs/__init__.py +0 -0
  36. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/jobs/manager.py +0 -0
  37. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/playlists/__init__.py +0 -0
  38. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/playlists/fire.py +0 -0
  39. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/playlists/importer.py +0 -0
  40. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/playlists/scheduler.py +0 -0
  41. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/queue/__init__.py +0 -0
  42. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/queue/ordering.py +0 -0
  43. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/queue/poller.py +0 -0
  44. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/__init__.py +0 -0
  45. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
  46. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
  47. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/admin_queue.py +0 -0
  48. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
  49. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/auth.py +0 -0
  50. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/catalog.py +0 -0
  51. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/pages.py +0 -0
  52. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/queue.py +0 -0
  53. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/routes/user.py +0 -0
  54. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/static/js/main.js +0 -0
  55. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/admin/index.html +0 -0
  56. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
  57. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  58. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
  59. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/auth/login.html +0 -0
  60. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/base.html +0 -0
  61. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/catalog/browse.html +0 -0
  62. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  63. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  64. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
  65. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/ws/__init__.py +0 -0
  66. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/kryten_webqueue/ws/handler.py +0 -0
  67. {kryten_webqueue-0.7.0 → kryten_webqueue-0.7.1}/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.1] - 2026-06-08
8
+
9
+ ### Changed
10
+
11
+ - **Now Playing box redesign.** The title now spans the full card width, the cover renders as a 2:3 poster, the elapsed/total time and progress bar are stacked with **Remaining** shown as its own line under the bar, and a details section beneath the poster shows the item description plus category and tag chips. The now-playing payload is enriched with `description`, `categories`, and `tags` from the catalog.
12
+ - **"Hide Previous" now defaults to ON** on the Queue page, so the view 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.1
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
@@ -374,6 +374,31 @@ class Database:
374
374
  async def get_item_admin(self, friendly_token: str) -> dict | None:
375
375
  return await self._fetch_one("SELECT * FROM catalog WHERE friendly_token = ?", [friendly_token])
376
376
 
377
+ async def get_item_facets(self, friendly_token: str) -> dict:
378
+ """Return category and tag names for a single catalog item."""
379
+ cats = await self._fetch_all(
380
+ """
381
+ SELECT cat.name FROM catalog_categories cc
382
+ JOIN categories cat ON cc.category_id = cat.id
383
+ WHERE cc.friendly_token = ?
384
+ ORDER BY cat.name
385
+ """,
386
+ [friendly_token],
387
+ )
388
+ tags = await self._fetch_all(
389
+ """
390
+ SELECT t.name FROM catalog_tags ct
391
+ JOIN tags t ON ct.tag_id = t.id
392
+ WHERE ct.friendly_token = ?
393
+ ORDER BY t.name
394
+ """,
395
+ [friendly_token],
396
+ )
397
+ return {
398
+ "categories": [c["name"] for c in cats],
399
+ "tags": [t["name"] for t in tags],
400
+ }
401
+
377
402
  async def get_catalog_brief(self, tokens: list[str], manifest_urls: list[str]) -> dict[str, dict]:
378
403
  """Return a lookup of catalog metadata keyed by BOTH friendly_token and
379
404
  manifest_url, for enriching queue-shadow items that may only carry one.
@@ -383,7 +408,7 @@ class Database:
383
408
  return {}
384
409
  placeholders = ",".join("?" * len(keys))
385
410
  rows = await self._fetch_all(
386
- "SELECT friendly_token, manifest_url, title, duration_sec, "
411
+ "SELECT friendly_token, manifest_url, title, description, duration_sec, "
387
412
  "cover_art_path, thumbnail_url FROM catalog "
388
413
  f"WHERE friendly_token IN ({placeholders}) OR manifest_url IN ({placeholders})",
389
414
  keys + keys,
@@ -201,6 +201,8 @@ class QueueShadow:
201
201
  if meta:
202
202
  np.setdefault("cover_art_path", meta.get("cover_art_path"))
203
203
  np.setdefault("thumbnail_url", meta.get("thumbnail_url"))
204
+ if not np.get("description"):
205
+ np["description"] = meta.get("description")
204
206
  if not np.get("friendly_token"):
205
207
  np["friendly_token"] = meta.get("friendly_token")
206
208
  # Ensure now-playing carries a playlist uid so the frontend can
@@ -217,6 +219,15 @@ class QueueShadow:
217
219
  ):
218
220
  np["uid"] = it.get("uid")
219
221
  break
222
+ # Attach category/tag facets for the now-playing detail panel.
223
+ ft = np.get("friendly_token")
224
+ if ft:
225
+ try:
226
+ facets = await db.get_item_facets(ft)
227
+ np["categories"] = facets.get("categories") or []
228
+ np["tags"] = facets.get("tags") or []
229
+ except Exception:
230
+ logger.debug("Failed to load now-playing facets", exc_info=True)
220
231
  state["now_playing"] = np
221
232
 
222
233
  return state
@@ -317,30 +317,42 @@ 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 card width. */
329
+ .np-title {
330
+ margin: 0;
334
331
  font-size: 1.4rem;
335
332
  line-height: 1.25;
336
333
  overflow-wrap: anywhere;
334
+ border-bottom: 1px solid var(--border);
335
+ padding-bottom: 0.75rem;
336
+ }
337
+ /* Poster (2:3) on the left, playback info on the right. */
338
+ .np-body {
339
+ display: flex;
340
+ gap: 1.25rem;
341
+ align-items: stretch;
342
+ }
343
+ .np-playback {
344
+ flex: 1;
345
+ min-width: 0;
346
+ display: flex;
347
+ flex-direction: column;
348
+ justify-content: flex-start;
349
+ gap: 0.5rem;
337
350
  }
338
351
  .np-progress {
339
352
  height: 6px;
340
353
  background: var(--border);
341
354
  border-radius: 3px;
342
355
  overflow: hidden;
343
- margin: 0.75rem 0;
344
356
  }
345
357
  .progress-bar {
346
358
  height: 100%;
@@ -348,8 +360,43 @@ a:hover {
348
360
  transition: width 1s linear;
349
361
  }
350
362
  .np-time {
351
- font-size: 0.9rem;
363
+ font-size: 0.95rem;
364
+ color: var(--text-primary);
365
+ }
366
+ /* Details below the poster/time row: category + tag chips, then description. */
367
+ .np-details {
368
+ border-top: 1px solid var(--border);
369
+ padding-top: 0.75rem;
370
+ display: flex;
371
+ flex-direction: column;
372
+ gap: 0.6rem;
373
+ }
374
+ .np-facets {
375
+ display: flex;
376
+ flex-wrap: wrap;
377
+ gap: 0.4rem;
378
+ }
379
+ .np-chip {
380
+ font-size: 0.7rem;
381
+ padding: 0.15rem 0.5rem;
382
+ border-radius: 999px;
383
+ white-space: nowrap;
384
+ }
385
+ .np-chip-cat {
386
+ background: var(--accent);
387
+ color: #fff;
388
+ }
389
+ .np-chip-tag {
390
+ background: var(--bg-elevated, #2a2a38);
352
391
  color: var(--text-secondary);
392
+ border: 1px solid var(--border);
393
+ }
394
+ .np-description {
395
+ font-size: 0.85rem;
396
+ line-height: 1.5;
397
+ color: var(--text-secondary);
398
+ margin: 0;
399
+ overflow-wrap: anywhere;
353
400
  }
354
401
 
355
402
  .queue-list {
@@ -412,8 +459,8 @@ a:hover {
412
459
  display: block;
413
460
  }
414
461
  .np-cover {
415
- width: 128px;
416
- height: 128px;
462
+ width: 120px;
463
+ aspect-ratio: 2 / 3;
417
464
  flex-shrink: 0;
418
465
  border-radius: 8px;
419
466
  overflow: hidden;
@@ -482,15 +529,13 @@ a:hover {
482
529
  margin: 0.25rem 0;
483
530
  }
484
531
  .np-times {
485
- display: flex;
486
- justify-content: space-between;
487
- gap: 1rem;
488
532
  font-size: 0.9rem;
489
- margin-top: 0.5rem;
490
533
  white-space: nowrap;
491
534
  }
492
535
  .np-remaining {
536
+ font-size: 0.85rem;
493
537
  color: var(--text-secondary);
538
+ margin-top: 0.1rem;
494
539
  }
495
540
 
496
541
  /* 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" checked onchange="toggleHidePrevious(this.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,34 @@ 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 facetHtml = [
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('');
111
+ const desc = (np.description || '').trim();
104
112
  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>
113
+ <h3 class="np-title">${escapeHtml(np.title || 'Unknown')}</h3>
114
+ ${np.paid_by ? `<div class="np-meta"><span class="np-user">Queued by ${escapeHtml(np.paid_by)}</span></div>` : ''}
115
+ <div class="np-body">
116
+ <div class="np-cover">${coverHtml(np)}</div>
117
+ <div class="np-playback">
118
+ <div class="np-times">
119
+ <span class="np-time">${formatTime(elapsed)} / ${formatTime(total)}</span>
120
+ </div>
121
+ <div class="np-progress">
122
+ <div class="progress-bar" style="width: ${pct}%"></div>
123
+ </div>
124
+ <div class="np-remaining">${formatTime(remaining)} remaining</div>
117
125
  </div>
118
126
  </div>
127
+ ${(desc || facetHtml) ? `
128
+ <div class="np-details">
129
+ ${facetHtml ? `<div class="np-facets">${facetHtml}</div>` : ''}
130
+ ${desc ? `<p class="np-description">${escapeHtml(desc)}</p>` : ''}
131
+ </div>` : ''}
119
132
  `;
120
133
  } else {
121
134
  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.1"
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"