kryten-webqueue 0.36.3__tar.gz → 0.36.4__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.
Files changed (165) hide show
  1. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/CHANGELOG.md +12 -0
  2. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/sync.py +69 -19
  4. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/pyproject.toml +1 -1
  5. kryten_webqueue-0.36.4/scripts/patch_mediacms_bulk_facets.py +105 -0
  6. kryten_webqueue-0.36.4/scripts/trigger_enrich.py +45 -0
  7. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/.github/workflows/gitleaks.yml +0 -0
  8. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/.github/workflows/python-publish.yml +0 -0
  9. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/.github/workflows/release.yml +0 -0
  10. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/.gitignore +0 -0
  11. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/.gitleaks.toml +0 -0
  12. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/.pre-commit-config.yaml +0 -0
  13. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/AGENT-WORKFLOW-GUIDE.md +0 -0
  14. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/README.md +0 -0
  15. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/config.example.json +0 -0
  16. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/deploy/kryten-webqueue.service +0 -0
  17. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/deploy/nginx-queue.conf +0 -0
  18. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/IMPLEMENTATION_SPEC.md +0 -0
  19. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/IMPL_API_GATE.md +0 -0
  20. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/IMPL_ECONOMY.md +0 -0
  21. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/IMPL_KRYTEN_PY.md +0 -0
  22. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/IMPL_ROBOT.md +0 -0
  23. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  24. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  25. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/Log for incorrect promo insertion problem.txt +0 -0
  26. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  27. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/PRE_PLAN_GAPS.md +0 -0
  28. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/PRODUCT_PLAN.md +0 -0
  29. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  30. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/SPEC_EMOTE_REHOST.md +0 -0
  31. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  32. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/docs/UX_POLISH_PLAN.md +0 -0
  33. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/__init__.py +0 -0
  34. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/__main__.py +0 -0
  35. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/api_gate/__init__.py +0 -0
  36. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/api_gate/client.py +0 -0
  37. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/app.py +0 -0
  38. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/auth/__init__.py +0 -0
  39. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/auth/otp.py +0 -0
  40. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/auth/rate_limit.py +0 -0
  41. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/auth/session.py +0 -0
  42. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/__init__.py +0 -0
  43. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/__init__.py +0 -0
  44. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  45. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_connection.py +0 -0
  46. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  47. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  48. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_people.py +0 -0
  49. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  50. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_queue.py +0 -0
  51. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  52. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  53. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  54. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  55. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  56. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  57. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  58. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  59. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  60. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  61. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  62. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  63. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  64. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  65. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/images.py +0 -0
  66. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/catalog/mediacms.py +0 -0
  67. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/config.py +0 -0
  68. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/__init__.py +0 -0
  69. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  70. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  71. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  72. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  73. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  74. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  75. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  76. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  77. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  78. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  79. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/jobs/__init__.py +0 -0
  80. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  81. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  82. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/jobs/manager.py +0 -0
  83. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  84. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/jobs/tasks.py +0 -0
  85. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/logging_config.py +0 -0
  86. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/playlists/__init__.py +0 -0
  87. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/playlists/bulk_add.py +0 -0
  88. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/playlists/fire.py +0 -0
  89. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/playlists/importer.py +0 -0
  90. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/playlists/ordering.py +0 -0
  91. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/playlists/scheduler.py +0 -0
  92. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/promos/__init__.py +0 -0
  93. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/promos/director.py +0 -0
  94. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/__init__.py +0 -0
  95. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/completion.py +0 -0
  96. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/ordering.py +0 -0
  97. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/poller.py +0 -0
  98. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/presence.py +0 -0
  99. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/race_poller.py +0 -0
  100. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/queue/shadow.py +0 -0
  101. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/__init__.py +0 -0
  102. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_catalog.py +0 -0
  103. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_feedback.py +0 -0
  104. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  105. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_jobs.py +0 -0
  106. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_moderation.py +0 -0
  107. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_playlists.py +0 -0
  108. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_promos.py +0 -0
  109. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_queue.py +0 -0
  110. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/admin_schedules.py +0 -0
  111. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/auth.py +0 -0
  112. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/catalog.py +0 -0
  113. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/feedback.py +0 -0
  114. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/pages.py +0 -0
  115. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/queue.py +0 -0
  116. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/routes/user.py +0 -0
  117. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/css/main.css +0 -0
  118. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  119. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/js/admin-promos.js +0 -0
  120. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  121. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  122. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/js/admin.js +0 -0
  123. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/static/js/main.js +0 -0
  124. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/admin/index.html +0 -0
  125. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/admin/playlists.html +0 -0
  126. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/admin/promos.html +0 -0
  127. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  128. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/admin/schedules.html +0 -0
  129. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/auth/login.html +0 -0
  130. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/base.html +0 -0
  131. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/catalog/browse.html +0 -0
  132. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  133. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  134. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/feedback/index.html +0 -0
  135. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/queue/index.html +0 -0
  136. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/race.html +0 -0
  137. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/user/dashboard.html +0 -0
  138. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/templates/user/my_list.html +0 -0
  139. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/ws/__init__.py +0 -0
  140. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/ws/handler.py +0 -0
  141. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/kryten_webqueue/ws/manager.py +0 -0
  142. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/scripts/patch_mediacms_tags.py +0 -0
  143. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/__init__.py +0 -0
  144. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_classify.py +0 -0
  145. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_config_persistence.py +0 -0
  146. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_feedback.py +0 -0
  147. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_fetchurls_sharepoint.py +0 -0
  148. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_people_facets.py +0 -0
  149. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_phase1.py +0 -0
  150. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_phase2_jobs.py +0 -0
  151. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_phase3_jobs.py +0 -0
  152. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_phase4_live_fixes.py +0 -0
  153. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_playlist_import.py +0 -0
  154. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_presence_refund.py +0 -0
  155. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_promo_director.py +0 -0
  156. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_promo_pool_exclusion.py +0 -0
  157. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_providers.py +0 -0
  158. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_queue_announce.py +0 -0
  159. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_race_poller.py +0 -0
  160. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_recently_played_hide.py +0 -0
  161. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_save_results_to_playlist.py +0 -0
  162. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_schedule_lock.py +0 -0
  163. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_search_facets.py +0 -0
  164. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_title_normalization.py +0 -0
  165. {kryten_webqueue-0.36.3 → kryten_webqueue-0.36.4}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.36.4] - 2026-08-14
