kryten-webqueue 0.36.5__tar.gz → 0.36.7__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.5 → kryten_webqueue-0.36.7}/CHANGELOG.md +26 -0
  2. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/classify.py +4 -28
  4. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/normalise.py +54 -0
  5. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/pipeline.py +25 -8
  6. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/title.py +4 -3
  7. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/pyproject.toml +1 -1
  8. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/.github/workflows/gitleaks.yml +0 -0
  9. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/.github/workflows/python-publish.yml +0 -0
  10. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/.github/workflows/release.yml +0 -0
  11. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/.gitignore +0 -0
  12. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/.gitleaks.toml +0 -0
  13. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/.pre-commit-config.yaml +0 -0
  14. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/AGENT-WORKFLOW-GUIDE.md +0 -0
  15. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/README.md +0 -0
  16. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/config.example.json +0 -0
  17. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/deploy/kryten-webqueue.service +0 -0
  18. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/deploy/nginx-queue.conf +0 -0
  19. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/IMPLEMENTATION_SPEC.md +0 -0
  20. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/IMPL_API_GATE.md +0 -0
  21. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/IMPL_ECONOMY.md +0 -0
  22. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/IMPL_KRYTEN_PY.md +0 -0
  23. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/IMPL_ROBOT.md +0 -0
  24. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  25. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  26. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/Log for incorrect promo insertion problem.txt +0 -0
  27. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  28. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/PRE_PLAN_GAPS.md +0 -0
  29. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/PRODUCT_PLAN.md +0 -0
  30. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  31. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/SPEC_EMOTE_REHOST.md +0 -0
  32. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  33. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/docs/UX_POLISH_PLAN.md +0 -0
  34. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/__init__.py +0 -0
  35. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/__main__.py +0 -0
  36. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/api_gate/__init__.py +0 -0
  37. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/api_gate/client.py +0 -0
  38. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/app.py +0 -0
  39. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/auth/__init__.py +0 -0
  40. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/auth/otp.py +0 -0
  41. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/auth/rate_limit.py +0 -0
  42. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/auth/session.py +0 -0
  43. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/__init__.py +0 -0
  44. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/__init__.py +0 -0
  45. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  46. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_connection.py +0 -0
  47. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  48. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  49. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_people.py +0 -0
  50. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  51. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_queue.py +0 -0
  52. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  53. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  54. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  55. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  56. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  57. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  58. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  59. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  60. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  61. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  62. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/images.py +0 -0
  63. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/mediacms.py +0 -0
  64. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/catalog/sync.py +0 -0
  65. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/config.py +0 -0
  66. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/__init__.py +0 -0
  67. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  68. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  69. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  70. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  71. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  72. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  73. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  74. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  75. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  76. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  77. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/jobs/__init__.py +0 -0
  78. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  79. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  80. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/jobs/manager.py +0 -0
  81. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  82. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/jobs/tasks.py +0 -0
  83. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/logging_config.py +0 -0
  84. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/playlists/__init__.py +0 -0
  85. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/playlists/bulk_add.py +0 -0
  86. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/playlists/fire.py +0 -0
  87. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/playlists/importer.py +0 -0
  88. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/playlists/ordering.py +0 -0
  89. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/playlists/scheduler.py +0 -0
  90. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/promos/__init__.py +0 -0
  91. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/promos/director.py +0 -0
  92. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/__init__.py +0 -0
  93. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/completion.py +0 -0
  94. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/ordering.py +0 -0
  95. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/poller.py +0 -0
  96. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/presence.py +0 -0
  97. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/race_poller.py +0 -0
  98. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/queue/shadow.py +0 -0
  99. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/__init__.py +0 -0
  100. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_catalog.py +0 -0
  101. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_feedback.py +0 -0
  102. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  103. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_jobs.py +0 -0
  104. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_moderation.py +0 -0
  105. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_playlists.py +0 -0
  106. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_promos.py +0 -0
  107. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_queue.py +0 -0
  108. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/admin_schedules.py +0 -0
  109. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/auth.py +0 -0
  110. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/catalog.py +0 -0
  111. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/feedback.py +0 -0
  112. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/pages.py +0 -0
  113. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/queue.py +0 -0
  114. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/routes/user.py +0 -0
  115. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/css/main.css +0 -0
  116. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  117. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/js/admin-promos.js +0 -0
  118. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  119. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  120. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/js/admin.js +0 -0
  121. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/static/js/main.js +0 -0
  122. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/admin/index.html +0 -0
  123. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/admin/playlists.html +0 -0
  124. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/admin/promos.html +0 -0
  125. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  126. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/admin/schedules.html +0 -0
  127. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/auth/login.html +0 -0
  128. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/base.html +0 -0
  129. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/catalog/browse.html +0 -0
  130. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  131. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/scripts/patch_mediacms_bulk_facets.py +0 -0
  141. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/scripts/patch_mediacms_tags.py +0 -0
  142. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/scripts/trigger_enrich.py +0 -0
  143. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/__init__.py +0 -0
  144. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_classify.py +0 -0
  145. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_config_persistence.py +0 -0
  146. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_feedback.py +0 -0
  147. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_fetchurls_sharepoint.py +0 -0
  148. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_people_facets.py +0 -0
  149. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_phase1.py +0 -0
  150. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_phase2_jobs.py +0 -0
  151. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_phase3_jobs.py +0 -0
  152. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_phase4_live_fixes.py +0 -0
  153. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_playlist_import.py +0 -0
  154. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_presence_refund.py +0 -0
  155. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_promo_director.py +0 -0
  156. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_promo_pool_exclusion.py +0 -0
  157. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_providers.py +0 -0
  158. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_queue_announce.py +0 -0
  159. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_race_poller.py +0 -0
  160. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_recently_played_hide.py +0 -0
  161. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_save_results_to_playlist.py +0 -0
  162. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_schedule_lock.py +0 -0
  163. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_search_facets.py +0 -0
  164. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_title_normalization.py +0 -0
  165. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.7}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.36.7] - 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - **`force` now re-derives classification from the raw title.** The enrichment
