kryten-webqueue 0.6.4__tar.gz → 0.6.6__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.6.4 → kryten_webqueue-0.6.6}/CHANGELOG.md +14 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/PKG-INFO +1 -1
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/catalog/db.py +10 -8
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/queue/shadow.py +14 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/static/css/main.css +27 -13
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/queue/index.html +12 -2
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/pyproject.toml +1 -1
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/.gitignore +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/README.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/config.example.json +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/ws/manager.py +0 -0
|
@@ -4,6 +4,20 @@ 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.6.6] - 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Queue view polish.** Three refinements to the Queue page: (1) **Predicted start times** now render reliably in the viewer's local timezone — the frontend defensively treats timezone-less timestamps as UTC before converting, so ETAs no longer risk being misread as server time. (2) **Now Playing card** is larger and easier to read — bigger cover art (128px), a larger title that wraps cleanly, and the elapsed/remaining times no longer wrap. (3) **The currently-playing item is now highlighted** in the queue list with an accent tint and ring. The now-playing playlist `uid` is resolved server-side (matching media id/type against the shadow playlist when CyTube's `changeMedia` payload omits it) so the matching queue item is identified reliably.
|
|
12
|
+
|
|
13
|
+
## [0.6.5] - 2026-06-08
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Refined catalog browse ordering** — Tightened the quality-weighted sort introduced in 0.6.4 based on feedback. (1) Dropped the popularity (times-queued) tier, which discouraged discovery. (2) Replaced the weak "has cover art" test with a real-poster signal: items are led by `cover_art_source IN ('tmdb', 'omdb')` rather than mere presence of a `cover_art_path`/`thumbnail_url` — nearly every item carries a MediaCMS thumbnail, and the resolver also caches that thumbnail as a last-resort cover (`cover_art_source = 'thumbnail'`), so the old test was almost always true. A genuine TMDB/OMDB poster match also implies a well-formed, matchable title. Letter-first-then-alphabetical ordering (which correctly sinks `"02 - Episode"` style entries) is retained.
|
|
18
|
+
|
|
19
|
+
[0.6.5]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.6.5
|
|
20
|
+
|
|
7
21
|
## [0.6.4] - 2026-06-08
|
|
8
22
|
|
|
9
23
|
### Changed
|
|
@@ -238,8 +238,7 @@ class Database:
|
|
|
238
238
|
|
|
239
239
|
async def browse(self, *, category: str | None = None, page: int = 1, per_page: int = 24) -> list[dict]:
|
|
240
240
|
query = """
|
|
241
|
-
SELECT c.friendly_token, c.title, c.duration_sec, c.cover_art_path, c.thumbnail_url, c.manifest_url
|
|
242
|
-
(SELECT COUNT(*) FROM queue_history qh WHERE qh.friendly_token = c.friendly_token) AS play_count
|
|
241
|
+
SELECT c.friendly_token, c.title, c.duration_sec, c.cover_art_path, c.thumbnail_url, c.manifest_url
|
|
243
242
|
FROM catalog c
|
|
244
243
|
WHERE c.friendly_token NOT IN (
|
|
245
244
|
SELECT spi.media_id FROM saved_playlist_items spi
|
|
@@ -260,15 +259,18 @@ class Database:
|
|
|
260
259
|
# Quality-weighted ordering so the landing page leads with presentable
|
|
261
260
|
# items instead of alphabetical junk. No curation required — every signal
|
|
262
261
|
# is derived from existing data:
|
|
263
|
-
# 1. Items with box art
|
|
264
|
-
#
|
|
265
|
-
#
|
|
262
|
+
# 1. Items with REAL box art first. The strong signal is a poster match
|
|
263
|
+
# from TMDB/OMDB (cover_art_source), NOT mere presence of a
|
|
264
|
+
# cover_art_path/thumbnail_url — almost every item carries a MediaCMS
|
|
265
|
+
# thumbnail, and the resolver also caches that thumbnail as a
|
|
266
|
+
# last-resort cover (cover_art_source = 'thumbnail'). A genuine
|
|
267
|
+
# poster match also implies a well-formed, matchable title.
|
|
268
|
+
# 2. Titles beginning with a letter before number/symbol-prefixed
|
|
266
269
|
# "02 - Episode" style entries.
|
|
267
|
-
#
|
|
270
|
+
# 3. Finally alphabetical for a stable, predictable tail.
|
|
268
271
|
query += """
|
|
269
272
|
ORDER BY
|
|
270
|
-
(c.
|
|
271
|
-
play_count DESC,
|
|
273
|
+
(c.cover_art_source IN ('tmdb', 'omdb')) DESC,
|
|
272
274
|
(CASE WHEN c.title GLOB '[A-Za-z]*' THEN 0 ELSE 1 END) ASC,
|
|
273
275
|
c.title ASC
|
|
274
276
|
LIMIT ? OFFSET ?
|
|
@@ -203,6 +203,20 @@ class QueueShadow:
|
|
|
203
203
|
np.setdefault("thumbnail_url", meta.get("thumbnail_url"))
|
|
204
204
|
if not np.get("friendly_token"):
|
|
205
205
|
np["friendly_token"] = meta.get("friendly_token")
|
|
206
|
+
# Ensure now-playing carries a playlist uid so the frontend can
|
|
207
|
+
# highlight the matching queue item. CyTube's changeMedia payload
|
|
208
|
+
# lacks a uid; recover it by matching media id/type against the
|
|
209
|
+
# shadow playlist (whose items do carry uids).
|
|
210
|
+
if np.get("uid") is None:
|
|
211
|
+
np_id = np.get("id")
|
|
212
|
+
np_type = np.get("type")
|
|
213
|
+
if np_id is not None:
|
|
214
|
+
for it in enriched_items:
|
|
215
|
+
if it.get("media_id") == np_id and (
|
|
216
|
+
np_type is None or it.get("media_type") == np_type
|
|
217
|
+
):
|
|
218
|
+
np["uid"] = it.get("uid")
|
|
219
|
+
break
|
|
206
220
|
state["now_playing"] = np
|
|
207
221
|
|
|
208
222
|
return state
|
|
@@ -249,10 +249,10 @@ a:hover {
|
|
|
249
249
|
.now-playing-card {
|
|
250
250
|
background: var(--bg-card);
|
|
251
251
|
border-radius: var(--radius);
|
|
252
|
-
padding:
|
|
252
|
+
padding: 2rem;
|
|
253
253
|
display: flex;
|
|
254
|
-
gap:
|
|
255
|
-
align-items:
|
|
254
|
+
gap: 1.5rem;
|
|
255
|
+
align-items: center;
|
|
256
256
|
/* Highlight the currently-playing item: subtle accent ring + glow. */
|
|
257
257
|
border: 1px solid var(--accent);
|
|
258
258
|
box-shadow: 0 0 0 1px rgba(108, 92, 231, 0.25), 0 6px 18px rgba(108, 92, 231, 0.18);
|
|
@@ -263,13 +263,16 @@ a:hover {
|
|
|
263
263
|
}
|
|
264
264
|
.np-info h3 {
|
|
265
265
|
margin-bottom: 0.5rem;
|
|
266
|
+
font-size: 1.4rem;
|
|
267
|
+
line-height: 1.25;
|
|
268
|
+
overflow-wrap: anywhere;
|
|
266
269
|
}
|
|
267
270
|
.np-progress {
|
|
268
|
-
height:
|
|
271
|
+
height: 6px;
|
|
269
272
|
background: var(--border);
|
|
270
|
-
border-radius:
|
|
273
|
+
border-radius: 3px;
|
|
271
274
|
overflow: hidden;
|
|
272
|
-
margin: 0.
|
|
275
|
+
margin: 0.75rem 0;
|
|
273
276
|
}
|
|
274
277
|
.progress-bar {
|
|
275
278
|
height: 100%;
|
|
@@ -277,7 +280,7 @@ a:hover {
|
|
|
277
280
|
transition: width 1s linear;
|
|
278
281
|
}
|
|
279
282
|
.np-time {
|
|
280
|
-
font-size: 0.
|
|
283
|
+
font-size: 0.9rem;
|
|
281
284
|
color: var(--text-secondary);
|
|
282
285
|
}
|
|
283
286
|
|
|
@@ -304,6 +307,15 @@ a:hover {
|
|
|
304
307
|
.queue-item-paid {
|
|
305
308
|
border-left-color: var(--accent);
|
|
306
309
|
}
|
|
310
|
+
.queue-item-now-playing {
|
|
311
|
+
border-left-color: var(--accent);
|
|
312
|
+
background: linear-gradient(90deg, rgba(108, 92, 231, 0.18), var(--bg-card) 60%);
|
|
313
|
+
box-shadow: inset 0 0 0 1px rgba(108, 92, 231, 0.35);
|
|
314
|
+
}
|
|
315
|
+
.queue-item-now-playing .qi-title {
|
|
316
|
+
color: var(--accent);
|
|
317
|
+
font-weight: 600;
|
|
318
|
+
}
|
|
307
319
|
.qi-pos {
|
|
308
320
|
font-size: 0.75rem;
|
|
309
321
|
color: var(--text-secondary);
|
|
@@ -332,10 +344,10 @@ a:hover {
|
|
|
332
344
|
display: block;
|
|
333
345
|
}
|
|
334
346
|
.np-cover {
|
|
335
|
-
width:
|
|
336
|
-
height:
|
|
347
|
+
width: 128px;
|
|
348
|
+
height: 128px;
|
|
337
349
|
flex-shrink: 0;
|
|
338
|
-
border-radius:
|
|
350
|
+
border-radius: 8px;
|
|
339
351
|
overflow: hidden;
|
|
340
352
|
background: var(--bg-elevated, #222);
|
|
341
353
|
}
|
|
@@ -397,15 +409,17 @@ a:hover {
|
|
|
397
409
|
color: var(--text-secondary);
|
|
398
410
|
}
|
|
399
411
|
.np-meta {
|
|
400
|
-
font-size: 0.
|
|
412
|
+
font-size: 0.9rem;
|
|
401
413
|
color: var(--text-secondary);
|
|
402
414
|
margin: 0.25rem 0;
|
|
403
415
|
}
|
|
404
416
|
.np-times {
|
|
405
417
|
display: flex;
|
|
406
418
|
justify-content: space-between;
|
|
407
|
-
|
|
408
|
-
|
|
419
|
+
gap: 1rem;
|
|
420
|
+
font-size: 0.9rem;
|
|
421
|
+
margin-top: 0.5rem;
|
|
422
|
+
white-space: nowrap;
|
|
409
423
|
}
|
|
410
424
|
.np-remaining {
|
|
411
425
|
color: var(--text-secondary);
|
|
@@ -110,8 +110,10 @@ function renderQueue(state) {
|
|
|
110
110
|
const totalDuration = state.items.reduce((sum, i) => sum + (i.duration_sec || 0), 0);
|
|
111
111
|
sumEl.innerHTML = `<span>${state.items.length} items · ${formatTime(totalDuration)} total</span>`;
|
|
112
112
|
|
|
113
|
+
const nowUid = state.now_playing && state.now_playing.uid != null
|
|
114
|
+
? String(state.now_playing.uid) : null;
|
|
113
115
|
qlEl.innerHTML = state.items.map((item, i) => `
|
|
114
|
-
<div class="queue-item ${item.is_pay ? 'queue-item-paid' : ''}" data-uid="${item.uid}">
|
|
116
|
+
<div class="queue-item ${item.is_pay ? 'queue-item-paid' : ''} ${nowUid != null && String(item.uid) === nowUid ? 'queue-item-now-playing' : ''}" data-uid="${item.uid}">
|
|
115
117
|
<span class="qi-drag" title="Drag to reorder">☰</span>
|
|
116
118
|
<span class="qi-pos">${i + 1}</span>
|
|
117
119
|
<div class="qi-cover">${coverHtml(item)}</div>
|
|
@@ -166,7 +168,15 @@ function coverHtml(item) {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function formatEta(isoStr) {
|
|
169
|
-
|
|
171
|
+
// Server emits UTC ISO timestamps. If a value arrives without an explicit
|
|
172
|
+
// timezone designator, treat it as UTC so the browser converts it to the
|
|
173
|
+
// viewer's local timezone rather than misreading it as local time.
|
|
174
|
+
let s = String(isoStr);
|
|
175
|
+
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) {
|
|
176
|
+
s += 'Z';
|
|
177
|
+
}
|
|
178
|
+
const d = new Date(s);
|
|
179
|
+
if (isNaN(d.getTime())) return '';
|
|
170
180
|
return d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
|
|
171
181
|
}
|
|
172
182
|
|
|
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
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.6.4 → kryten_webqueue-0.6.6}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|