6
+
7
+ ### Changed
8
+
9
+ - **Catalog sync pulls tags/categories in bulk.** Instead of one
10
+ `GET /api/v1/media/{token}` per item (10k+ requests, ~30+ min), sync now
11
+ prefetches every item's facets from a new `GET /api/v1/media_facets` endpoint
12
+ in a handful of paginated calls (~35s for the full catalog). Falls back to the
13
+ per-item detail endpoint automatically when the bulk endpoint is unavailable
14
+ (stock MediaCMS). See `scripts/patch_mediacms_bulk_facets.py` for the CMS-side
15
+ endpoint.
16
+
5
17
  ## [0.36.3] - 2026-08-14
6
18
 
7
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.36.3
3
+ Version: 0.36.4
4
4
  Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
5
5
  Author: grobertson
6
6
  License-Expression: MIT
@@ -46,6 +46,9 @@ class CatalogSync:
46
46
  self._token = mediacms_token
47
47
  self._db = db
48
48
  self._cover_art = cover_art
49
+ # Populated once per sync by _prefetch_all_facets; None means the bulk
50
+ # endpoint is unavailable and we fall back to per-item detail fetches.
51
+ self._facets_map: dict[str, dict] | None = None
49
52
  self._client = httpx.AsyncClient(
50
53
  headers={"Authorization": f"Token {mediacms_token}"},
51
54
  timeout=30.0,
@@ -60,6 +63,10 @@ class CatalogSync:
60
63
  stats = {"seen": 0, "new": 0, "updated": 0, "errors": 0, "deleted": 0}
61
64
  sync_started_at = datetime.now(UTC).isoformat()
62
65
 
66
+ # Pull every item's tags/categories in a few paginated calls up front so
67
+ # per-item facet sync needs no HTTP (falls back to detail fetch on miss).
68
+ await self._prefetch_all_facets()
69
+
63
70
  try:
64
71
  # /manage_media returns all items (9k+); /media is capped at 1000
65
72
  next_url: str | None = f"{self._url}/api/v1/manage_media"
@@ -191,33 +198,76 @@ class CatalogSync:
191
198
  await asyncio.sleep(0.25)
192
199
 
193
200
  async def _sync_item_facets(self, token: str):
194
- """Populate category/tag memberships from the media detail endpoint.
201
+ """Populate category/tag memberships for an item.
195
202
 
196
- The list (manage_media) serializer omits categories/tags, so we fetch
197
- the per-item detail (``/api/v1/media/{token}``) which exposes
198
- ``categories_info`` and ``tags_info``.
203
+ Prefers the bulk facets pulled by :meth:`_prefetch_all_facets`; falls
204
+ back to the per-item detail endpoint (``/api/v1/media/{token}``) which
205
+ exposes ``categories_info`` and ``tags_info`` when the bulk map is
206
+ unavailable or missing this token.
199
207
  """
200
- resp = await self._client.get(f"{self._url}/api/v1/media/{token}")
201
- if resp.status_code != 200:
202
- return
203
- data = resp.json()
204
-
205
- cat_names = [
206
- c.get("title")
207
- for c in (data.get("categories_info") or [])
208
- if isinstance(c, dict) and c.get("title")
209
- ]
210
- tag_names = []
211
- for t in data.get("tags_info") or []:
212
- name = t.get("title") if isinstance(t, dict) else t
213
- if name:
214
- tag_names.append(name)
208
+ cached = self._facets_map.get(token) if self._facets_map is not None else None
209
+ if cached is not None:
210
+ cat_names = cached.get("categories") or []
211
+ tag_names = cached.get("tags") or []
212
+ else:
213
+ resp = await self._client.get(f"{self._url}/api/v1/media/{token}")
214
+ if resp.status_code != 200:
215
+ return
216
+ data = resp.json()
217
+ cat_names = [
218
+ c.get("title")
219
+ for c in (data.get("categories_info") or [])
220
+ if isinstance(c, dict) and c.get("title")
221
+ ]
222
+ tag_names = []
223
+ for t in data.get("tags_info") or []:
224
+ name = t.get("title") if isinstance(t, dict) else t
225
+ if name:
226
+ tag_names.append(name)
215
227
 
216
228
  cat_ids = [await self._db.upsert_category(n) for n in cat_names]
217
229
  tag_ids = [await self._db.upsert_tag(n) for n in tag_names]
218
230
  await self._db.set_catalog_categories(token, cat_ids)
219
231
  await self._db.set_catalog_tags(token, tag_ids)
220
232
 
233
+ async def _prefetch_all_facets(self) -> None:
234
+ """Bulk-load every item's tags/categories from ``/api/v1/media_facets``.
235
+
236
+ One call per page (page_size=500) instead of one GET per item. Sets
237
+ ``self._facets_map`` to None on any failure so callers fall back to the
238
+ per-item detail endpoint.
239
+ """
240
+ facets: dict[str, dict] = {}
241
+ page = 1
242
+ try:
243
+ while True:
244
+ resp = await self._client.get(
245
+ f"{self._url}/api/v1/media_facets",
246
+ params={"page": page, "page_size": 500},
247
+ )
248
+ if resp.status_code != 200:
249
+ # Endpoint absent (stock CMS) — fall back to per-item.
250
+ self._facets_map = None if page == 1 else facets
251
+ return
252
+ data = resp.json()
253
+ for row in data.get("results") or []:
254
+ tok = row.get("friendly_token")
255
+ if tok:
256
+ facets[tok] = {
257
+ "tags": row.get("tags") or [],
258
+ "categories": row.get("categories") or [],
259
+ }
260
+ if not data.get("has_next"):
261
+ break
262
+ page += 1
263
+ self._facets_map = facets
264
+ logger.info(
265
+ "Prefetched facets for %d items in %d page(s)", len(facets), page
266
+ )
267
+ except (httpx.HTTPError, ValueError) as exc:
268
+ logger.debug("Facet prefetch failed (%s); using per-item fallback", exc)
269
+ self._facets_map = None
270
+
221
271
  def _build_manifest_url(self, media: dict) -> str:
222
272
  # CyTube custom media ("cm") requires the manifest JSON URL, NOT the
223
273
  # human watch page (/view?m=TOKEN). MediaCMS exposes a CyTube manifest at
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.36.3"
3
+ version = "0.36.4"
4
4
  description = "Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """Add a read-only bulk facets endpoint to MediaCMS so integrations can pull
3
+ every item's friendly_token + tags + categories in a few paginated calls
4
+ instead of one GET per media. Editor/superuser only. Idempotent."""
5
+
6
+ import sys
7
+
8
+ BASE = "/home/mediacms.io/mediacms/files"
9
+ MEDIA = f"{BASE}/views/media.py"
10
+ INIT = f"{BASE}/views/__init__.py"
11
+ URLS = f"{BASE}/urls.py"
12
+
13
+ VIEW_MARKER = "class MediaFacetsList(APIView):"
14
+ VIEW_CODE = '''
15
+
16
+ class MediaFacetsList(APIView):
17
+ """Bulk read-only listing of media friendly_token + tags + categories.
18
+
19
+ Lets trusted integrations (MediaCMS editors) pull every item's facets in a
20
+ few paginated calls instead of one GET per media. Slice pagination by id.
21
+ """
22
+
23
+ permission_classes = (permissions.IsAuthenticated,)
24
+
25
+ def get(self, request, format=None):
26
+ if not is_mediacms_editor(request.user):
27
+ return Response({"detail": "not allowed"}, status=status.HTTP_403_FORBIDDEN)
28
+ try:
29
+ page = max(1, int(request.query_params.get("page", 1)))
30
+ except (TypeError, ValueError):
31
+ page = 1
32
+ try:
33
+ page_size = int(request.query_params.get("page_size", 500))
34
+ except (TypeError, ValueError):
35
+ page_size = 500
36
+ page_size = max(1, min(2000, page_size))
37
+ start = (page - 1) * page_size
38
+ rows = Media.objects.order_by("id").prefetch_related("tags", "category")[
39
+ start : start + page_size
40
+ ]
41
+ results = [
42
+ {
43
+ "friendly_token": m.friendly_token,
44
+ "tags": [t.title for t in m.tags.all()],
45
+ "categories": [c.title for c in m.category.all()],
46
+ }
47
+ for m in rows
48
+ ]
49
+ return Response(
50
+ {
51
+ "page": page,
52
+ "page_size": page_size,
53
+ "has_next": len(results) == page_size,
54
+ "results": results,
55
+ }
56
+ )
57
+ '''
58
+
59
+ INIT_ANCHOR = "from .media import MediaBulkUserActions # noqa: F401\n"
60
+ INIT_ADD = "from .media import MediaFacetsList # noqa: F401\n"
61
+
62
+ URL_ANCHOR = (
63
+ ' re_path(r"^api/v1/media/user/bulk_actions/$", views.MediaBulkUserActions.as_view()),\n'
64
+ )
65
+ URL_ADD = ' re_path(r"^api/v1/media_facets$", views.MediaFacetsList.as_view()),\n'
66
+
67
+
68
+ def patch(path, check, apply_fn):
69
+ with open(path, encoding="utf-8") as f:
70
+ src = f.read()
71
+ if check in src:
72
+ print(f"{path}: already patched")
73
+ return True
74
+ new = apply_fn(src)
75
+ if new is None:
76
+ print(f"ERROR: anchor not found in {path}")
77
+ return False
78
+ with open(path, "w", encoding="utf-8") as f:
79
+ f.write(new)
80
+ print(f"{path}: patched")
81
+ return True
82
+
83
+
84
+ def main() -> int:
85
+ ok = True
86
+ ok &= patch(MEDIA, VIEW_MARKER, lambda s: s.rstrip("\n") + "\n" + VIEW_CODE)
87
+ ok &= patch(
88
+ INIT,
89
+ INIT_ADD.strip(),
90
+ lambda s: s.replace(INIT_ANCHOR, INIT_ANCHOR + INIT_ADD, 1)
91
+ if INIT_ANCHOR in s
92
+ else None,
93
+ )
94
+ ok &= patch(
95
+ URLS,
96
+ "media_facets",
97
+ lambda s: s.replace(URL_ANCHOR, URL_ANCHOR + URL_ADD, 1)
98
+ if URL_ANCHOR in s
99
+ else None,
100
+ )
101
+ return 0 if ok else 1
102
+
103
+
104
+ if __name__ == "__main__":
105
+ sys.exit(main())
@@ -0,0 +1,45 @@
1
+ """One-off: mint an admin session and trigger the catalog_enrich job locally.
2
+
3
+ Usage (on the server, with the service venv python):
4
+ python trigger_enrich.py steps=tags force=1
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ import urllib.request
10
+ from datetime import datetime, timedelta, UTC
11
+
12
+ import jwt
13
+
14
+ CONFIG_PATH = "/etc/kryten-webqueue/config.json"
15
+
16
+
17
+ def main() -> int:
18
+ params = dict(a.split("=", 1) for a in sys.argv[1:] if "=" in a)
19
+ cfg = json.load(open(CONFIG_PATH))
20
+ secret = cfg["secret_key"]
21
+ port = cfg.get("port", 2010)
22
+ token = jwt.encode(
23
+ {
24
+ "sub": "backfill-admin",
25
+ "rank": 3,
26
+ "iat": datetime.now(UTC),
27
+ "exp": datetime.now(UTC) + timedelta(hours=1),
28
+ },
29
+ secret,
30
+ algorithm="HS256",
31
+ )
32
+ body = json.dumps({"params": params}).encode()
33
+ req = urllib.request.Request(
34
+ f"http://127.0.0.1:{port}/admin/jobs/catalog_enrich/run",
35
+ data=body,
36
+ headers={"Content-Type": "application/json", "Cookie": f"session={token}"},
37
+ method="POST",
38
+ )
39
+ with urllib.request.urlopen(req, timeout=30) as resp:
40
+ print(resp.status, resp.read().decode())
41
+ return 0
42
+
43
+
44
+ if __name__ == "__main__":
45
+ raise SystemExit(main())