10
+ pipeline cached `content_type`/`lookup_title` from the first classify run and
11
+ reused them on every subsequent run — so classify-logic improvements (e.g. the
12
+ new dub/sub and YouTube-pipe title cleaning) never reached already-classified
13
+ items even under `force`. A forced run now bypasses the cache and recomputes
14
+ the classification, so title/lookup fixes take effect on re-enrichment.
15
+
16
+ ## [0.36.6] - 2026-08-14
17
+
18
+ ### Fixed
19
+
20
+ - **`(Dubbed)` / `(Subbed)` markers no longer break art lookup.** `(dubbed)` is
21
+ stripped from both the display title and the search query (dubbed is the
22
+ default). `(subbed)`/`(subs)`/`(subtitles)` stay in the display title but are
23
+ omitted from the metadata/art search.
24
+ - **YouTube “full movie” pipe titles are cleaned properly.** Detection now matches
25
+ non-adjacent variants like “Full **Horror** Movie”, the title is taken from the
26
+ first segment before the pipe, and the year is extracted from the whole string
27
+ (it often trails after the last pipe, e.g. “… | Larry Bucha (1961)”). Both the
28
+ search query and the display title use this cleanup consistently (previously the
29
+ display title was never tidied for pipe uploads).
30
+
5
31
  ## [0.36.5] - 2026-08-14
6
32
 
7
33
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.36.5
3
+ Version: 0.36.7
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
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import re
10
10
  from dataclasses import dataclass, field
11
11
 
12
- from .normalise import normalize_and_clean, strip_extension
12
+ from .normalise import normalize_movie_title
13
13
 
14
14
  # ---------------------------------------------------------------------------
15
15
  # Hosted show registry
@@ -250,13 +250,6 @@ class ItemClassification:
250
250
  has_real_art: bool = False
251
251
 
252
252
 
