kryten-webqueue 0.32.0__tar.gz → 0.32.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/CHANGELOG.md +18 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/PKG-INFO +1 -1
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/db/_catalog.py +88 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/db/_connection.py +18 -0
- kryten_webqueue-0.32.2/kryten_webqueue/routes/admin_catalog.py +92 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/catalog/browse.html +36 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/pyproject.toml +1 -1
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_recently_played_hide.py +89 -0
- kryten_webqueue-0.32.0/kryten_webqueue/routes/admin_catalog.py +0 -45
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/.gitignore +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/README.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/config.example.json +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/__init__.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/tests/test_search_facets.py +0 -0
|
@@ -5,6 +5,22 @@ 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.2] — 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Promo clips are exempt from recently-played hiding.** Playing a promo-pool clip no longer records any hide state — promos are already excluded from the public catalog and must not be treated like normal mutable/immutable playlist items. `record_play_completion` now skips promo-pool members entirely. A one-time migration (v14) purges any promo hide state recorded before this fix; the same cleanup is also available on-demand via `purge_promo_hide_state()`.
|
|
13
|
+
|
|
14
|
+
## [0.32.1] — 2026-07-09
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Admin controls to test recently-played hiding in situ** without waiting for real playback:
|
|
19
|
+
- `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).
|
|
20
|
+
- `POST /admin/catalog/{token}/clear-played` — removes an item's completion and playlist-pass rows so it reappears for regular users immediately.
|
|
21
|
+
- `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).
|
|
22
|
+
- The admin catalog browser gains **Mark played** and **Clear played** buttons per item.
|
|
23
|
+
|
|
8
24
|
## [0.32.0] — 2026-07-09
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -17,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
33
|
- Promos are unaffected — promo/immutable-playlist items are already excluded from the public catalog.
|
|
18
34
|
|
|
19
35
|
[0.32.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.0
|
|
36
|
+
[0.32.1]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.1
|
|
37
|
+
[0.32.2]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.32.2
|
|
20
38
|
|
|
21
39
|
## [0.31.0] — 2026-07-04
|
|
22
40
|
|
|
@@ -360,7 +360,19 @@ class _CatalogMixin:
|
|
|
360
360
|
collection releases together. Everything else (movies, standalone clips,
|
|
361
361
|
long items even inside a mutable playlist) gets a time-boxed
|
|
362
362
|
``play_completions`` row instead.
|
|
363
|
+
|
|
364
|
+
Promo-pool clips are exempt entirely: they are already excluded from the
|
|
365
|
+
public catalog and must never be treated like normal (mutable/immutable)
|
|
366
|
+
playlist items, so playing one records nothing.
|
|
363
367
|
"""
|
|
368
|
+
promo = await self._fetch_one(
|
|
369
|
+
"SELECT 1 FROM saved_playlist_items spi "
|
|
370
|
+
"JOIN saved_playlists sp ON sp.id = spi.playlist_id "
|
|
371
|
+
"WHERE spi.media_id = ? AND spi.media_type = ? AND sp.promo_type IS NOT NULL LIMIT 1",
|
|
372
|
+
[friendly_token, media_type],
|
|
373
|
+
)
|
|
374
|
+
if promo:
|
|
375
|
+
return
|
|
364
376
|
rows = await self._fetch_all(
|
|
365
377
|
"""
|
|
366
378
|
SELECT spi.playlist_id AS playlist_id, spi.position AS position,
|
|
@@ -409,6 +421,82 @@ class _CatalogMixin:
|
|
|
409
421
|
)
|
|
410
422
|
return {r["media_id"] for r in rows}
|
|
411
423
|
|
|
424
|
+
async def clear_play_state(self, friendly_token: str, *, media_type: str = "cm") -> dict:
|
|
425
|
+
"""Remove all recently-played hide state for an item (admin test helper).
|
|
426
|
+
|
|
427
|
+
Deletes both time-boxed ``play_completions`` rows and any
|
|
428
|
+
``playlist_item_played`` (mutable-playlist pass) rows for the item, so it
|
|
429
|
+
immediately reappears in the public catalog. Returns the row counts
|
|
430
|
+
removed.
|
|
431
|
+
"""
|
|
432
|
+
pc = await self._fetch_one(
|
|
433
|
+
"SELECT COUNT(*) AS c FROM play_completions WHERE media_id = ? AND media_type = ?",
|
|
434
|
+
[friendly_token, media_type],
|
|
435
|
+
)
|
|
436
|
+
pip = await self._fetch_one(
|
|
437
|
+
"SELECT COUNT(*) AS c FROM playlist_item_played WHERE media_id = ? AND media_type = ?",
|
|
438
|
+
[friendly_token, media_type],
|
|
439
|
+
)
|
|
440
|
+
await self._execute(
|
|
441
|
+
"DELETE FROM play_completions WHERE media_id = ? AND media_type = ?",
|
|
442
|
+
[friendly_token, media_type],
|
|
443
|
+
)
|
|
444
|
+
await self._execute(
|
|
445
|
+
"DELETE FROM playlist_item_played WHERE media_id = ? AND media_type = ?",
|
|
446
|
+
[friendly_token, media_type],
|
|
447
|
+
)
|
|
448
|
+
return {"completions": pc["c"] if pc else 0, "playlist_pass": pip["c"] if pip else 0}
|
|
449
|
+
|
|
450
|
+
async def purge_promo_hide_state(self) -> dict:
|
|
451
|
+
"""Remove any recently-played hide state recorded for promo-pool clips.
|
|
452
|
+
|
|
453
|
+
Promos must never be subject to recently-played hiding. New completions
|
|
454
|
+
are already skipped in ``record_play_completion``; this purges rows
|
|
455
|
+
written before that exemption existed (also run once by migration v14).
|
|
456
|
+
Returns the row counts removed.
|
|
457
|
+
"""
|
|
458
|
+
promo_subquery = (
|
|
459
|
+
"SELECT spi.media_id FROM saved_playlist_items spi "
|
|
460
|
+
"JOIN saved_playlists sp ON sp.id = spi.playlist_id "
|
|
461
|
+
"WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'"
|
|
462
|
+
)
|
|
463
|
+
pc = await self._fetch_one(
|
|
464
|
+
f"SELECT COUNT(*) AS c FROM play_completions WHERE media_id IN ({promo_subquery})"
|
|
465
|
+
)
|
|
466
|
+
pip = await self._fetch_one(
|
|
467
|
+
f"SELECT COUNT(*) AS c FROM playlist_item_played WHERE media_id IN ({promo_subquery})"
|
|
468
|
+
)
|
|
469
|
+
await self._execute(f"DELETE FROM play_completions WHERE media_id IN ({promo_subquery})")
|
|
470
|
+
await self._execute(f"DELETE FROM playlist_item_played WHERE media_id IN ({promo_subquery})")
|
|
471
|
+
return {"completions": pc["c"] if pc else 0, "playlist_pass": pip["c"] if pip else 0}
|
|
472
|
+
|
|
473
|
+
async def get_recently_played_debug(self, days: int) -> dict:
|
|
474
|
+
"""Snapshot of what the recently-played rules currently hide (admin test).
|
|
475
|
+
|
|
476
|
+
Mirrors the two exclusion signals used by browse/search so an admin can
|
|
477
|
+
see exactly which titles a regular user would NOT see, and why.
|
|
478
|
+
"""
|
|
479
|
+
by_completion = await self._fetch_all(
|
|
480
|
+
"SELECT pc.media_id, c.title, MAX(pc.completed_at) AS last_completed "
|
|
481
|
+
"FROM play_completions pc "
|
|
482
|
+
"LEFT JOIN catalog c ON c.friendly_token = pc.media_id "
|
|
483
|
+
"WHERE pc.media_type = 'cm' AND pc.completed_at >= datetime('now', ?) "
|
|
484
|
+
"GROUP BY pc.media_id ORDER BY last_completed DESC",
|
|
485
|
+
[f"-{int(days)} days"],
|
|
486
|
+
)
|
|
487
|
+
by_playlist_pass = await self._fetch_all(
|
|
488
|
+
"SELECT pip.media_id, c.title, pip.playlist_id, sp.name AS playlist_name, pip.position "
|
|
489
|
+
"FROM playlist_item_played pip "
|
|
490
|
+
"JOIN saved_playlists sp ON sp.id = pip.playlist_id "
|
|
491
|
+
"LEFT JOIN catalog c ON c.friendly_token = pip.media_id "
|
|
492
|
+
"WHERE pip.media_type = 'cm' ORDER BY sp.name, pip.position",
|
|
493
|
+
)
|
|
494
|
+
return {
|
|
495
|
+
"window_days": days,
|
|
496
|
+
"by_completion": by_completion,
|
|
497
|
+
"by_playlist_pass": by_playlist_pass,
|
|
498
|
+
}
|
|
499
|
+
|
|
412
500
|
async def get_catalog_brief(self, tokens: list[str], manifest_urls: list[str]) -> dict[str, dict]:
|
|
413
501
|
"""Return a lookup of catalog metadata keyed by BOTH friendly_token and
|
|
414
502
|
manifest_url, for enriching queue-shadow items that may only carry one.
|
|
@@ -291,6 +291,24 @@ MIGRATIONS = [
|
|
|
291
291
|
);
|
|
292
292
|
CREATE INDEX IF NOT EXISTS idx_playlist_item_played_media ON playlist_item_played(media_id);
|
|
293
293
|
""",
|
|
294
|
+
# v14: One-time cleanup of recently-played hide state wrongly recorded for
|
|
295
|
+
# promo-pool clips. Promos are excluded from the public catalog and must
|
|
296
|
+
# never be treated like normal playlist items, so any hide state recorded for
|
|
297
|
+
# a promo clip (before the exemption in record_play_completion) is purged.
|
|
298
|
+
"""
|
|
299
|
+
DELETE FROM play_completions
|
|
300
|
+
WHERE media_id IN (
|
|
301
|
+
SELECT spi.media_id FROM saved_playlist_items spi
|
|
302
|
+
JOIN saved_playlists sp ON sp.id = spi.playlist_id
|
|
303
|
+
WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'
|
|
304
|
+
);
|
|
305
|
+
DELETE FROM playlist_item_played
|
|
306
|
+
WHERE media_id IN (
|
|
307
|
+
SELECT spi.media_id FROM saved_playlist_items spi
|
|
308
|
+
JOIN saved_playlists sp ON sp.id = spi.playlist_id
|
|
309
|
+
WHERE sp.promo_type IS NOT NULL AND spi.media_type = 'cm'
|
|
310
|
+
);
|
|
311
|
+
""",
|
|
294
312
|
]
|
|
295
313
|
|
|
296
314
|
|
|
@@ -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.2}/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,92 @@ 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
|
+
async def test_promo_clips_are_exempt_from_hiding(db):
|
|
353
|
+
# A clip in a promo pool (promo_type set) must never be recorded as played
|
|
354
|
+
# nor appear in the recently-played debug list.
|
|
355
|
+
await _add_catalog(db, "promo1", "Bumper", duration_sec=30)
|
|
356
|
+
pid = await db.create_saved_playlist(
|
|
357
|
+
name="Bumpers", description=None, is_immutable=False,
|
|
358
|
+
created_by="admin", promo_type="general",
|
|
359
|
+
)
|
|
360
|
+
await db.replace_playlist_items(pid, [
|
|
361
|
+
{"media_type": "cm", "media_id": "promo1", "title": "Bumper", "duration_sec": 30},
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
await db.record_play_completion(friendly_token="promo1", duration_sec=30)
|
|
365
|
+
|
|
366
|
+
rows = await db._fetch_all("SELECT media_id FROM play_completions")
|
|
367
|
+
assert rows == []
|
|
368
|
+
pip = await db._fetch_all("SELECT media_id FROM playlist_item_played")
|
|
369
|
+
assert pip == []
|
|
370
|
+
debug = await db.get_recently_played_debug(21)
|
|
371
|
+
assert debug["by_completion"] == []
|
|
372
|
+
assert debug["by_playlist_pass"] == []
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def test_purge_promo_hide_state_removes_stale_rows(db):
|
|
376
|
+
# Rows recorded before the promo exemption (e.g. in production) are cleaned up
|
|
377
|
+
# at the data layer by purge_promo_hide_state (also run by migration v14).
|
|
378
|
+
await _add_catalog(db, "promo1", "Bumper", duration_sec=30)
|
|
379
|
+
pid = await db.create_saved_playlist(
|
|
380
|
+
name="Bumpers", description=None, is_immutable=False,
|
|
381
|
+
created_by="admin", promo_type="general",
|
|
382
|
+
)
|
|
383
|
+
await db.replace_playlist_items(pid, [
|
|
384
|
+
{"media_type": "cm", "media_id": "promo1", "title": "Bumper", "duration_sec": 30},
|
|
385
|
+
])
|
|
386
|
+
# Simulate a stale row written before the exemption existed.
|
|
387
|
+
await db._execute("INSERT INTO play_completions (media_type, media_id) VALUES ('cm', 'promo1')")
|
|
388
|
+
# Before the purge it shows in the (unfiltered) debug view.
|
|
389
|
+
assert {r["media_id"] for r in (await db.get_recently_played_debug(21))["by_completion"]} == {"promo1"}
|
|
390
|
+
|
|
391
|
+
removed = await db.purge_promo_hide_state()
|
|
392
|
+
assert removed["completions"] == 1
|
|
393
|
+
|
|
394
|
+
assert await db._fetch_all("SELECT media_id FROM play_completions") == []
|
|
395
|
+
assert (await db.get_recently_played_debug(21))["by_completion"] == []
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
@@ -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.2}/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
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/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.2}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/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.2}/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.2}/kryten_webqueue/static/js/admin-playlists.js
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/static/js/admin-queue-mgmt.js
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/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.2}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/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.2}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/feedback/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.32.0 → kryten_webqueue-0.32.2}/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
|