kryten-webqueue 0.45.0__tar.gz → 0.46.0__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.45.0 → kryten_webqueue-0.46.0}/CHANGELOG.md +37 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/config.example.json +6 -3
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/config.py +13 -2
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/motd_publish.py +2 -1
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/motd/builder.py +72 -14
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/motd/render.py +1 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_motd.py +4 -1
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/motd/channel_z.html +1 -1
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/pyproject.toml +1 -1
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_motd_publish.py +110 -2
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/.gitignore +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/AGENTS.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/README.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/PUBLIC_API.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/auth/device_keys.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_catalog.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_people.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/log_capture.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/motd/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/devices.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/public_api.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/keybindings.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/admin/motd.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/user/link_device.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/user/my_list.html +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/scripts/AGENTS.md +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/scripts/patch_mediacms_tags.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/scripts/trigger_enrich.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_catalog_blackout.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_catalog_delete.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_classify.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_device_linking.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_fetch_queue_pacing.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_identify_step.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_people_facets.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_providers.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_queue_preview_fallback.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_search_facets.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_title_normalization.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_tmdb_index.py +0 -0
- {kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/tests/test_watchlist.py +0 -0
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.46.0] - 2026-09-10
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **MOTD template rebuilt from the live hand-built MOTD** (pulled from the
|
|
8
|
+
channel via api-gate) rather than the older copy in kryten-llm. Banner image,
|
|
9
|
+
headline, and both footer lines now match the real markup verbatim — including
|
|
10
|
+
the "Our Webqueue app is in open beta!" footer text that the earlier default
|
|
11
|
+
dropped. The generated snippet diffs clean against the live one apart from
|
|
12
|
+
properly closed `<a>` tags and `title`/`alt` attributes.
|
|
13
|
+
- **Grid shape follows the schedule.** A 5/7 weekend now renders 5/7 instead of
|
|
14
|
+
being forced into an even 6/6, matching how the MOTD is actually built. A
|
|
15
|
+
night is never truncated, so an over-full weekend shows every scheduled movie;
|
|
16
|
+
`motd.slots` is a target the grid is only padded *up* to when the weekend is
|
|
17
|
+
still thin.
|
|
18
|
+
- **Sunday is excluded.** New `motd.nights` (default `[1, 2]`) controls which
|
|
19
|
+
nights the grid covers; Sunday has no schedule yet, so its workbook titles are
|
|
20
|
+
parsed but ignored. Add `3` once the scheduled-playlists feature fills that
|
|
21
|
+
slot.
|
|
22
|
+
- **The next-event line stays hidden.** It is still carried in the render
|
|
23
|
+
context, but is only emitted when `motd.show_next_event` is true (default
|
|
24
|
+
false), so the published MOTD matches the hand-built one until the feature is
|
|
25
|
+
deliberately debuted.
|
|
26
|
+
|
|
27
|
+
## [0.45.1] - 2026-09-10
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **MOTD mystery boxes now use the branded placeholder art** the catalog browse
|
|
32
|
+
view already draws from (`placeholder_dir`, served under `/images`), picking a
|
|
33
|
+
different tape per slot the way browse varies it per tile. The 0.45.0 default
|
|
34
|
+
pointed at a `motd_boxes/mystery-box.jpg` that doesn't exist, so every
|
|
35
|
+
unresolved slot would have rendered a broken image. New
|
|
36
|
+
`motd.mystery_box_base_url` absolutizes those URLs for CyTube, which renders
|
|
37
|
+
the MOTD off-site; `motd.mystery_box_url` is now an empty-by-default fallback
|
|
38
|
+
used only when no branded placeholders are installed.
|
|
39
|
+
|
|
3
40
|
## [0.45.0] - 2026-09-10
|
|
4
41
|
|
|
5
42
|
### Added
|
|
@@ -92,19 +92,22 @@
|
|
|
92
92
|
"output_dir": "~/kryten",
|
|
93
93
|
"template": "channel_z.html",
|
|
94
94
|
"slots": 12,
|
|
95
|
+
"nights": [1, 2],
|
|
95
96
|
"banner_url": "https://i.postimg.cc/jdr2mR8Y/1562479186-8-channel-Ztitlenew.png",
|
|
96
97
|
"headline": "CHANNEL Z WEEKEND MOVIE PLAYLIST ({dates})",
|
|
97
98
|
"showtime": "6pm ET",
|
|
98
|
-
"
|
|
99
|
+
"mystery_box_base_url": "https://queue.dropsugar.co",
|
|
100
|
+
"mystery_box_url": "",
|
|
99
101
|
"mystery_box_href": "https://queue.dropsugar.co/",
|
|
100
102
|
"upload_max_bytes": 5242880,
|
|
101
103
|
"links": [
|
|
102
104
|
{ "label": "Join us on Reddit!", "url": "https://www.reddit.com/r/Channel_Z/" },
|
|
103
105
|
{
|
|
104
|
-
"label": "See the queue and decide what's next!",
|
|
106
|
+
"label": "See the queue and decide what's next! Our Webqueue app is in open beta!",
|
|
105
107
|
"url": "https://queue.dropsugar.co/"
|
|
106
108
|
}
|
|
107
|
-
]
|
|
109
|
+
],
|
|
110
|
+
"show_next_event": false
|
|
108
111
|
},
|
|
109
112
|
|
|
110
113
|
"prometheus_port": 28292
|
|
@@ -43,11 +43,19 @@ class MOTDConfig(BaseModel):
|
|
|
43
43
|
template: str = "channel_z.html"
|
|
44
44
|
# Grid size; unresolved positions are filled with mystery boxes.
|
|
45
45
|
slots: int = 12
|
|
46
|
+
# Nights the grid covers, in display order (1=Fri, 2=Sat, 3=Sun). Sunday has
|
|
47
|
+
# no schedule yet, so its workbook titles are ignored until it's added here.
|
|
48
|
+
nights: list[int] = Field(default_factory=lambda: [1, 2])
|
|
46
49
|
banner_url: str = "https://i.postimg.cc/jdr2mR8Y/1562479186-8-channel-Ztitlenew.png"
|
|
47
50
|
# {dates} is substituted with the weekend showtimes (e.g. "8/14 @ 6pm ET & ...").
|
|
48
51
|
headline: str = "CHANNEL Z WEEKEND MOVIE PLAYLIST ({dates})"
|
|
49
52
|
showtime: str = "6pm ET"
|
|
50
|
-
|
|
53
|
+
# Mystery boxes are drawn from the same branded placeholder pool the browse
|
|
54
|
+
# view uses (served from the webqueue /images mount); this absolutizes them
|
|
55
|
+
# for CyTube, which renders the MOTD off-site.
|
|
56
|
+
mystery_box_base_url: str = "https://queue.dropsugar.co"
|
|
57
|
+
# Single fallback used only when no branded placeholders are installed.
|
|
58
|
+
mystery_box_url: str = ""
|
|
51
59
|
mystery_box_href: str = "https://queue.dropsugar.co/"
|
|
52
60
|
links: list[MOTDLink] = Field(
|
|
53
61
|
default_factory=lambda: [
|
|
@@ -56,11 +64,14 @@ class MOTDConfig(BaseModel):
|
|
|
56
64
|
url="https://www.reddit.com/r/Channel_Z/",
|
|
57
65
|
),
|
|
58
66
|
MOTDLink(
|
|
59
|
-
label="See the queue and decide what's next!",
|
|
67
|
+
label="See the queue and decide what's next! Our Webqueue app is in open beta!",
|
|
60
68
|
url="https://queue.dropsugar.co/",
|
|
61
69
|
),
|
|
62
70
|
]
|
|
63
71
|
)
|
|
72
|
+
# The next-event line is carried in the render context but stays hidden until
|
|
73
|
+
# it's deliberately debuted; the hand-built MOTD has no such line.
|
|
74
|
+
show_next_event: bool = False
|
|
64
75
|
# Max size accepted by the admin alternate-art upload endpoint.
|
|
65
76
|
upload_max_bytes: int = 5 * 1024 * 1024
|
|
66
77
|
|
|
@@ -17,7 +17,7 @@ import datetime
|
|
|
17
17
|
import logging
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
-
from ..motd.builder import build_slots
|
|
20
|
+
from ..motd.builder import build_slots, mystery_pool
|
|
21
21
|
from ..motd.render import render_motd
|
|
22
22
|
from .manager import JobError
|
|
23
23
|
|
|
@@ -140,6 +140,7 @@ async def motd_publish_job(params: dict, ctx) -> dict:
|
|
|
140
140
|
refresh_art=refresh_art,
|
|
141
141
|
dry_run=dry_run,
|
|
142
142
|
week_offset=week_offset,
|
|
143
|
+
mystery_urls=mystery_pool(ctx.config, getattr(ctx, "cover_art", None)),
|
|
143
144
|
emit=_emit,
|
|
144
145
|
)
|
|
145
146
|
except RuntimeError as exc:
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import datetime
|
|
15
15
|
import logging
|
|
16
|
+
import random
|
|
16
17
|
import re
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from pathlib import Path
|
|
@@ -34,7 +35,9 @@ logger = logging.getLogger(__name__)
|
|
|
34
35
|
|
|
35
36
|
SLOT_KEY_RE = re.compile(r"^night[1-3]-slot([1-9]\d{0,2})$")
|
|
36
37
|
|
|
37
|
-
#
|
|
38
|
+
# Every night the workbook can describe. The grid itself covers only the nights
|
|
39
|
+
# in ``motd.nights`` — Sunday has no schedule yet, so its titles are parsed but
|
|
40
|
+
# never placed.
|
|
38
41
|
_NIGHTS = (1, 2, 3)
|
|
39
42
|
|
|
40
43
|
|
|
@@ -117,17 +120,43 @@ def _imdb_href(imdb_id: str | None) -> str:
|
|
|
117
120
|
return f"https://www.imdb.com/title/{imdb_id}/" if imdb_id else ""
|
|
118
121
|
|
|
119
122
|
|
|
123
|
+
def _grid_nights(motd_cfg) -> tuple[int, ...]:
|
|
124
|
+
"""Nights the grid reserves space for, in display order."""
|
|
125
|
+
configured = getattr(motd_cfg, "nights", None) or _NIGHTS
|
|
126
|
+
nights = tuple(int(n) for n in configured if int(n) in _NIGHTS)
|
|
127
|
+
return nights or _NIGHTS
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _slot_counts(
|
|
131
|
+
titles_by_night: dict[int, list[str]], nights: tuple[int, ...], slots: int
|
|
132
|
+
) -> dict[int, int]:
|
|
133
|
+
"""Decide how many positions each night gets.
|
|
134
|
+
|
|
135
|
+
The schedule drives the shape — a 5/7 weekend renders 5/7, not an even 6/6 —
|
|
136
|
+
and a night is never truncated, so a scheduled movie can't be hidden. Only
|
|
137
|
+
when the weekend is still thin is the grid padded up to ``slots``, handing
|
|
138
|
+
each extra to whichever night is furthest below its even share.
|
|
139
|
+
"""
|
|
140
|
+
counts = {n: len(titles_by_night.get(n, [])) for n in nights}
|
|
141
|
+
share = slots / len(nights)
|
|
142
|
+
while sum(counts.values()) < slots:
|
|
143
|
+
night = min(nights, key=lambda n: (counts[n] - share, nights.index(n)))
|
|
144
|
+
counts[night] += 1
|
|
145
|
+
return counts
|
|
146
|
+
|
|
147
|
+
|
|
120
148
|
def _blank_grid(
|
|
121
|
-
friday: datetime.date,
|
|
149
|
+
friday: datetime.date,
|
|
150
|
+
saturday: datetime.date,
|
|
151
|
+
sunday: datetime.date,
|
|
152
|
+
counts: dict[int, int],
|
|
153
|
+
nights: tuple[int, ...],
|
|
122
154
|
) -> list[MOTDSlot]:
|
|
123
|
-
"""Lay out
|
|
124
|
-
per_night = max(1, slots // len(_NIGHTS))
|
|
125
|
-
remainder = slots - per_night * len(_NIGHTS)
|
|
155
|
+
"""Lay out the per-night positions in display order."""
|
|
126
156
|
dates = {1: friday, 2: saturday, 3: sunday}
|
|
127
157
|
grid: list[MOTDSlot] = []
|
|
128
|
-
for night in
|
|
129
|
-
|
|
130
|
-
for position in range(1, count + 1):
|
|
158
|
+
for night in nights:
|
|
159
|
+
for position in range(1, counts[night] + 1):
|
|
131
160
|
grid.append(
|
|
132
161
|
MOTDSlot(
|
|
133
162
|
slot_key=f"night{night}-slot{position}",
|
|
@@ -149,9 +178,28 @@ def _titles_by_night(movies_by_section: dict[str, list[str]]) -> dict[int, list[
|
|
|
149
178
|
return by_night
|
|
150
179
|
|
|
151
180
|
|
|
152
|
-
def
|
|
181
|
+
def mystery_pool(config, cover_art) -> list[str]:
|
|
182
|
+
"""Absolute URLs for the branded placeholder art the browse view uses.
|
|
183
|
+
|
|
184
|
+
``list_placeholder_urls`` returns site-relative ``/images/...`` paths; CyTube
|
|
185
|
+
renders the MOTD off-site, so they have to be absolutized.
|
|
186
|
+
"""
|
|
187
|
+
if cover_art is None:
|
|
188
|
+
return []
|
|
189
|
+
base = str(getattr(config.motd, "mystery_box_base_url", "") or "").rstrip("/")
|
|
190
|
+
if not base:
|
|
191
|
+
return []
|
|
192
|
+
return [base + url for url in cover_art.list_placeholder_urls()]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _apply_mystery(slot: MOTDSlot, motd_cfg, pool: list[str]) -> None:
|
|
153
196
|
slot.source = "mystery"
|
|
154
|
-
|
|
197
|
+
# Vary the art across the grid the way browse varies it across tiles.
|
|
198
|
+
slot.poster_url = (
|
|
199
|
+
random.choice(pool)
|
|
200
|
+
if pool
|
|
201
|
+
else (getattr(motd_cfg, "mystery_box_url", "") or "")
|
|
202
|
+
)
|
|
155
203
|
slot.href = getattr(motd_cfg, "mystery_box_href", "") or ""
|
|
156
204
|
|
|
157
205
|
|
|
@@ -164,6 +212,7 @@ def build_slots(
|
|
|
164
212
|
dry_run: bool = False,
|
|
165
213
|
today: datetime.date | None = None,
|
|
166
214
|
week_offset: int = 0,
|
|
215
|
+
mystery_urls: list[str] | None = None,
|
|
167
216
|
emit=None,
|
|
168
217
|
) -> MOTDWeek:
|
|
169
218
|
"""Resolve the upcoming weekend into a complete grid of poster slots.
|
|
@@ -178,6 +227,7 @@ def build_slots(
|
|
|
178
227
|
emit(detail)
|
|
179
228
|
|
|
180
229
|
overrides = overrides or {}
|
|
230
|
+
pool = mystery_urls or []
|
|
181
231
|
motd_cfg = getattr(config, "motd", None)
|
|
182
232
|
slot_count = int(getattr(motd_cfg, "slots", 12) or 12)
|
|
183
233
|
poster_dir = Path(
|
|
@@ -190,7 +240,6 @@ def build_slots(
|
|
|
190
240
|
week_key, friday, saturday = upcoming_weekend_sheet(base_day)
|
|
191
241
|
sunday = friday + datetime.timedelta(days=2)
|
|
192
242
|
week = MOTDWeek(week_key=week_key, friday=friday, saturday=saturday, sunday=sunday)
|
|
193
|
-
week.slots = _blank_grid(friday, saturday, sunday, slot_count)
|
|
194
243
|
|
|
195
244
|
# The workbook is best-effort: a missing sheet yields an all-mystery grid
|
|
196
245
|
# rather than a failed run, so the MOTD still publishes on schedule.
|
|
@@ -208,6 +257,15 @@ def build_slots(
|
|
|
208
257
|
week.warnings.append(f"workbook unavailable: {exc}")
|
|
209
258
|
logger.warning("motd: workbook unavailable for %s: %s", week_key, exc)
|
|
210
259
|
|
|
260
|
+
nights = _grid_nights(motd_cfg)
|
|
261
|
+
week.slots = _blank_grid(
|
|
262
|
+
friday,
|
|
263
|
+
saturday,
|
|
264
|
+
sunday,
|
|
265
|
+
_slot_counts(titles_by_night, nights, slot_count),
|
|
266
|
+
nights,
|
|
267
|
+
)
|
|
268
|
+
|
|
211
269
|
if not dry_run:
|
|
212
270
|
poster_dir.mkdir(parents=True, exist_ok=True)
|
|
213
271
|
|
|
@@ -234,13 +292,13 @@ def build_slots(
|
|
|
234
292
|
continue
|
|
235
293
|
|
|
236
294
|
if not slot.title:
|
|
237
|
-
_apply_mystery(slot, motd_cfg)
|
|
295
|
+
_apply_mystery(slot, motd_cfg, pool)
|
|
238
296
|
slot.note = "no title on the schedule yet"
|
|
239
297
|
continue
|
|
240
298
|
|
|
241
299
|
info = _resolve_omdb(slot.title, api_key=omdb_key) if omdb_key else None
|
|
242
300
|
if not info:
|
|
243
|
-
_apply_mystery(slot, motd_cfg)
|
|
301
|
+
_apply_mystery(slot, motd_cfg, pool)
|
|
244
302
|
slot.note = "title could not be verified"
|
|
245
303
|
logger.info("motd: no OMDB match for %r (%s)", slot.title, slot.slot_key)
|
|
246
304
|
if override.get("href"):
|
|
@@ -266,7 +324,7 @@ def build_slots(
|
|
|
266
324
|
slot.poster_url = f"{poster_base_url}/{filename}"
|
|
267
325
|
slot.source = "omdb"
|
|
268
326
|
else:
|
|
269
|
-
_apply_mystery(slot, motd_cfg)
|
|
327
|
+
_apply_mystery(slot, motd_cfg, pool)
|
|
270
328
|
slot.note = "poster download failed"
|
|
271
329
|
|
|
272
330
|
slot.href = override.get("href") or _imdb_href(slot.imdb_id)
|
|
@@ -73,6 +73,7 @@ def render_motd(config, week: MOTDWeek, *, next_event: dict | None = None) -> st
|
|
|
73
73
|
for link in getattr(motd_cfg, "links", [])
|
|
74
74
|
],
|
|
75
75
|
next_event=next_event,
|
|
76
|
+
show_next_event=bool(getattr(motd_cfg, "show_next_event", False)),
|
|
76
77
|
week_key=week.week_key,
|
|
77
78
|
generated_at=datetime.datetime.now(datetime.timezone.utc).isoformat(
|
|
78
79
|
timespec="seconds"
|
|
@@ -18,7 +18,7 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, Uplo
|
|
|
18
18
|
from pydantic import BaseModel
|
|
19
19
|
|
|
20
20
|
from ..auth.session import require_admin
|
|
21
|
-
from ..motd.builder import SLOT_KEY_RE, build_slots, week_context
|
|
21
|
+
from ..motd.builder import SLOT_KEY_RE, build_slots, mystery_pool, week_context
|
|
22
22
|
from ..motd.render import render_motd
|
|
23
23
|
|
|
24
24
|
router = APIRouter(prefix="/admin/motd", tags=["admin"])
|
|
@@ -72,6 +72,9 @@ async def _build_current(request: Request, week: str = "current"):
|
|
|
72
72
|
overrides=overrides,
|
|
73
73
|
dry_run=True,
|
|
74
74
|
week_offset=_week_offset(week),
|
|
75
|
+
mystery_urls=mystery_pool(
|
|
76
|
+
config, getattr(request.app.state, "cover_art", None)
|
|
77
|
+
),
|
|
75
78
|
)
|
|
76
79
|
|
|
77
80
|
|
{kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/templates/motd/channel_z.html
RENAMED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{% endfor %}
|
|
23
23
|
{% endfor %}
|
|
24
24
|
</div>
|
|
25
|
-
{% if next_event %}
|
|
25
|
+
{% if show_next_event and next_event %}
|
|
26
26
|
<div class="weekend-title">🟥🟧🟨🟩Up next: {{ next_event.label }}{% if next_event.starts_in %} — {{ next_event.starts_in }}{% endif %}🟩🟨🟧🟥</div>
|
|
27
27
|
{% endif %}
|
|
28
28
|
{% for link in links %}
|
|
@@ -30,6 +30,7 @@ def _config(tmp_path, **motd_kw):
|
|
|
30
30
|
poster_dir=str(tmp_path / "boxes"),
|
|
31
31
|
poster_base_url="https://cdn.example/boxes",
|
|
32
32
|
output_dir=str(tmp_path / "out"),
|
|
33
|
+
mystery_box_base_url="https://queue.example",
|
|
33
34
|
mystery_box_url="https://cdn.example/boxes/mystery.jpg",
|
|
34
35
|
mystery_box_href="https://queue.example/",
|
|
35
36
|
**motd_kw,
|
|
@@ -71,12 +72,70 @@ def _stub_workbook(monkeypatch, titles_by_night):
|
|
|
71
72
|
# --- grid layout ---
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
def
|
|
75
|
+
def test_grid_pads_a_thin_weekend_to_the_target_size(
|
|
76
|
+
tmp_path, monkeypatch, stub_lookup
|
|
77
|
+
):
|
|
75
78
|
_stub_workbook(monkeypatch, {})
|
|
76
79
|
week = builder.build_slots(_config(tmp_path), today=datetime.date(2026, 3, 4))
|
|
77
80
|
assert len(week.slots) == 12
|
|
78
81
|
assert [s.slot_key for s in week.slots[:2]] == ["night1-slot1", "night1-slot2"]
|
|
82
|
+
# Sunday has no schedule yet, so the grid is Friday + Saturday.
|
|
83
|
+
assert {s.night for s in week.slots} == {1, 2}
|
|
84
|
+
assert len([s for s in week.slots if s.night == 1]) == 6
|
|
85
|
+
assert len([s for s in week.slots if s.night == 2]) == 6
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_grid_shape_follows_the_schedule(tmp_path, monkeypatch, stub_lookup):
|
|
89
|
+
# A real 5/7 weekend renders 5/7, not an even 6/6.
|
|
90
|
+
_stub_workbook(
|
|
91
|
+
monkeypatch,
|
|
92
|
+
{
|
|
93
|
+
1: [f"Friday {n} (1980)" for n in range(5)],
|
|
94
|
+
2: [f"Saturday {n} (1980)" for n in range(7)],
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
week = builder.build_slots(_config(tmp_path), today=datetime.date(2026, 3, 4))
|
|
98
|
+
assert len([s for s in week.slots if s.night == 1]) == 5
|
|
99
|
+
assert len([s for s in week.slots if s.night == 2]) == 7
|
|
100
|
+
assert all(s.resolved for s in week.slots)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_grid_never_truncates_an_overfull_weekend(tmp_path, monkeypatch, stub_lookup):
|
|
104
|
+
_stub_workbook(
|
|
105
|
+
monkeypatch,
|
|
106
|
+
{
|
|
107
|
+
1: [f"Friday {n} (1980)" for n in range(8)],
|
|
108
|
+
2: [f"Saturday {n} (1980)" for n in range(7)],
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
week = builder.build_slots(_config(tmp_path), today=datetime.date(2026, 3, 4))
|
|
112
|
+
assert len(week.slots) == 15
|
|
113
|
+
assert "Friday 7 (1980)" in [s.title for s in week.slots]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_sunday_titles_are_ignored(tmp_path, monkeypatch, stub_lookup):
|
|
117
|
+
_stub_workbook(
|
|
118
|
+
monkeypatch,
|
|
119
|
+
{
|
|
120
|
+
1: ["Friday One (1976)"],
|
|
121
|
+
2: ["Saturday One (1980)"],
|
|
122
|
+
3: ["Sunday One (1990)"],
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
week = builder.build_slots(_config(tmp_path), today=datetime.date(2026, 3, 4))
|
|
126
|
+
titles = [s.title for s in week.slots if s.title]
|
|
127
|
+
assert "Friday One (1976)" in titles
|
|
128
|
+
assert "Saturday One (1980)" in titles
|
|
129
|
+
assert "Sunday One (1990)" not in titles
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_nights_config_can_re_enable_sunday(tmp_path, monkeypatch, stub_lookup):
|
|
133
|
+
_stub_workbook(monkeypatch, {3: ["Sunday One (1990)"]})
|
|
134
|
+
week = builder.build_slots(
|
|
135
|
+
_config(tmp_path, nights=[1, 2, 3]), today=datetime.date(2026, 3, 4)
|
|
136
|
+
)
|
|
79
137
|
assert {s.night for s in week.slots} == {1, 2, 3}
|
|
138
|
+
assert "Sunday One (1990)" in [s.title for s in week.slots if s.title]
|
|
80
139
|
|
|
81
140
|
|
|
82
141
|
def test_missing_workbook_yields_all_mystery(tmp_path, monkeypatch, stub_lookup):
|
|
@@ -126,6 +185,43 @@ def test_unverifiable_title_falls_back_to_mystery(tmp_path, monkeypatch, stub_lo
|
|
|
126
185
|
assert "could not be verified" in slot.note
|
|
127
186
|
|
|
128
187
|
|
|
188
|
+
# --- mystery art comes from the browse view's branded placeholder pool ---
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_mystery_pool_absolutizes_browse_placeholders(tmp_path):
|
|
192
|
+
config = _config(tmp_path)
|
|
193
|
+
cover_art = SimpleNamespace(
|
|
194
|
+
list_placeholder_urls=lambda: ["/images/placeholders/a.webp"]
|
|
195
|
+
)
|
|
196
|
+
assert builder.mystery_pool(config, cover_art) == [
|
|
197
|
+
"https://queue.example/images/placeholders/a.webp"
|
|
198
|
+
]
|
|
199
|
+
assert builder.mystery_pool(config, None) == []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_mystery_slots_use_the_placeholder_pool(tmp_path, monkeypatch, stub_lookup):
|
|
203
|
+
_stub_workbook(monkeypatch, {})
|
|
204
|
+
week = builder.build_slots(
|
|
205
|
+
_config(tmp_path),
|
|
206
|
+
today=datetime.date(2026, 3, 4),
|
|
207
|
+
mystery_urls=["https://queue.example/images/placeholders/a.webp"],
|
|
208
|
+
)
|
|
209
|
+
assert all(
|
|
210
|
+
s.poster_url == "https://queue.example/images/placeholders/a.webp"
|
|
211
|
+
for s in week.slots
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_mystery_falls_back_when_no_placeholders_installed(
|
|
216
|
+
tmp_path, monkeypatch, stub_lookup
|
|
217
|
+
):
|
|
218
|
+
_stub_workbook(monkeypatch, {})
|
|
219
|
+
week = builder.build_slots(
|
|
220
|
+
_config(tmp_path), today=datetime.date(2026, 3, 4), mystery_urls=[]
|
|
221
|
+
)
|
|
222
|
+
assert all(s.poster_url.endswith("mystery.jpg") for s in week.slots)
|
|
223
|
+
|
|
224
|
+
|
|
129
225
|
def test_dry_run_writes_nothing(tmp_path, monkeypatch, stub_lookup):
|
|
130
226
|
_stub_workbook(monkeypatch, {1: ["The Big Bus (1976)"]})
|
|
131
227
|
builder.build_slots(
|
|
@@ -203,13 +299,25 @@ def test_render_groups_by_night_and_includes_links(tmp_path, monkeypatch, stub_l
|
|
|
203
299
|
assert "3/6 @ 6pm ET & 3/7 @ 6pm ET" in html
|
|
204
300
|
|
|
205
301
|
|
|
206
|
-
def
|
|
302
|
+
def test_next_event_is_carried_but_hidden_by_default(
|
|
303
|
+
tmp_path, monkeypatch, stub_lookup
|
|
304
|
+
):
|
|
207
305
|
_stub_workbook(monkeypatch, {})
|
|
208
306
|
config = _config(tmp_path)
|
|
209
307
|
week = builder.build_slots(config, today=datetime.date(2026, 3, 4))
|
|
210
308
|
html = render.render_motd(
|
|
211
309
|
config, week, next_event={"label": "Saturday Night", "starts_in": "in 2h"}
|
|
212
310
|
)
|
|
311
|
+
assert "Up next" not in html
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_render_includes_next_event_when_enabled(tmp_path, monkeypatch, stub_lookup):
|
|
315
|
+
_stub_workbook(monkeypatch, {})
|
|
316
|
+
config = _config(tmp_path, show_next_event=True)
|
|
317
|
+
week = builder.build_slots(config, today=datetime.date(2026, 3, 4))
|
|
318
|
+
html = render.render_motd(
|
|
319
|
+
config, week, next_event={"label": "Saturday Night", "starts_in": "in 2h"}
|
|
320
|
+
)
|
|
213
321
|
assert "Up next: Saturday Night — in 2h" in html
|
|
214
322
|
|
|
215
323
|
|
|
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.45.0 → kryten_webqueue-0.46.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/db/_fetch_queue.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/classify.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.45.0 → kryten_webqueue-0.46.0}/kryten_webqueue/catalog/enrichment/normalise.py
RENAMED
|
File without changes
|