kryten-webqueue 0.9.3__tar.gz → 0.9.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.9.3 → kryten_webqueue-0.9.5}/CHANGELOG.md +17 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/PKG-INFO +1 -1
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/catalog/db.py +6 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/playlists/fire.py +23 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/admin_schedules.py +1 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/pages.py +4 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/static/css/main.css +15 -4
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/schedules.html +12 -1
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/base.html +1 -1
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/pyproject.toml +1 -1
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/tests/test_phase4_live_fixes.py +80 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/.gitignore +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/README.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/config.example.json +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/tests/__init__.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/tests/test_phase3_jobs.py +0 -0
|
@@ -6,6 +6,23 @@ 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
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.5] — 2026-06-11
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- No code changes — version bump only, to refresh the PyPI release index.
|
|
14
|
+
|
|
15
|
+
## [0.9.4] — 2026-06-11
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **App version in the footer.** Every view now shows the running `kryten-webqueue` version (e.g. `v0.9.4`) in the footer, sourced from the installed package metadata.
|
|
20
|
+
- **Scheduled-event fallback playlist.** Each schedule can name an optional *mutable* fallback playlist that is appended to the live CyTube queue right after the event's items, so the queue is no longer left empty once a scheduled event is exhausted — no manual intervention required. The fallback items aren't part of the "scheduled event", so they stay available for pay-to-play/search and don't affect when the event lock lifts. Configured via a dropdown (mutable playlists only) in the admin Schedule editor.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Playlist rows with a description no longer break row formatting.** The admin table action cell was a `display:flex` `<td>`, which dropped it from the table layout so its bottom border stopped aligning once a name + description grew taller. Action cells are now normal table cells (top-aligned, inline button layout), so buttons and row separators line up regardless of description length.
|
|
25
|
+
|
|
9
26
|
## [0.9.3] — 2026-06-11
|
|
10
27
|
|
|
11
28
|
### Fixed
|
|
@@ -284,6 +284,12 @@ MIGRATIONS = [
|
|
|
284
284
|
ALTER TABLE active_schedule ADD COLUMN last_item_uid INTEGER;
|
|
285
285
|
ALTER TABLE active_schedule ADD COLUMN lock_disabled INTEGER NOT NULL DEFAULT 0;
|
|
286
286
|
""",
|
|
287
|
+
# v9: Optional fallback (mutable) playlist appended to the live queue after a
|
|
288
|
+
# scheduled event's items, so the queue isn't left empty when the event is
|
|
289
|
+
# exhausted. NULL = no fallback (legacy behaviour).
|
|
290
|
+
"""
|
|
291
|
+
ALTER TABLE playlist_schedules ADD COLUMN fallback_playlist_id INTEGER REFERENCES saved_playlists(id) ON DELETE SET NULL;
|
|
292
|
+
""",
|
|
287
293
|
]
|
|
288
294
|
|
|
289
295
|
|
|
@@ -48,6 +48,29 @@ async def fire_schedule(*, schedule_id: int, api_gate, db, shadow, ws_manager):
|
|
|
48
48
|
except Exception as e:
|
|
49
49
|
logger.warning(f"Schedule fire: failed to add {item['media_id']}: {e}")
|
|
50
50
|
|
|
51
|
+
# Append the optional fallback (mutable) playlist AFTER the event items so
|
|
52
|
+
# the live queue isn't left empty once the event is exhausted. The
|
|
53
|
+
# fallback items are not part of the "scheduled event", so they do not
|
|
54
|
+
# change last_item_uid (the event lock still lifts when the last EVENT
|
|
55
|
+
# item begins) and they remain available for pay-to-play/search.
|
|
56
|
+
fallback_id = schedule.get("fallback_playlist_id")
|
|
57
|
+
if fallback_id:
|
|
58
|
+
fallback_items = await db.get_saved_playlist_items(fallback_id)
|
|
59
|
+
for item in fallback_items:
|
|
60
|
+
try:
|
|
61
|
+
await api_gate.playlist_add(
|
|
62
|
+
media_type=item["media_type"],
|
|
63
|
+
media_id=item["media_id"],
|
|
64
|
+
position="end",
|
|
65
|
+
)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.warning(f"Schedule fire: failed to add fallback {item['media_id']}: {e}")
|
|
68
|
+
if fallback_items:
|
|
69
|
+
logger.info(
|
|
70
|
+
f"Schedule {schedule_id}: appended {len(fallback_items)} fallback item(s) "
|
|
71
|
+
f"from playlist {fallback_id}"
|
|
72
|
+
)
|
|
73
|
+
|
|
51
74
|
# Update active schedule
|
|
52
75
|
now = datetime.now(UTC)
|
|
53
76
|
await db.set_active_schedule(
|
|
@@ -53,6 +53,7 @@ async def create_schedule(request: Request, user: dict = Depends(require_admin))
|
|
|
53
53
|
is_recurring=body.get("is_recurring", False),
|
|
54
54
|
rrule=body.get("rrule"),
|
|
55
55
|
pre_fire_lock_minutes=body.get("pre_fire_lock_minutes", 15),
|
|
56
|
+
fallback_playlist_id=body.get("fallback_playlist_id"),
|
|
56
57
|
is_active=True,
|
|
57
58
|
created_by=user["username"],
|
|
58
59
|
)
|
|
@@ -8,6 +8,10 @@ from ..auth.session import get_current_user
|
|
|
8
8
|
|
|
9
9
|
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
10
10
|
|
|
11
|
+
# Expose the package version to every template (used in the footer).
|
|
12
|
+
from .. import __version__ as _wq_version
|
|
13
|
+
templates.env.globals["app_version"] = _wq_version
|
|
14
|
+
|
|
11
15
|
router = APIRouter(tags=["pages"])
|
|
12
16
|
|
|
13
17
|
# Sort keys exposed in the browse/search UI. Order defines the dropdown order.
|
|
@@ -804,6 +804,7 @@ a.np-chip {
|
|
|
804
804
|
padding: 0.5rem;
|
|
805
805
|
text-align: left;
|
|
806
806
|
border-bottom: 1px solid var(--border);
|
|
807
|
+
vertical-align: top;
|
|
807
808
|
}
|
|
808
809
|
.admin-table th {
|
|
809
810
|
color: var(--text-secondary);
|
|
@@ -934,6 +935,11 @@ a.np-chip {
|
|
|
934
935
|
border-top: 1px solid var(--border);
|
|
935
936
|
margin-top: 4rem;
|
|
936
937
|
}
|
|
938
|
+
.footer-version {
|
|
939
|
+
opacity: 0.7;
|
|
940
|
+
margin-left: 0.35rem;
|
|
941
|
+
font-variant-numeric: tabular-nums;
|
|
942
|
+
}
|
|
937
943
|
|
|
938
944
|
/* Schedule info */
|
|
939
945
|
.schedule-info {
|
|
@@ -1056,10 +1062,15 @@ a.np-chip {
|
|
|
1056
1062
|
.btn-xs:hover { background: var(--bg-hover); }
|
|
1057
1063
|
.btn-xs:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
1058
1064
|
.row-actions {
|
|
1059
|
-
display:
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1065
|
+
/* Kept as a normal table cell: applying display:flex to a <td> drops it
|
|
1066
|
+
from the table layout, so its bottom border stops aligning with the rest
|
|
1067
|
+
of the row once another cell (e.g. a playlist name + description) grows
|
|
1068
|
+
taller. Lay the buttons out inline instead. */
|
|
1069
|
+
white-space: nowrap;
|
|
1070
|
+
text-align: right;
|
|
1071
|
+
}
|
|
1072
|
+
.row-actions > * + * {
|
|
1073
|
+
margin-left: 0.4rem;
|
|
1063
1074
|
}
|
|
1064
1075
|
.muted { color: var(--text-secondary); font-size: 0.85rem; }
|
|
1065
1076
|
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
{% block scripts %}
|
|
23
23
|
<script>
|
|
24
24
|
let playlistMap = {};
|
|
25
|
+
let playlistRows = [];
|
|
25
26
|
|
|
26
27
|
function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : str; return d.innerHTML; }
|
|
27
28
|
|
|
@@ -30,6 +31,7 @@ async function loadPlaylistsForSelect() {
|
|
|
30
31
|
if (!resp.ok) return [];
|
|
31
32
|
const rows = await resp.json();
|
|
32
33
|
playlistMap = {};
|
|
34
|
+
playlistRows = rows;
|
|
33
35
|
rows.forEach(p => playlistMap[p.id] = p.name);
|
|
34
36
|
return rows;
|
|
35
37
|
}
|
|
@@ -88,7 +90,7 @@ async function loadSchedules() {
|
|
|
88
90
|
const preFireLocked = inPreFire && !s.lock_disabled;
|
|
89
91
|
return `<tr>
|
|
90
92
|
<td>${escapeHtml(s.label)}${s.is_recurring ? ' <span class="badge">↻</span>' : ''}</td>
|
|
91
|
-
<td>${escapeHtml(playlistMap[s.playlist_id] || ('#' + s.playlist_id))}</td>
|
|
93
|
+
<td>${escapeHtml(playlistMap[s.playlist_id] || ('#' + s.playlist_id))}${s.fallback_playlist_id ? `<div class="muted">then: ${escapeHtml(playlistMap[s.fallback_playlist_id] || ('#' + s.fallback_playlist_id))}</div>` : ''}</td>
|
|
92
94
|
<td>${formatLocalDateTime(s.fire_at)}</td>
|
|
93
95
|
<td>${lockMin}m${preFireLocked ? ' <button class="btn btn-xs" onclick="unlockNow()" title="Lift the active pre-fire lock now">Unlock</button>' : (inPreFire && s.lock_disabled ? ' <span class="muted">(unlocked)</span>' : '')}</td>
|
|
94
96
|
<td><span class="job-status job-status-${fired === 'pending' ? 'running' : (fired === 'fired' ? 'completed' : 'cancelled')}">${s.is_active ? fired : 'disabled'}</span></td>
|
|
@@ -106,6 +108,11 @@ function scheduleForm(s) {
|
|
|
106
108
|
s = s || {};
|
|
107
109
|
const opts = Object.entries(playlistMap).map(([id, name]) =>
|
|
108
110
|
`<option value="${id}" ${s.playlist_id == id ? 'selected' : ''}>${escapeHtml(name)}</option>`).join('');
|
|
111
|
+
// Fallback options: mutable (non-immutable) playlists only, plus a "none" choice.
|
|
112
|
+
const mutable = playlistRows.filter(p => !p.is_immutable);
|
|
113
|
+
const fallbackOpts = '<option value="">— none (leave queue empty) —</option>' +
|
|
114
|
+
mutable.map(p =>
|
|
115
|
+
`<option value="${p.id}" ${s.fallback_playlist_id == p.id ? 'selected' : ''}>${escapeHtml(p.name)}</option>`).join('');
|
|
109
116
|
// fire_at → datetime-local value (local tz)
|
|
110
117
|
let dtLocal = '';
|
|
111
118
|
if (s.fire_at) {
|
|
@@ -116,6 +123,8 @@ function scheduleForm(s) {
|
|
|
116
123
|
<h3>${s.id ? 'Edit' : 'New'} Schedule</h3>
|
|
117
124
|
<label class="field"><span>Label</span><input type="text" id="s-label" value="${escapeHtml(s.label || '')}"></label>
|
|
118
125
|
<label class="field"><span>Playlist</span><select id="s-playlist">${opts}</select></label>
|
|
126
|
+
<label class="field"><span>Fallback playlist</span><select id="s-fallback">${fallbackOpts}</select></label>
|
|
127
|
+
<p class="muted" style="font-size:0.8rem;margin-top:-0.25rem;">Plays after the scheduled playlist is exhausted, so the queue isn't left empty. Only mutable playlists can be used.</p>
|
|
119
128
|
<label class="field"><span>Fire at</span><input type="datetime-local" id="s-fireat" value="${dtLocal}"></label>
|
|
120
129
|
<label class="field"><span>Pre-fire lock (min)</span><input type="number" id="s-lock" min="0" value="${s.pre_fire_lock_minutes ?? 15}"></label>
|
|
121
130
|
<label class="check"><input type="checkbox" id="s-recur" ${s.is_recurring ? 'checked' : ''}> Recurring</label>
|
|
@@ -151,6 +160,8 @@ function collectSchedule() {
|
|
|
151
160
|
is_recurring: document.getElementById('s-recur').checked,
|
|
152
161
|
rrule: document.getElementById('s-rrule').value.trim() || null,
|
|
153
162
|
};
|
|
163
|
+
const fallbackVal = document.getElementById('s-fallback').value;
|
|
164
|
+
body.fallback_playlist_id = fallbackVal ? parseInt(fallbackVal, 10) : null;
|
|
154
165
|
const active = document.getElementById('s-active');
|
|
155
166
|
if (active) body.is_active = active.checked;
|
|
156
167
|
return body;
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</main>
|
|
36
36
|
|
|
37
37
|
<footer class="footer">
|
|
38
|
-
<p>© Channel-Z — Powered by <a href="https://github.com/grobertson/kryten-webqueue" target="_blank" rel="noopener">kryten-webqueue</a
|
|
38
|
+
<p>© Channel-Z — Powered by <a href="https://github.com/grobertson/kryten-webqueue" target="_blank" rel="noopener">kryten-webqueue</a>{% if app_version %} <span class="footer-version">v{{ app_version }}</span>{% endif %}</p>
|
|
39
39
|
</footer>
|
|
40
40
|
|
|
41
41
|
<script src="/static/js/main.js"></script>
|
|
@@ -168,3 +168,83 @@ async def test_pre_fire_lock_can_be_disabled(db):
|
|
|
168
168
|
|
|
169
169
|
await db.update_schedule(sid, lock_disabled=1)
|
|
170
170
|
assert await db.is_pre_fire_lock_active() is False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --- #2 (v0.9.4) scheduled-event fallback playlist ---
|
|
174
|
+
|
|
175
|
+
class _FakeApiGate:
|
|
176
|
+
"""Minimal api_gate stub recording playlist_add calls."""
|
|
177
|
+
|
|
178
|
+
def __init__(self):
|
|
179
|
+
self.added = []
|
|
180
|
+
self._uid = 0
|
|
181
|
+
self.cleared = False
|
|
182
|
+
|
|
183
|
+
async def playlist_clear(self):
|
|
184
|
+
self.cleared = True
|
|
185
|
+
|
|
186
|
+
async def playlist_add(self, *, media_type, media_id, position="end"):
|
|
187
|
+
self._uid += 1
|
|
188
|
+
self.added.append({"media_type": media_type, "media_id": media_id, "uid": self._uid})
|
|
189
|
+
return {"success": True, "uid": self._uid}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _FakeWs:
|
|
193
|
+
async def broadcast(self, *_a, **_k):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def test_fire_appends_fallback_after_event(db):
|
|
198
|
+
from kryten_webqueue.playlists.fire import fire_schedule
|
|
199
|
+
|
|
200
|
+
event_pid = await db.create_saved_playlist(
|
|
201
|
+
name="Event", description=None, is_immutable=True, created_by="admin"
|
|
202
|
+
)
|
|
203
|
+
await db.replace_playlist_items(event_pid, [
|
|
204
|
+
{"media_type": "cm", "media_id": "e1", "title": "E1", "duration_sec": 100},
|
|
205
|
+
{"media_type": "cm", "media_id": "e2", "title": "E2", "duration_sec": 100},
|
|
206
|
+
])
|
|
207
|
+
fallback_pid = await db.create_saved_playlist(
|
|
208
|
+
name="Filler", description=None, is_immutable=False, created_by="admin"
|
|
209
|
+
)
|
|
210
|
+
await db.replace_playlist_items(fallback_pid, [
|
|
211
|
+
{"media_type": "cm", "media_id": "f1", "title": "F1", "duration_sec": 100},
|
|
212
|
+
])
|
|
213
|
+
sid = await db.create_schedule(
|
|
214
|
+
playlist_id=event_pid, label="Event",
|
|
215
|
+
fire_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
216
|
+
is_active=1, created_by="admin", fallback_playlist_id=fallback_pid,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
api = _FakeApiGate()
|
|
220
|
+
shadow = QueueShadow(db)
|
|
221
|
+
await fire_schedule(schedule_id=sid, api_gate=api, db=db, shadow=shadow, ws_manager=_FakeWs())
|
|
222
|
+
|
|
223
|
+
# Event items first, fallback appended after.
|
|
224
|
+
assert [a["media_id"] for a in api.added] == ["e1", "e2", "f1"]
|
|
225
|
+
|
|
226
|
+
# The event lock's last item is the last EVENT item (e2, uid=2), NOT the
|
|
227
|
+
# fallback — so the lock lifts when e2 starts, not when the filler plays.
|
|
228
|
+
active = await db.get_active_schedule()
|
|
229
|
+
assert active["last_item_uid"] == 2
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async def test_fire_without_fallback_only_adds_event(db):
|
|
233
|
+
from kryten_webqueue.playlists.fire import fire_schedule
|
|
234
|
+
|
|
235
|
+
event_pid = await db.create_saved_playlist(
|
|
236
|
+
name="Event", description=None, is_immutable=True, created_by="admin"
|
|
237
|
+
)
|
|
238
|
+
await db.replace_playlist_items(event_pid, [
|
|
239
|
+
{"media_type": "cm", "media_id": "e1", "title": "E1", "duration_sec": 100},
|
|
240
|
+
])
|
|
241
|
+
sid = await db.create_schedule(
|
|
242
|
+
playlist_id=event_pid, label="Event",
|
|
243
|
+
fire_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
244
|
+
is_active=1, created_by="admin",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
api = _FakeApiGate()
|
|
248
|
+
shadow = QueueShadow(db)
|
|
249
|
+
await fire_schedule(schedule_id=sid, api_gate=api, db=db, shadow=shadow, ws_manager=_FakeWs())
|
|
250
|
+
assert [a["media_id"] for a in api.added] == ["e1"]
|
|
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.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/enrichmeta.py
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
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.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.9.3 → kryten_webqueue-0.9.5}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|