kryten-webqueue 0.48.0__tar.gz → 0.48.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.48.0 → kryten_webqueue-0.48.1}/CHANGELOG.md +21 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/PKG-INFO +1 -1
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/deploy/nginx-queue.conf +6 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/DATABASE_ARCHITECTURE_PLAN.md +23 -7
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-cutover.md +61 -3
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/PRD-postgres-migration.md +3 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +9 -2
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/__init__.py +47 -4
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_catalog.py +17 -6
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_catalog_db.py +24 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/pyproject.toml +1 -1
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_multi_db_connection.py +62 -0
- kryten_webqueue-0.48.1/tests/test_postgres_live.py +87 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/.gitignore +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/AGENTS.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/Containerfile +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/README.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/config.example.json +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/deploy/podman/webqueue/webqueue-app.container +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/deploy/podman/webqueue/webqueue.network +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/PUBLIC_API.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/SPEC_EMOTE_REHOST.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/device_keys.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_base_domain.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_devices.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_jobs_db.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_motd.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_people.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_queue_db.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_users_db.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/engine.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/catalog_schema.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/jobs_schema.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/queue_schema.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/users_schema.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/sql/001_initial_schema.sql +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/report.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/job_scheduler.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/log_capture.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/motd_publish.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/migrate_sqlite_to_pg.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/motd/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/motd/builder.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/motd/render.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_motd.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/devices.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/public_api.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/scripts/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/scripts/split_databases.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-motd.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/keybindings.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/motd.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/motd/channel_z.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/user/link_device.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/user/my_list.html +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/scripts/AGENTS.md +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/scripts/patch_mediacms_bulk_facets.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/scripts/patch_mediacms_tags.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/scripts/split_databases.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/scripts/trigger_enrich.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/__init__.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_catalog_blackout.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_catalog_delete.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_classify.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_concurrency_split.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_database_config_and_schemas.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_device_linking.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_fetch_queue_pacing.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_identify_step.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_job_logs_and_queue_visibility.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_motd_publish.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_people_facets.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_postgres_config_and_engine.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_postgres_pruner.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_providers.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_queue_preview_fallback.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_search_facets.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_title_normalization.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_tmdb_index.py +0 -0
- {kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/tests/test_watchlist.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.48.1] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Production 500 on `/catalog/browse` in partitioned SQLite mode**: `get_tags()` (facet
|
|
8
|
+
counts) unconditionally ran the monolith reserved-item exclusion subquery referencing
|
|
9
|
+
`saved_playlist_items`/`saved_playlists`, tables that live in `queue.sqlite3`, not
|
|
10
|
+
`catalog.sqlite3`. Every browse/search page load crashed with
|
|
11
|
+
`sqlite3.OperationalError: no such table: saved_playlist_items` once the site was
|
|
12
|
+
cut over to `layout="partitioned"`. `get_tags` now accepts `exclude_tokens`/
|
|
13
|
+
`is_partitioned`, and the `Database` facade resolves reserved/blackout exclusions via
|
|
14
|
+
the `queue` domain before calling it, matching the existing `browse`/`search` pattern.
|
|
15
|
+
- **Reserved-item exclusion leak in partitioned mode**: `queue.get_reserved_media_ids()`
|
|
16
|
+
returns raw `media_id` values, which are usually CyTube manifest URLs, not bare
|
|
17
|
+
`friendly_token`s. `browse`, `browse_count`, `search`, and `search_count` were comparing
|
|
18
|
+
these raw values directly against `catalog.friendly_token`, so immutable/promo-pool
|
|
19
|
+
items whose playlist entry stored a manifest URL were **not excluded** from public
|
|
20
|
+
results. Added `catalog.resolve_friendly_tokens()` to resolve either shape back to
|
|
21
|
+
`friendly_token`, and all four call sites now use it via a shared
|
|
22
|
+
`Database._get_reserved_tokens()` helper.
|
|
23
|
+
|
|
3
24
|
## [0.48.0] - 2026-09-23
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -13,8 +13,13 @@ server {
|
|
|
13
13
|
|
|
14
14
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
|
15
15
|
|
|
16
|
+
# Upstream application backend:
|
|
17
|
+
# - Local grindhouse service: http://127.0.0.1:2010
|
|
18
|
+
# - Cross-network Chandra-1 Podman container: http://chandra-1.local:2010
|
|
19
|
+
|
|
16
20
|
# WebSocket upgrade
|
|
17
21
|
location /ws {
|
|
22
|
+
# Use http://chandra-1.local:2010 when deployed to Chandra-1 Podman pod
|
|
18
23
|
proxy_pass http://127.0.0.1:2010;
|
|
19
24
|
proxy_http_version 1.1;
|
|
20
25
|
proxy_set_header Upgrade $http_upgrade;
|
|
@@ -28,6 +33,7 @@ server {
|
|
|
28
33
|
|
|
29
34
|
# API + pages
|
|
30
35
|
location / {
|
|
36
|
+
# Use http://chandra-1.local:2010 when deployed to Chandra-1 Podman pod
|
|
31
37
|
proxy_pass http://127.0.0.1:2010;
|
|
32
38
|
proxy_http_version 1.1;
|
|
33
39
|
proxy_set_header Connection "";
|
|
@@ -133,18 +133,29 @@ Upon completion of Part 1, the partitioned domain model directly maps to Postgre
|
|
|
133
133
|
- `tmdb.*` — Persistent, queryable local TMDB dump index.
|
|
134
134
|
|
|
135
135
|
```
|
|
136
|
+
┌─────────────────────────────────────────┐
|
|
137
|
+
│ grindhouse.local Host │
|
|
138
|
+
│ │
|
|
139
|
+
│ ┌─────────────────────────────────┐ │
|
|
140
|
+
│ │ nginx proxy │ │
|
|
141
|
+
│ │ queue.dropsugar.co (SSL) │ │
|
|
142
|
+
│ └────────────────┬────────────────┘ │
|
|
143
|
+
└────────────────────┼────────────────────┘
|
|
144
|
+
│ LAN Reverse Proxy
|
|
145
|
+
│ http://chandra-1.local:2010
|
|
146
|
+
▼
|
|
136
147
|
┌────────────────────────────────────────────────────────────────────────────────────────┐
|
|
137
148
|
│ chandra-1 Host System │
|
|
138
149
|
│ │
|
|
139
150
|
│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
|
|
140
|
-
│ │
|
|
151
|
+
│ │ Podman Container: webqueue-app │ │
|
|
141
152
|
│ │ │ │
|
|
142
|
-
│ │ ┌─────────────────────────────┐
|
|
143
|
-
│ │ │ kryten-webqueue-app │
|
|
144
|
-
│ │ │ (FastAPI + uvicorn)
|
|
145
|
-
│ │ │ SQLAlchemy 2.0 (asyncpg) │
|
|
146
|
-
│ │ └──────────────┬──────────────┘
|
|
147
|
-
│
|
|
153
|
+
│ │ ┌─────────────────────────────┐ │ │
|
|
154
|
+
│ │ │ kryten-webqueue-app │ │ │
|
|
155
|
+
│ │ │ (FastAPI + uvicorn:2010) │ │ │
|
|
156
|
+
│ │ │ SQLAlchemy 2.0 (asyncpg) │ │ │
|
|
157
|
+
│ │ └──────────────┬──────────────┘ │ │
|
|
158
|
+
│ └─────────────────┼───────────────────────────────────────────┬────────────────────┘ │
|
|
148
159
|
│ │ │ │
|
|
149
160
|
│ ▼ ▼ │
|
|
150
161
|
│ ┌─────────────────────────────────────┐ ┌──────────────────────────────────────┐ │
|
|
@@ -156,6 +167,11 @@ Upon completion of Part 1, the partitioned domain model directly maps to Postgre
|
|
|
156
167
|
└────────────────────────────────────────────────────────────────────────────────────────┘
|
|
157
168
|
```
|
|
158
169
|
|
|
170
|
+
The public hostname `https://queue.dropsugar.co/` terminates TLS via Let's Encrypt Nginx on
|
|
171
|
+
`grindhouse.local`. During the Chandra-1 cutover, the Nginx reverse proxy configuration on
|
|
172
|
+
`grindhouse.local` (`/etc/nginx/sites-available/queue.conf`) is updated to forward HTTP and
|
|
173
|
+
WebSocket traffic across the local network to `http://chandra-1.local:2010`.
|
|
174
|
+
|
|
159
175
|
### 2.2 Core Technical Specifications
|
|
160
176
|
|
|
161
177
|
1. **ORM & Driver**: **SQLAlchemy 2.0 with `asyncpg`** (`create_async_engine`, `async_sessionmaker`, `AsyncSession`).
|
|
@@ -80,15 +80,73 @@ sudo systemctl start webqueue-app.service
|
|
|
80
80
|
sudo systemctl status webqueue-app.service --no-pager
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Verify local health inside `chandra-1`:
|
|
84
84
|
```bash
|
|
85
85
|
curl -I http://127.0.0.1:2010/auth/login
|
|
86
86
|
curl -s http://127.0.0.1:2010/queue/next-schedule
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Step 6: Update Grindhouse Nginx Reverse Proxy Across Network
|
|
90
|
+
The public website `https://queue.dropsugar.co/` terminates SSL via Let's Encrypt Nginx on `grindhouse.local`. Update its upstream to proxy across the LAN to `chandra-1.local:2010`.
|
|
91
|
+
|
|
92
|
+
On `grindhouse.local`:
|
|
93
|
+
1. Edit `/etc/nginx/sites-available/queue.conf`:
|
|
94
|
+
```nginx
|
|
95
|
+
# WebSocket upgrade
|
|
96
|
+
location /ws {
|
|
97
|
+
proxy_pass http://chandra-1.local:2010;
|
|
98
|
+
proxy_http_version 1.1;
|
|
99
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
100
|
+
proxy_set_header Connection "upgrade";
|
|
101
|
+
proxy_set_header Host $host;
|
|
102
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
103
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
104
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
105
|
+
proxy_read_timeout 86400;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# API + pages
|
|
109
|
+
location / {
|
|
110
|
+
proxy_pass http://chandra-1.local:2010;
|
|
111
|
+
proxy_http_version 1.1;
|
|
112
|
+
proxy_set_header Connection "";
|
|
113
|
+
proxy_set_header Host $host;
|
|
114
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
115
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
116
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
2. Test and reload Nginx:
|
|
121
|
+
```bash
|
|
122
|
+
sudo nginx -t
|
|
123
|
+
sudo systemctl reload nginx
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Step 7: Public Smoke Testing & Verification
|
|
127
|
+
Test public endpoints through the reverse proxy:
|
|
128
|
+
```bash
|
|
129
|
+
curl -I https://queue.dropsugar.co/auth/login
|
|
130
|
+
curl -s https://queue.dropsugar.co/queue/next-schedule
|
|
131
|
+
```
|
|
132
|
+
Monitor logs on `chandra-1`:
|
|
133
|
+
```bash
|
|
134
|
+
sudo journalctl -u webqueue-app.service -f
|
|
135
|
+
```
|
|
136
|
+
|
|
89
137
|
---
|
|
90
138
|
|
|
91
139
|
## 3. Rollback & Forward Repair Policy
|
|
92
140
|
|
|
93
|
-
- **Pre-Cutover Failure**: If ETL validation fails
|
|
94
|
-
|
|
141
|
+
- **Pre-Cutover Failure**: If ETL validation fails, container startup fails on `chandra-1`, or initial smoke tests fail before Nginx is updated:
|
|
142
|
+
1. Do not update or revert Nginx on `grindhouse.local` (`proxy_pass http://127.0.0.1:2010;`).
|
|
143
|
+
2. Restart `kryten-webqueue.service` on `grindhouse.local`:
|
|
144
|
+
```bash
|
|
145
|
+
sudo systemctl start kryten-webqueue.service
|
|
146
|
+
```
|
|
147
|
+
3. The SQLite databases remain untouched and consistent.
|
|
148
|
+
- **Traffic Rollback (if Nginx was already switched but app has issues)**:
|
|
149
|
+
1. Revert `/etc/nginx/sites-available/queue.conf` on `grindhouse.local` to point back to `http://127.0.0.1:2010`.
|
|
150
|
+
2. Reload Nginx: `sudo nginx -t && sudo systemctl reload nginx`.
|
|
151
|
+
3. Start `kryten-webqueue.service` on `grindhouse.local`.
|
|
152
|
+
- **Post-Cutover Forward Repair**: Once production traffic is accepted by PostgreSQL (user logins, new spend requests, play completions), recovery is a **forward repair in PostgreSQL**. The retained SQLite snapshot is preserved read-only for forensics.
|
{kryten_webqueue-0.48.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/PRD-postgres-migration.md
RENAMED
|
@@ -28,6 +28,9 @@ The following decisions are fixed for this sprint:
|
|
|
28
28
|
- One PostgreSQL database named `webqueue` owns all five schemas. `tmdb` is a schema, not a
|
|
29
29
|
separate database, so catalog enrichment can use relational queries when needed.
|
|
30
30
|
- The application runs as a rootful Podman Quadlet service on `chandra-1`.
|
|
31
|
+
- Public traffic on `https://queue.dropsugar.co/` terminates TLS at Nginx on `grindhouse.local`.
|
|
32
|
+
During cutover, Nginx on `grindhouse.local` (`/etc/nginx/sites-available/queue.conf`) is updated
|
|
33
|
+
to reverse-proxy HTTP and WebSocket (`/ws`) traffic across the local network to `http://chandra-1.local:2010`.
|
|
31
34
|
- PostgreSQL is host-managed on `chandra-1`. The rootful application container connects through
|
|
32
35
|
an explicitly configured host-gateway address; it must not use `localhost`.
|
|
33
36
|
- The migration uses a maintenance window and a forward-fix rollback policy. There is no
|
|
@@ -66,12 +66,19 @@ python -m kryten_webqueue.migrate_sqlite_to_pg --data-dir /path/to/sqlite/data -
|
|
|
66
66
|
# 5. Grant runtime/pruner roles only after verification, then start the rootful Quadlet.
|
|
67
67
|
sudo systemctl start webqueue-app.service
|
|
68
68
|
|
|
69
|
-
# 6.
|
|
69
|
+
# 6. Update Nginx reverse proxy on grindhouse.local to forward across the LAN:
|
|
70
|
+
# In /etc/nginx/sites-available/queue.conf:
|
|
71
|
+
# proxy_pass http://chandra-1.local:2010; (for /ws and /)
|
|
72
|
+
# sudo nginx -t && sudo systemctl reload nginx
|
|
73
|
+
|
|
74
|
+
# 7. Verify health and smoke test public HTTPS and WebSocket paths:
|
|
75
|
+
curl -f https://queue.dropsugar.co/auth/login
|
|
70
76
|
curl -f http://127.0.0.1:2010/api/public/v1/state
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
If a gate fails before PostgreSQL receives production writes, restart the unchanged SQLite
|
|
74
|
-
service
|
|
80
|
+
service and revert the Nginx proxy_pass on grindhouse.local back to http://127.0.0.1:2010.
|
|
81
|
+
Once PostgreSQL accepts a write, recovery is a forward repair using request identifiers
|
|
75
82
|
and audit records; the retained SQLite snapshot is not an automatic rollback target.
|
|
76
83
|
|
|
77
84
|
## 4. Implementation Plan
|
|
@@ -182,6 +182,7 @@ _DOMAIN_METHOD_MAP: dict[str, str] = {
|
|
|
182
182
|
"get_item_edit_history": "catalog",
|
|
183
183
|
"get_items_by_tokens": "catalog",
|
|
184
184
|
"get_hidden_category_and_tag_tokens": "catalog",
|
|
185
|
+
"resolve_friendly_tokens": "catalog",
|
|
185
186
|
"get_people": "catalog",
|
|
186
187
|
"get_studios": "catalog",
|
|
187
188
|
"upsert_person": "catalog",
|
|
@@ -333,6 +334,16 @@ class Database(
|
|
|
333
334
|
|
|
334
335
|
# --- Decoupled Cross-Domain Orchestrations ---
|
|
335
336
|
|
|
337
|
+
async def _get_reserved_tokens(self) -> set[str]:
|
|
338
|
+
"""Reserved (immutable/promo) media_ids resolved to friendly_tokens.
|
|
339
|
+
|
|
340
|
+
queue.db stores media_id as either a bare token or a manifest URL, so
|
|
341
|
+
the raw values must be resolved through catalog.db before comparing
|
|
342
|
+
against friendly_token.
|
|
343
|
+
"""
|
|
344
|
+
raw = await self.queue.get_reserved_media_ids()
|
|
345
|
+
return await self.catalog.resolve_friendly_tokens(raw)
|
|
346
|
+
|
|
336
347
|
async def browse(
|
|
337
348
|
self,
|
|
338
349
|
*,
|
|
@@ -357,7 +368,7 @@ class Database(
|
|
|
357
368
|
recently_played_days
|
|
358
369
|
)
|
|
359
370
|
exclude_set.update(hidden)
|
|
360
|
-
reserved = await self.
|
|
371
|
+
reserved = await self._get_reserved_tokens()
|
|
361
372
|
exclude_set.update(reserved)
|
|
362
373
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
363
374
|
if not show_hidden:
|
|
@@ -425,7 +436,7 @@ class Database(
|
|
|
425
436
|
recently_played_days
|
|
426
437
|
)
|
|
427
438
|
exclude_set.update(hidden)
|
|
428
|
-
reserved = await self.
|
|
439
|
+
reserved = await self._get_reserved_tokens()
|
|
429
440
|
exclude_set.update(reserved)
|
|
430
441
|
if not show_hidden:
|
|
431
442
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
@@ -483,7 +494,7 @@ class Database(
|
|
|
483
494
|
recently_played_days
|
|
484
495
|
)
|
|
485
496
|
exclude_set.update(hidden)
|
|
486
|
-
reserved = await self.
|
|
497
|
+
reserved = await self._get_reserved_tokens()
|
|
487
498
|
exclude_set.update(reserved)
|
|
488
499
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
489
500
|
if not show_hidden:
|
|
@@ -554,7 +565,7 @@ class Database(
|
|
|
554
565
|
recently_played_days
|
|
555
566
|
)
|
|
556
567
|
exclude_set.update(hidden)
|
|
557
|
-
reserved = await self.
|
|
568
|
+
reserved = await self._get_reserved_tokens()
|
|
558
569
|
exclude_set.update(reserved)
|
|
559
570
|
if not show_hidden:
|
|
560
571
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
@@ -612,6 +623,38 @@ class Database(
|
|
|
612
623
|
self, username, page=page, per_page=per_page
|
|
613
624
|
)
|
|
614
625
|
|
|
626
|
+
async def get_tags(
|
|
627
|
+
self,
|
|
628
|
+
*,
|
|
629
|
+
limit: int = 100,
|
|
630
|
+
show_hidden: bool = False,
|
|
631
|
+
min_duration_sec: int = 0,
|
|
632
|
+
max_duration_sec: int | None = None,
|
|
633
|
+
**kwargs: Any,
|
|
634
|
+
) -> list[dict]:
|
|
635
|
+
if self._layout == "partitioned":
|
|
636
|
+
exclude_set: set[str] = await self._get_reserved_tokens()
|
|
637
|
+
if not show_hidden:
|
|
638
|
+
blackouts = await self.queue.get_active_blackout_tokens()
|
|
639
|
+
exclude_set.update(blackouts)
|
|
640
|
+
return await self.catalog.get_tags(
|
|
641
|
+
limit=limit,
|
|
642
|
+
show_hidden=show_hidden,
|
|
643
|
+
min_duration_sec=min_duration_sec,
|
|
644
|
+
max_duration_sec=max_duration_sec,
|
|
645
|
+
exclude_tokens=exclude_set,
|
|
646
|
+
is_partitioned=True,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
return await _CatalogMixin.get_tags(
|
|
650
|
+
self,
|
|
651
|
+
limit=limit,
|
|
652
|
+
show_hidden=show_hidden,
|
|
653
|
+
min_duration_sec=min_duration_sec,
|
|
654
|
+
max_duration_sec=max_duration_sec,
|
|
655
|
+
is_partitioned=False,
|
|
656
|
+
)
|
|
657
|
+
|
|
615
658
|
async def get_item(self, friendly_token: str) -> dict | None:
|
|
616
659
|
if self._layout == "partitioned":
|
|
617
660
|
item = await self.catalog.get_item(friendly_token, is_partitioned=True)
|
|
@@ -916,6 +916,8 @@ class _CatalogMixin:
|
|
|
916
916
|
show_hidden: bool = False,
|
|
917
917
|
min_duration_sec: int = 0,
|
|
918
918
|
max_duration_sec: int | None = None,
|
|
919
|
+
exclude_tokens: set[str] | list[str] | None = None,
|
|
920
|
+
is_partitioned: bool = False,
|
|
919
921
|
) -> list[dict]:
|
|
920
922
|
"""Most-used tags that have at least one catalog item, for facets.
|
|
921
923
|
|
|
@@ -929,16 +931,25 @@ class _CatalogMixin:
|
|
|
929
931
|
WHERE 1=1
|
|
930
932
|
"""
|
|
931
933
|
params: list = []
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
934
|
+
if not is_partitioned:
|
|
935
|
+
resv_sql, resv_params = _reserved_exclusion("c")
|
|
936
|
+
sql += resv_sql
|
|
937
|
+
params.extend(resv_params)
|
|
938
|
+
if not show_hidden:
|
|
939
|
+
bo_sql, bo_params = _blackout_exclusion("c")
|
|
940
|
+
sql += bo_sql
|
|
941
|
+
params.extend(bo_params)
|
|
935
942
|
if not show_hidden:
|
|
936
|
-
bo_sql, bo_params = _blackout_exclusion("c")
|
|
937
|
-
sql += bo_sql
|
|
938
|
-
params.extend(bo_params)
|
|
939
943
|
ph = ",".join("?" * len(HIDDEN_TAG_NAMES))
|
|
940
944
|
sql += f" AND t.name NOT IN ({ph})"
|
|
941
945
|
params.extend(HIDDEN_TAG_NAMES)
|
|
946
|
+
if exclude_tokens:
|
|
947
|
+
tokens_list = [t for t in exclude_tokens if t]
|
|
948
|
+
for i in range(0, len(tokens_list), 500):
|
|
949
|
+
chunk = tokens_list[i : i + 500]
|
|
950
|
+
ph = ",".join("?" * len(chunk))
|
|
951
|
+
sql += f" AND c.friendly_token NOT IN ({ph}) "
|
|
952
|
+
params.extend(chunk)
|
|
942
953
|
if min_duration_sec > 0 or max_duration_sec is not None:
|
|
943
954
|
dur_sql, dur_params = _duration_range_filter(
|
|
944
955
|
"c", min_duration_sec or None, max_duration_sec
|
|
@@ -55,3 +55,27 @@ class _CatalogDB(_CatalogMixin, _PeopleMixin, _EnrichmentMixin, _MOTDMixin, _Dom
|
|
|
55
55
|
"""
|
|
56
56
|
rows = await self._fetch_all(sql, [*HIDDEN_CATEGORY_NAMES, *HIDDEN_TAG_NAMES])
|
|
57
57
|
return {r["friendly_token"] for r in rows if r.get("friendly_token")}
|
|
58
|
+
|
|
59
|
+
async def resolve_friendly_tokens(
|
|
60
|
+
self, identifiers: list[str] | set[str]
|
|
61
|
+
) -> set[str]:
|
|
62
|
+
"""Resolve a mix of bare friendly_tokens and manifest URLs to friendly_tokens.
|
|
63
|
+
|
|
64
|
+
queue.db stores playlist item media_id as either shape; this maps both
|
|
65
|
+
back to the canonical friendly_token so cross-domain exclusion filters
|
|
66
|
+
(reserved/promo items) match regardless of which shape was recorded.
|
|
67
|
+
"""
|
|
68
|
+
ids = [i for i in identifiers if i]
|
|
69
|
+
if not ids:
|
|
70
|
+
return set()
|
|
71
|
+
tokens: set[str] = set()
|
|
72
|
+
for i in range(0, len(ids), 500):
|
|
73
|
+
chunk = ids[i : i + 500]
|
|
74
|
+
ph = ",".join("?" * len(chunk))
|
|
75
|
+
rows = await self._fetch_all(
|
|
76
|
+
f"SELECT friendly_token FROM catalog "
|
|
77
|
+
f"WHERE friendly_token IN ({ph}) OR manifest_url IN ({ph})",
|
|
78
|
+
[*chunk, *chunk],
|
|
79
|
+
)
|
|
80
|
+
tokens.update(r["friendly_token"] for r in rows if r.get("friendly_token"))
|
|
81
|
+
return tokens
|
|
@@ -195,3 +195,65 @@ async def test_partitioned_is_restricted_decoupling(partitioned_db):
|
|
|
195
195
|
|
|
196
196
|
# Now is_restricted is True
|
|
197
197
|
assert await partitioned_db.is_restricted("tok_restricted") is True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def test_partitioned_get_tags_decoupled(partitioned_db):
|
|
201
|
+
"""Regression test: get_tags must not reference saved_playlist_items in catalog.db.
|
|
202
|
+
|
|
203
|
+
Previously crashed with 'sqlite3.OperationalError: no such table:
|
|
204
|
+
saved_playlist_items' because get_tags ran the monolith reserved-item
|
|
205
|
+
exclusion subquery unconditionally, even in partitioned mode.
|
|
206
|
+
"""
|
|
207
|
+
for i in range(1, 4):
|
|
208
|
+
token = f"tag_movie_{i}"
|
|
209
|
+
await partitioned_db.catalog.insert_catalog(
|
|
210
|
+
{
|
|
211
|
+
"friendly_token": token,
|
|
212
|
+
"title": f"Tag Movie {i}",
|
|
213
|
+
"manifest_url": f"https://dropsugar.com/cytube/{token}.json",
|
|
214
|
+
"synced_at": "2026-01-01T00:00:00Z",
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
tag_id = await partitioned_db.upsert_tag("actionpacked")
|
|
218
|
+
await partitioned_db.set_catalog_tags(token, [tag_id])
|
|
219
|
+
|
|
220
|
+
tags = await partitioned_db.get_tags()
|
|
221
|
+
assert any(t["name"] == "actionpacked" for t in tags)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def test_partitioned_reserved_exclusion_resolves_manifest_url(partitioned_db):
|
|
225
|
+
"""Regression test: reserved-item exclusion must resolve queue.db media_id
|
|
226
|
+
(usually a manifest URL) back to friendly_token before filtering catalog.db,
|
|
227
|
+
or immutable/promo-pool items leak into public browse/search/tags.
|
|
228
|
+
"""
|
|
229
|
+
manifest_url = "https://dropsugar.com/cytube/reserved_by_url.json"
|
|
230
|
+
await partitioned_db.catalog.insert_catalog(
|
|
231
|
+
{
|
|
232
|
+
"friendly_token": "tok_reserved_by_url",
|
|
233
|
+
"title": "Reserved By URL",
|
|
234
|
+
"manifest_url": manifest_url,
|
|
235
|
+
"synced_at": "2026-01-01T00:00:00Z",
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
await partitioned_db.catalog.insert_catalog(
|
|
239
|
+
{
|
|
240
|
+
"friendly_token": "tok_public",
|
|
241
|
+
"title": "Public Item",
|
|
242
|
+
"manifest_url": "https://dropsugar.com/cytube/public.json",
|
|
243
|
+
"synced_at": "2026-01-01T00:00:00Z",
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
pl_id = await partitioned_db.queue.create_saved_playlist(
|
|
248
|
+
name="Immutable Event", description=None, is_immutable=True, created_by="admin"
|
|
249
|
+
)
|
|
250
|
+
# media_id stored as the manifest URL, the common real-world shape.
|
|
251
|
+
await partitioned_db.queue.append_playlist_items(
|
|
252
|
+
pl_id,
|
|
253
|
+
[{"media_type": "cm", "media_id": manifest_url, "title": "Reserved By URL"}],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
items = await partitioned_db.browse(show_hidden=False)
|
|
257
|
+
tokens = [i["friendly_token"] for i in items]
|
|
258
|
+
assert "tok_public" in tokens
|
|
259
|
+
assert "tok_reserved_by_url" not in tokens
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Integration test suite verifying live PostgreSQL webqueue database on chandra-1."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pytest
|
|
5
|
+
import asyncpg
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
CHANDRA_PG_DSN = os.getenv(
|
|
9
|
+
"KRYTEN_WEBQUEUE_PG_DSN",
|
|
10
|
+
"postgresql://kryten:kryten_secret_password@chandra-1.local:5432/webqueue",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
async def pg_conn():
|
|
16
|
+
try:
|
|
17
|
+
conn = await asyncpg.connect(CHANDRA_PG_DSN, timeout=5)
|
|
18
|
+
yield conn
|
|
19
|
+
await conn.close()
|
|
20
|
+
except (OSError, asyncpg.PostgresError) as exc:
|
|
21
|
+
pytest.skip(f"Chandra-1 PostgreSQL not reachable from this environment: {exc}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def test_live_postgres_schemas_and_extensions(pg_conn):
|
|
25
|
+
"""Verify webqueue database contains all 5 schemas and pg_trgm."""
|
|
26
|
+
schemas = await pg_conn.fetch(
|
|
27
|
+
"SELECT schema_name FROM information_schema.schemata WHERE schema_name IN ('catalog', 'queue', 'jobs', 'users', 'tmdb')"
|
|
28
|
+
)
|
|
29
|
+
found_schemas = {r["schema_name"] for r in schemas}
|
|
30
|
+
assert found_schemas == {"catalog", "queue", "jobs", "users", "tmdb"}
|
|
31
|
+
|
|
32
|
+
ext = await pg_conn.fetchval("SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm'")
|
|
33
|
+
assert ext == 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def test_live_postgres_table_counts(pg_conn):
|
|
37
|
+
"""Verify row counts in live migrated schemas."""
|
|
38
|
+
catalog_count = await pg_conn.fetchval("SELECT count(*) FROM catalog.catalog")
|
|
39
|
+
assert catalog_count >= 9900
|
|
40
|
+
|
|
41
|
+
people_count = await pg_conn.fetchval("SELECT count(*) FROM catalog.people")
|
|
42
|
+
assert people_count >= 36000
|
|
43
|
+
|
|
44
|
+
job_logs_count = await pg_conn.fetchval("SELECT count(*) FROM jobs.job_run_logs")
|
|
45
|
+
assert job_logs_count >= 500000
|
|
46
|
+
|
|
47
|
+
playlists_count = await pg_conn.fetchval(
|
|
48
|
+
"SELECT count(*) FROM queue.saved_playlists"
|
|
49
|
+
)
|
|
50
|
+
assert playlists_count >= 30
|
|
51
|
+
|
|
52
|
+
watchlist_count = await pg_conn.fetchval(
|
|
53
|
+
"SELECT count(*) FROM users.user_watchlist"
|
|
54
|
+
)
|
|
55
|
+
assert watchlist_count >= 1700
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def test_live_postgres_full_text_search(pg_conn):
|
|
59
|
+
"""Verify tsvector full-text search with GIN index on catalog.catalog."""
|
|
60
|
+
rows = await pg_conn.fetch(
|
|
61
|
+
"""
|
|
62
|
+
SELECT friendly_token, title, ts_rank(search_vector, websearch_to_tsquery('english', 'Terminator')) AS rank
|
|
63
|
+
FROM catalog.catalog
|
|
64
|
+
WHERE search_vector @@ websearch_to_tsquery('english', 'Terminator')
|
|
65
|
+
ORDER BY rank DESC
|
|
66
|
+
LIMIT 5;
|
|
67
|
+
"""
|
|
68
|
+
)
|
|
69
|
+
assert len(rows) > 0
|
|
70
|
+
titles = [r["title"] for r in rows]
|
|
71
|
+
assert any("Terminator" in t for t in titles)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def test_live_postgres_trigram_fuzzy_matching(pg_conn):
|
|
75
|
+
"""Verify pg_trgm fuzzy matching handles typos like 'Terminatr'."""
|
|
76
|
+
rows = await pg_conn.fetch(
|
|
77
|
+
"""
|
|
78
|
+
SELECT friendly_token, title, similarity(title, 'Terminatr') AS sim
|
|
79
|
+
FROM catalog.catalog
|
|
80
|
+
WHERE similarity(title, 'Terminatr') > 0.3
|
|
81
|
+
ORDER BY sim DESC
|
|
82
|
+
LIMIT 5;
|
|
83
|
+
"""
|
|
84
|
+
)
|
|
85
|
+
assert len(rows) > 0
|
|
86
|
+
top_title = rows[0]["title"]
|
|
87
|
+
assert "Terminator" in top_title
|
|
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.48.0 → kryten_webqueue-0.48.1}/deploy/podman/webqueue/webqueue-app.container
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
|
{kryten_webqueue-0.48.0 → kryten_webqueue-0.48.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
|