kryten-webqueue 0.32.0__tar.gz → 0.32.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.
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/CHANGELOG.md +11 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/PKG-INFO +1 -1
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/db/_catalog.py +53 -0
- kryten_webqueue-0.32.1/kryten_webqueue/routes/admin_catalog.py +92 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/catalog/browse.html +36 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/pyproject.toml +1 -1
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_recently_played_hide.py +41 -0
- kryten_webqueue-0.32.0/kryten_webqueue/routes/admin_catalog.py +0 -45
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/.gitignore +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/README.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/config.example.json +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/tests/test_search_facets.py +0 -0
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
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
7
|
|
|
8
|
+
## [0.32.1] — 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Admin controls to test recently-played hiding in situ** without waiting for real playback:
|
|
13
|
+
- `POST /admin/catalog/{token}/mark-played` — simulates a genuine completion via the same `record_play_completion` the poll loop uses, so it also exercises the mutable-playlist pass logic (marking episodes, resetting on the last item).
|
|
14
|
+
- `POST /admin/catalog/{token}/clear-played` — removes an item's completion and playlist-pass rows so it reappears for regular users immediately.
|
|
15
|
+
- `GET /admin/catalog/recently-played/debug` — read-only snapshot of exactly what regular users are not seeing and why (time-window vs mutable-playlist pass).
|
|
16
|
+
- The admin catalog browser gains **Mark played** and **Clear played** buttons per item.
|
|
17
|
+
|
|
8
18
|
## [0.32.0] — 2026-07-09
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -17,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
27
|
- Promos are unaffected — promo/immutable-playlist items are already excluded from the public catalog.
|
|
18
28
|
|
|
19
29
|
[0.32.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.0
|
|
30
|
+
[0.32.1]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.1
|
|
20
31
|
|
|
21
32
|
## [0.31.0] — 2026-07-04
|
|
22
33
|
|
|
@@ -409,6 +409,59 @@ class _CatalogMixin:
|
|
|
409
409
|
)
|
|
410
410
|
return {r["media_id"] for r in rows}
|
|
411
411
|
|
|
412
|
+
async def clear_play_state(self, friendly_token: str, *, media_type: str = "cm") -> dict:
|
|
413
|
+
"""Remove all recently-played hide state for an item (admin test helper).
|
|
414
|
+
|
|
415
|
+
Deletes both time-boxed ``play_completions`` rows and any
|
|
416
|
+
``playlist_item_played`` (mutable-playlist pass) rows for the item, so it
|
|
417
|
+
immediately reappears in the public catalog. Returns the row counts
|
|
418
|
+
removed.
|
|
419
|
+
"""
|
|
420
|
+
pc = await self._fetch_one(
|
|
421
|
+
"SELECT COUNT(*) AS c FROM play_completions WHERE media_id = ? AND media_type = ?",
|
|
422
|
+
[friendly_token, media_type],
|
|
423
|
+
)
|
|
424
|
+
pip = await self._fetch_one(
|
|
425
|
+
"SELECT COUNT(*) AS c FROM playlist_item_played WHERE media_id = ? AND media_type = ?",
|
|
426
|
+
[friendly_token, media_type],
|
|
427
|
+
)
|
|
428
|
+
await self._execute(
|
|
429
|
+
"DELETE FROM play_completions WHERE media_id = ? AND media_type = ?",
|
|
430
|
+
[friendly_token, media_type],
|
|
431
|
+
)
|
|
432
|
+
await self._execute(
|
|
433
|
+
"DELETE FROM playlist_item_played WHERE media_id = ? AND media_type = ?",
|
|
434
|
+
[friendly_token, media_type],
|
|
435
|
+
)
|
|
436
|
+
return {"completions": pc["c"] if pc else 0, "playlist_pass": pip["c"] if pip else 0}
|
|
437
|
+
|
|
438
|
+
async def get_recently_played_debug(self, days: int) -> dict:
|
|
439
|
+
"""Snapshot of what the recently-played rules currently hide (admin test).
|
|
440
|
+
|
|
441
|
+
Mirrors the two exclusion signals used by browse/search so an admin can
|
|
442
|
+
see exactly which titles a regular user would NOT see, and why.
|
|
443
|
+
"""
|
|
444
|
+
by_completion = await self._fetch_all(
|
|
445
|
+
"SELECT pc.media_id, c.title, MAX(pc.completed_at) AS last_completed "
|
|
446
|
+
"FROM play_completions pc "
|
|
447
|
+
"LEFT JOIN catalog c ON c.friendly_token = pc.media_id "
|
|
448
|
+
"WHERE pc.media_type = 'cm' AND pc.completed_at >= datetime('now', ?) "
|
|
449
|
+
"GROUP BY pc.media_id ORDER BY last_completed DESC",
|
|
450
|
+
[f"-{int(days)} days"],
|
|
451
|
+
)
|
|
452
|
+
by_playlist_pass = await self._fetch_all(
|
|
453
|
+
"SELECT pip.media_id, c.title, pip.playlist_id, sp.name AS playlist_name, pip.position "
|
|
454
|
+
"FROM playlist_item_played pip "
|
|
455
|
+
"JOIN saved_playlists sp ON sp.id = pip.playlist_id "
|
|
456
|
+
"LEFT JOIN catalog c ON c.friendly_token = pip.media_id "
|
|
457
|
+
"WHERE pip.media_type = 'cm' ORDER BY sp.name, pip.position",
|
|
458
|
+
)
|
|
459
|
+
return {
|
|
460
|
+
"window_days": days,
|
|
461
|
+
"by_completion": by_completion,
|
|
462
|
+
"by_playlist_pass": by_playlist_pass,
|
|
463
|
+
}
|
|
464
|
+
|
|
412
465
|
async def get_catalog_brief(self, tokens: list[str], manifest_urls: list[str]) -> dict[str, dict]:
|
|
413
466
|
"""Return a lookup of catalog metadata keyed by BOTH friendly_token and
|
|
414
467
|
manifest_url, for enriching queue-shadow items that may only carry one.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, Depends, HTTPException
|
|
2
|
+
|
|
3
|
+
from ..auth.session import require_admin
|
|
4
|
+
from ..catalog.db import HIDDEN_ITEM_TAG
|
|
5
|
+
from ..catalog.mediacms import MediaCMSClient
|
|
6
|
+
|
|
7
|
+
router = APIRouter(prefix="/admin/catalog", tags=["admin"])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _mediacms_client(request: Request) -> MediaCMSClient:
|
|
11
|
+
config = request.app.state.config
|
|
12
|
+
return MediaCMSClient(mediacms_url=config.mediacms_url, token=config.mediacms_token)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post("/{friendly_token}/hide")
|
|
16
|
+
async def hide_item(request: Request, friendly_token: str, user: dict = Depends(require_admin)):
|
|
17
|
+
"""Tag an item ``kryten-hidden`` in MediaCMS and hide it locally (B6).
|
|
18
|
+
|
|
19
|
+
The local catalog is updated immediately so the admin sees the item
|
|
20
|
+
disappear; the next sync re-reads the tag from MediaCMS (the source of
|
|
21
|
+
truth) and the hide persists.
|
|
22
|
+
"""
|
|
23
|
+
db = request.app.state.db
|
|
24
|
+
item = await db.get_item_admin(friendly_token)
|
|
25
|
+
if not item:
|
|
26
|
+
raise HTTPException(404, "Catalog item not found")
|
|
27
|
+
|
|
28
|
+
# Local mirror first so the UI hides immediately regardless of the remote
|
|
29
|
+
# write outcome (which can be retried/confirmed by sync).
|
|
30
|
+
await db.add_catalog_tag(friendly_token, HIDDEN_ITEM_TAG)
|
|
31
|
+
remote_ok = await _mediacms_client(request).set_tag(friendly_token, HIDDEN_ITEM_TAG, present=True)
|
|
32
|
+
return {"success": True, "remote_ok": remote_ok}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("/{friendly_token}/unhide")
|
|
36
|
+
async def unhide_item(request: Request, friendly_token: str, user: dict = Depends(require_admin)):
|
|
37
|
+
"""Remove the ``kryten-hidden`` tag in MediaCMS and locally (B6)."""
|
|
38
|
+
db = request.app.state.db
|
|
39
|
+
item = await db.get_item_admin(friendly_token)
|
|
40
|
+
if not item:
|
|
41
|
+
raise HTTPException(404, "Catalog item not found")
|
|
42
|
+
|
|
43
|
+
await db.remove_catalog_tag(friendly_token, HIDDEN_ITEM_TAG)
|
|
44
|
+
remote_ok = await _mediacms_client(request).set_tag(friendly_token, HIDDEN_ITEM_TAG, present=False)
|
|
45
|
+
return {"success": True, "remote_ok": remote_ok}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- Recently-played test helpers (admin-only) -----------------------------
|
|
49
|
+
#
|
|
50
|
+
# These let an admin exercise the v0.32 "hide recently-played" rules in situ
|
|
51
|
+
# without waiting for real playback: simulate a completion, clear an item's
|
|
52
|
+
# hide state, or inspect exactly what a regular user would not see.
|
|
53
|
+
|
|
54
|
+
@router.post("/{friendly_token}/mark-played")
|
|
55
|
+
async def mark_played(request: Request, friendly_token: str, user: dict = Depends(require_admin)):
|
|
56
|
+
"""Simulate a genuine play-completion for an item (testing aid).
|
|
57
|
+
|
|
58
|
+
Routes through the same ``record_play_completion`` the poll loop uses, so it
|
|
59
|
+
exercises the real rules: short (<1h) episodes of a mutable playlist mark the
|
|
60
|
+
current pass (and reset it when the last item is marked), everything else
|
|
61
|
+
gets a time-boxed completion. The item then hides from regular users exactly
|
|
62
|
+
as it would after real playback.
|
|
63
|
+
"""
|
|
64
|
+
db = request.app.state.db
|
|
65
|
+
item = await db.get_item_admin(friendly_token)
|
|
66
|
+
if not item:
|
|
67
|
+
raise HTTPException(404, "Catalog item not found")
|
|
68
|
+
await db.record_play_completion(
|
|
69
|
+
friendly_token=friendly_token,
|
|
70
|
+
duration_sec=item.get("duration_sec"),
|
|
71
|
+
)
|
|
72
|
+
return {"success": True}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.post("/{friendly_token}/clear-played")
|
|
76
|
+
async def clear_played(request: Request, friendly_token: str, user: dict = Depends(require_admin)):
|
|
77
|
+
"""Clear an item's recently-played hide state so it reappears immediately."""
|
|
78
|
+
db = request.app.state.db
|
|
79
|
+
item = await db.get_item_admin(friendly_token)
|
|
80
|
+
if not item:
|
|
81
|
+
raise HTTPException(404, "Catalog item not found")
|
|
82
|
+
removed = await db.clear_play_state(friendly_token)
|
|
83
|
+
return {"success": True, "removed": removed}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get("/recently-played/debug")
|
|
87
|
+
async def recently_played_debug(request: Request, user: dict = Depends(require_admin)):
|
|
88
|
+
"""List what the recently-played rules currently hide from regular users."""
|
|
89
|
+
db = request.app.state.db
|
|
90
|
+
days = request.app.state.config.catalog_recently_played_hide_days
|
|
91
|
+
return await db.get_recently_played_debug(days)
|
|
92
|
+
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
@@ -94,6 +94,8 @@
|
|
|
94
94
|
<button class="btn btn-sm btn-admin" onclick="queueAsAdmin('{{ item.friendly_token }}')">Queue as Admin</button>
|
|
95
95
|
<button class="btn btn-sm" onclick="addToPlaylist('{{ item.friendly_token }}')">Add to playlist</button>
|
|
96
96
|
<button class="btn btn-sm" onclick="addToRecentPlaylist('{{ item.friendly_token }}')">+ Recent</button>
|
|
97
|
+
<button class="btn btn-sm" onclick="markPlayed('{{ item.friendly_token }}')" title="Simulate a play-completion so this item hides from regular users (testing)">Mark played</button>
|
|
98
|
+
<button class="btn btn-sm" onclick="clearPlayed('{{ item.friendly_token }}')" title="Clear recently-played hide state so this item reappears for regular users">Clear played</button>
|
|
97
99
|
<button class="btn btn-sm btn-danger" onclick="hideItem('{{ item.friendly_token }}')">Hide</button>
|
|
98
100
|
{% endif %}
|
|
99
101
|
</div>
|
|
@@ -378,6 +380,40 @@ async function hideItem(token) {
|
|
|
378
380
|
showToast(`Network error: ${e.message}`, 'error');
|
|
379
381
|
}
|
|
380
382
|
}
|
|
383
|
+
|
|
384
|
+
async function markPlayed(token) {
|
|
385
|
+
try {
|
|
386
|
+
const resp = await fetch(`/admin/catalog/${encodeURIComponent(token)}/mark-played`, {
|
|
387
|
+
method: 'POST', credentials: 'same-origin'
|
|
388
|
+
});
|
|
389
|
+
if (resp.ok) {
|
|
390
|
+
showToast('Marked played — now hidden from regular users');
|
|
391
|
+
} else {
|
|
392
|
+
const data = await resp.json().catch(() => ({}));
|
|
393
|
+
showToast(data.detail || `Failed (${resp.status})`, 'error');
|
|
394
|
+
}
|
|
395
|
+
} catch (e) {
|
|
396
|
+
showToast(`Network error: ${e.message}`, 'error');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function clearPlayed(token) {
|
|
401
|
+
try {
|
|
402
|
+
const resp = await fetch(`/admin/catalog/${encodeURIComponent(token)}/clear-played`, {
|
|
403
|
+
method: 'POST', credentials: 'same-origin'
|
|
404
|
+
});
|
|
405
|
+
if (resp.ok) {
|
|
406
|
+
const data = await resp.json().catch(() => ({}));
|
|
407
|
+
const r = data.removed || {};
|
|
408
|
+
showToast(`Cleared play state (${r.completions || 0} completion, ${r.playlist_pass || 0} pass)`);
|
|
409
|
+
} else {
|
|
410
|
+
const data = await resp.json().catch(() => ({}));
|
|
411
|
+
showToast(data.detail || `Failed (${resp.status})`, 'error');
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
showToast(`Network error: ${e.message}`, 'error');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
381
417
|
// queueItem / playNext / queueAsAdmin / showToast / escapeHtml are provided by /static/js/main.js
|
|
382
418
|
</script>
|
|
383
419
|
{% endblock %}
|
|
@@ -309,3 +309,44 @@ async def test_import_full_loads_entire_list(db):
|
|
|
309
309
|
assert result["skipped"] == 0
|
|
310
310
|
|
|
311
311
|
|
|
312
|
+
# --- Admin test helpers (mark-played / clear / debug) ---------------------
|
|
313
|
+
|
|
314
|
+
async def test_clear_play_state_unhides_time_window_item(db):
|
|
315
|
+
await _add_catalog(db, "movie", "Movie", duration_sec=7200)
|
|
316
|
+
await db.record_play_completion(friendly_token="movie", duration_sec=7200)
|
|
317
|
+
assert await _browse_tokens(db, recently_played_days=21) == set()
|
|
318
|
+
|
|
319
|
+
removed = await db.clear_play_state("movie")
|
|
320
|
+
assert removed == {"completions": 1, "playlist_pass": 0}
|
|
321
|
+
assert await _browse_tokens(db, recently_played_days=21) == {"movie"}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def test_clear_play_state_unhides_playlist_episode(db):
|
|
325
|
+
tokens = ["ep1", "ep2", "ep3"]
|
|
326
|
+
for t in tokens:
|
|
327
|
+
await _add_catalog(db, t, f"Episode {t}", duration_sec=1500)
|
|
328
|
+
await _make_mutable_playlist(db, tokens, duration_sec=1500)
|
|
329
|
+
await db.record_play_completion(friendly_token="ep1", duration_sec=1500)
|
|
330
|
+
assert "ep1" not in await _browse_tokens(db, recently_played_days=21)
|
|
331
|
+
|
|
332
|
+
removed = await db.clear_play_state("ep1")
|
|
333
|
+
assert removed["playlist_pass"] == 1
|
|
334
|
+
assert "ep1" in await _browse_tokens(db, recently_played_days=21)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
async def test_recently_played_debug_reports_both_signals(db):
|
|
338
|
+
await _add_catalog(db, "movie", "Movie", duration_sec=7200)
|
|
339
|
+
await _add_catalog(db, "ep1", "Episode 1", duration_sec=1500)
|
|
340
|
+
await _add_catalog(db, "ep2", "Episode 2", duration_sec=1500)
|
|
341
|
+
await _make_mutable_playlist(db, ["ep1", "ep2"], duration_sec=1500)
|
|
342
|
+
|
|
343
|
+
await db.record_play_completion(friendly_token="movie", duration_sec=7200) # time window
|
|
344
|
+
await db.record_play_completion(friendly_token="ep1", duration_sec=1500) # playlist pass
|
|
345
|
+
|
|
346
|
+
debug = await db.get_recently_played_debug(21)
|
|
347
|
+
assert debug["window_days"] == 21
|
|
348
|
+
assert {r["media_id"] for r in debug["by_completion"]} == {"movie"}
|
|
349
|
+
assert {r["media_id"] for r in debug["by_playlist_pass"]} == {"ep1"}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from fastapi import APIRouter, Request, Depends, HTTPException
|
|
2
|
-
|
|
3
|
-
from ..auth.session import require_admin
|
|
4
|
-
from ..catalog.db import HIDDEN_ITEM_TAG
|
|
5
|
-
from ..catalog.mediacms import MediaCMSClient
|
|
6
|
-
|
|
7
|
-
router = APIRouter(prefix="/admin/catalog", tags=["admin"])
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _mediacms_client(request: Request) -> MediaCMSClient:
|
|
11
|
-
config = request.app.state.config
|
|
12
|
-
return MediaCMSClient(mediacms_url=config.mediacms_url, token=config.mediacms_token)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@router.post("/{friendly_token}/hide")
|
|
16
|
-
async def hide_item(request: Request, friendly_token: str, user: dict = Depends(require_admin)):
|
|
17
|
-
"""Tag an item ``kryten-hidden`` in MediaCMS and hide it locally (B6).
|
|
18
|
-
|
|
19
|
-
The local catalog is updated immediately so the admin sees the item
|
|
20
|
-
disappear; the next sync re-reads the tag from MediaCMS (the source of
|
|
21
|
-
truth) and the hide persists.
|
|
22
|
-
"""
|
|
23
|
-
db = request.app.state.db
|
|
24
|
-
item = await db.get_item_admin(friendly_token)
|
|
25
|
-
if not item:
|
|
26
|
-
raise HTTPException(404, "Catalog item not found")
|
|
27
|
-
|
|
28
|
-
# Local mirror first so the UI hides immediately regardless of the remote
|
|
29
|
-
# write outcome (which can be retried/confirmed by sync).
|
|
30
|
-
await db.add_catalog_tag(friendly_token, HIDDEN_ITEM_TAG)
|
|
31
|
-
remote_ok = await _mediacms_client(request).set_tag(friendly_token, HIDDEN_ITEM_TAG, present=True)
|
|
32
|
-
return {"success": True, "remote_ok": remote_ok}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@router.post("/{friendly_token}/unhide")
|
|
36
|
-
async def unhide_item(request: Request, friendly_token: str, user: dict = Depends(require_admin)):
|
|
37
|
-
"""Remove the ``kryten-hidden`` tag in MediaCMS and locally (B6)."""
|
|
38
|
-
db = request.app.state.db
|
|
39
|
-
item = await db.get_item_admin(friendly_token)
|
|
40
|
-
if not item:
|
|
41
|
-
raise HTTPException(404, "Catalog item not found")
|
|
42
|
-
|
|
43
|
-
await db.remove_catalog_tag(friendly_token, HIDDEN_ITEM_TAG)
|
|
44
|
-
remote_ok = await _mediacms_client(request).set_tag(friendly_token, HIDDEN_ITEM_TAG, present=False)
|
|
45
|
-
return {"success": True, "remote_ok": remote_ok}
|
|
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.32.0 → kryten_webqueue-0.32.1}/docs/Log for incorrect promo insertion problem.txt
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/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
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/routes/admin_moderation.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
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-playlists.js
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-queue-mgmt.js
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/static/js/admin-schedules.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/feedback/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.1}/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
|
|
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
|