253
- # YouTube "Title | English Full Movie | Genre1 Genre2" pattern
254
- # Also handles variants like "Title | Full Movie HD | Description"
255
- _YT_FULL_MOVIE_RE = re.compile(
256
- r"^(?P<title>.+?)\s*\|.*(Full\s+(?:Movie|Film|Length|Feature)).*$", re.I
257
- )
258
-
259
-
260
253
  def classify_item(
261
254
  friendly_token: str,
262
255
  raw_title: str,
@@ -326,26 +319,9 @@ def classify_item(
326
319
 
327
320
  # 4. Movie (>= 30 min)
328
321
  if duration_sec >= 1800:
329
- # YouTube title cleanup: extract title before first pipe if "full movie" appears
330
- yt = _YT_FULL_MOVIE_RE.match(raw_title.strip())
331
- if yt:
332
- clean_title = yt.group("title").strip()
333
- # Extract year from the clean title part
334
- clean_title, year = normalize_and_clean(clean_title)
335
- return ItemClassification(
336
- friendly_token=friendly_token,
337
- raw_title=raw_title,
338
- content_type="movie",
339
- hosted=None,
340
- lookup_title=clean_title,
341
- lookup_year=year,
342
- genre_hints=[],
343
- duration_sec=duration_sec,
344
- description_score=description_score,
345
- has_real_art=has_real_art,
346
- )
347
-
348
- clean_title, year = normalize_and_clean(raw_title)
322
+ # Handles YouTube pipe titles and dub/sub markers; year is pulled from
323
+ # the whole string so a trailing "(1961)" after pipes is still captured.
324
+ clean_title, year = normalize_movie_title(raw_title, for_search=True)
349
325
  return ItemClassification(
350
326
  friendly_token=friendly_token,
351
327
  raw_title=raw_title,
@@ -103,3 +103,57 @@ def normalize_and_clean(title: str) -> tuple[str, str | None]:
103
103
  t = strip_extension(t.strip(" .-"))
104
104
  t = re.sub(r"\s+", " ", t).strip()
105
105
  return t or title, year
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Dub / sub markers and YouTube "full movie" pipe titles
110
+ # ---------------------------------------------------------------------------
111
+
112
+ # "dubbed" is the default expectation → stripped from title and search.
113
+ _DUB_TERMS = ["dubbed"]
114
+ # sub markers stay in the display title but are omitted from art/metadata search.
115
+ _SUB_TERMS = ["subbed", "subtitled", "subtitles", "subs"]
116
+
117
+ # A pipe-delimited YouTube upload advertising a full movie, e.g.
118
+ # "Title | English Full Movie | ...", "Title | Full Horror Movie | ...".
119
+ _YT_FULL_MOVIE_RE = re.compile(
120
+ r"\bfull\s+(?:\w+\s+)?(?:movie|film|feature|length)\b", re.I
121
+ )
122
+
123
+
124
+ def _strip_terms(title: str, terms: list[str]) -> str:
125
+ """Remove bracketed and bare occurrences of ``terms`` from ``title``."""
126
+ pat = "|".join(terms)
127
+ title = re.sub(rf"\s*[\(\[\{{]\s*(?:{pat})\s*[\)\]\}}]", "", title, flags=re.I)
128
+ title = re.sub(rf"\s*\b(?:{pat})\b", "", title, flags=re.I)
129
+ return re.sub(r"\s+", " ", title).strip()
130
+
131
+
132
+ def _pre_pipe_title(title: str) -> str | None:
133
+ """First segment of a pipe-delimited YouTube 'full movie' title, else None."""
134
+ if "|" not in title or not _YT_FULL_MOVIE_RE.search(title):
135
+ return None
136
+ return title.split("|", 1)[0].strip()
137
+
138
+
139
+ def normalize_movie_title(raw: str, *, for_search: bool) -> tuple[str, str | None]:
140
+ """Return ``(clean_title, year_or_None)`` for a plain movie title.
141
+
142
+ Handles two cases the base cleaner misses:
143
+ - YouTube pipe titles ("Title | Full Movie | ..."): uses the first segment
144
+ before the pipe, but extracts the year from the *whole* string (it often
145
+ trails after the last pipe, e.g. "... | Larry Bucha (1961)").
146
+ - Dub/sub markers: ``(dubbed)`` is always removed; sub markers are removed
147
+ only when ``for_search`` (they stay in the display title).
148
+ """
149
+ # Year first, from the full string, so a trailing "(1961)" is captured even
150
+ # when the title before the pipe has none.
151
+ _, full_year = normalize_and_clean(raw)
152
+ base = _pre_pipe_title(raw) or raw
153
+ base = (
154
+ _strip_terms(base, _DUB_TERMS + _SUB_TERMS)
155
+ if for_search
156
+ else _strip_terms(base, _DUB_TERMS)
157
+ )
158
+ clean, base_year = normalize_and_clean(base)
159
+ return clean, base_year or full_year
@@ -47,7 +47,11 @@ class CatalogEnrichmentPipeline:
47
47
 
48
48
  logger.info(
49
49
  "[enrichment] Starting pipeline: steps=%s tokens=%s force=%s dry_run=%s limit=%s",
50
- steps_to_run, "specific" if tokens else "all", force, dry_run, limit
50
+ steps_to_run,
51
+ "specific" if tokens else "all",
52
+ force,
53
+ dry_run,
54
+ limit,
51
55
  )
52
56
 
53
57
  start = time.monotonic()
@@ -73,13 +77,15 @@ class CatalogEnrichmentPipeline:
73
77
  )
74
78
  logger.info(
75
79
  "[enrichment] step=%s found %d item(s) (force=%s)",
76
- step_name, len(rows), force
80
+ step_name,
81
+ len(rows),
82
+ force,
77
83
  )
78
84
  if not rows:
79
85
  by_step[step_name] = StepResult(skipped=0)
80
86
  continue
81
87
 
82
- classifications = [self._classify_row(row) for row in rows]
88
+ classifications = [self._classify_row(row, force=force) for row in rows]
83
89
  total_items = max(total_items, len(classifications))
84
90
 
85
91
  if step_name == "classify":
@@ -140,14 +146,20 @@ class CatalogEnrichmentPipeline:
140
146
  by_step[step_name] = r
141
147
  logger.info(
142
148
  "[enrichment] step=%s complete: processed=%d changed=%d skipped=%d errors=%d",
143
- step_name, r.processed, r.changed, r.skipped, len(r.errors)
149
+ step_name,
150
+ r.processed,
151
+ r.changed,
152
+ r.skipped,
153
+ len(r.errors),
144
154
  )
145
155
  await self._report_progress(ctx, step_name, r)
146
156
 
147
157
  elapsed = time.monotonic() - start
148
158
  logger.info(
149
159
  "[enrichment] Pipeline complete in %.1fs: %d item(s), %d step(s)",
150
- elapsed, total_items, len(steps_to_run)
160
+ elapsed,
161
+ total_items,
162
+ len(steps_to_run),
151
163
  )
152
164
 
153
165
  return EnrichmentReport(
@@ -158,12 +170,17 @@ class CatalogEnrichmentPipeline:
158
170
  dry_run=dry_run,
159
171
  )
160
172
 
161
- def _classify_row(self, row: dict) -> ItemClassification:
162
- """Classify a DB row, using cached enrichment state when available."""
173
+ def _classify_row(self, row: dict, *, force: bool = False) -> ItemClassification:
174
+ """Classify a DB row, using cached enrichment state when available.
175
+
176
+ ``force`` bypasses the cache and re-derives the classification from the
177
+ raw title so classify-logic improvements apply to already-classified
178
+ items.
179
+ """
163
180
  token = row["friendly_token"]
164
181
  # If enrichment state already has lookup_title from a prior classify run,
165
182
  # we reconstruct the classification from that cached data.
166
- if row.get("content_type") and row.get("lookup_title"):
183
+ if not force and row.get("content_type") and row.get("lookup_title"):
167
184
  from .classify import ItemClassification, HostedInfo, HOSTED_SHOW_REGISTRY
168
185
 
169
186
  hosted: HostedInfo | None = None
@@ -7,7 +7,7 @@ from datetime import datetime, UTC
7
7
  import httpx
8
8
 
9
9
  from ..classify import ItemClassification
10
- from ..normalise import normalize_and_clean
10
+ from ..normalise import normalize_movie_title
11
11
  from ..report import StepResult
12
12
 
13
13
  logger = logging.getLogger(__name__)
@@ -83,7 +83,9 @@ class TitleStep:
83
83
  """Return the desired CMS title, or None if no change needed."""
84
84
  ct = cls.content_type
85
85
  if ct == "movie":
86
- clean, year = normalize_and_clean(cls.raw_title)
86
+ # Display title keeps sub markers (dubbed is stripped); mirrors the
87
+ # search cleanup so pipe/YouTube titles are tidied consistently.
88
+ clean, year = normalize_movie_title(cls.raw_title, for_search=False)
87
89
  if year:
88
90
  candidate = f"{clean} ({year})"
89
91
  else:
@@ -100,7 +102,6 @@ class TitleStep:
100
102
  candidate = f"{t} - {show}"
101
103
  return candidate if candidate != cls.raw_title else None
102
104
  if ct == "riffed_movie" and cls.hosted:
103
- entry_template = cls.hosted # HostedInfo has show_name
104
105
  if "{title}" in (cls.hosted.show_name or ""):
105
106
  return None # template handled below
106
107
  # Only Rifftrax gets reformat; MST3K keeps original
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.36.5"
3
+ version = "0.36.7"
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"