kryten-webqueue 0.36.5__tar.gz → 0.36.6__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.6}/CHANGELOG.md +15 -0
  2. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/classify.py +4 -28
  4. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/normalise.py +54 -0
  5. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/title.py +4 -3
  6. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/pyproject.toml +1 -1
  7. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/.github/workflows/gitleaks.yml +0 -0
  8. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/.github/workflows/python-publish.yml +0 -0
  9. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/.github/workflows/release.yml +0 -0
  10. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/.gitignore +0 -0
  11. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/.gitleaks.toml +0 -0
  12. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/.pre-commit-config.yaml +0 -0
  13. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/AGENT-WORKFLOW-GUIDE.md +0 -0
  14. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/README.md +0 -0
  15. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/config.example.json +0 -0
  16. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/deploy/kryten-webqueue.service +0 -0
  17. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/deploy/nginx-queue.conf +0 -0
  18. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/IMPLEMENTATION_SPEC.md +0 -0
  19. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/IMPL_API_GATE.md +0 -0
  20. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/IMPL_ECONOMY.md +0 -0
  21. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/IMPL_KRYTEN_PY.md +0 -0
  22. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/IMPL_ROBOT.md +0 -0
  23. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  24. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  25. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/Log for incorrect promo insertion problem.txt +0 -0
  26. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  27. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/PRE_PLAN_GAPS.md +0 -0
  28. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/PRODUCT_PLAN.md +0 -0
  29. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  30. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/SPEC_EMOTE_REHOST.md +0 -0
  31. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  32. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/docs/UX_POLISH_PLAN.md +0 -0
  33. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/__init__.py +0 -0
  34. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/__main__.py +0 -0
  35. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/api_gate/__init__.py +0 -0
  36. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/api_gate/client.py +0 -0
  37. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/app.py +0 -0
  38. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/auth/__init__.py +0 -0
  39. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/auth/otp.py +0 -0
  40. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/auth/rate_limit.py +0 -0
  41. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/auth/session.py +0 -0
  42. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/__init__.py +0 -0
  43. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/__init__.py +0 -0
  44. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  45. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_connection.py +0 -0
  46. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  47. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  48. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_people.py +0 -0
  49. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  50. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_queue.py +0 -0
  51. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  52. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  53. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  54. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  55. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  56. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  57. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  58. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  59. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  60. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  61. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  62. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/images.py +0 -0
  63. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/mediacms.py +0 -0
  64. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/catalog/sync.py +0 -0
  65. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/config.py +0 -0
  66. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/__init__.py +0 -0
  67. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  68. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  69. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  70. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  71. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  72. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  73. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  74. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  75. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  76. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  77. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/jobs/__init__.py +0 -0
  78. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  79. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  80. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/jobs/manager.py +0 -0
  81. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  82. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/jobs/tasks.py +0 -0
  83. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/logging_config.py +0 -0
  84. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/playlists/__init__.py +0 -0
  85. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/playlists/bulk_add.py +0 -0
  86. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/playlists/fire.py +0 -0
  87. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/playlists/importer.py +0 -0
  88. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/playlists/ordering.py +0 -0
  89. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/playlists/scheduler.py +0 -0
  90. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/promos/__init__.py +0 -0
  91. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/promos/director.py +0 -0
  92. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/__init__.py +0 -0
  93. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/completion.py +0 -0
  94. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/ordering.py +0 -0
  95. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/poller.py +0 -0
  96. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/presence.py +0 -0
  97. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/race_poller.py +0 -0
  98. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/queue/shadow.py +0 -0
  99. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/__init__.py +0 -0
  100. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_catalog.py +0 -0
  101. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_feedback.py +0 -0
  102. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  103. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_jobs.py +0 -0
  104. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_moderation.py +0 -0
  105. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_playlists.py +0 -0
  106. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_promos.py +0 -0
  107. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_queue.py +0 -0
  108. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/admin_schedules.py +0 -0
  109. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/auth.py +0 -0
  110. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/catalog.py +0 -0
  111. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/feedback.py +0 -0
  112. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/pages.py +0 -0
  113. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/queue.py +0 -0
  114. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/routes/user.py +0 -0
  115. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/css/main.css +0 -0
  116. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  117. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/js/admin-promos.js +0 -0
  118. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  119. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  120. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/js/admin.js +0 -0
  121. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/static/js/main.js +0 -0
  122. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/admin/index.html +0 -0
  123. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/admin/playlists.html +0 -0
  124. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/admin/promos.html +0 -0
  125. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  126. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/admin/schedules.html +0 -0
  127. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/auth/login.html +0 -0
  128. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/base.html +0 -0
  129. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/catalog/browse.html +0 -0
  130. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  131. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/scripts/patch_mediacms_bulk_facets.py +0 -0
  141. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/scripts/patch_mediacms_tags.py +0 -0
  142. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/scripts/trigger_enrich.py +0 -0
  143. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/__init__.py +0 -0
  144. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_classify.py +0 -0
  145. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_config_persistence.py +0 -0
  146. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_feedback.py +0 -0
  147. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_fetchurls_sharepoint.py +0 -0
  148. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_people_facets.py +0 -0
  149. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_phase1.py +0 -0
  150. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_phase2_jobs.py +0 -0
  151. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_phase3_jobs.py +0 -0
  152. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_phase4_live_fixes.py +0 -0
  153. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_playlist_import.py +0 -0
  154. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_presence_refund.py +0 -0
  155. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_promo_director.py +0 -0
  156. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_promo_pool_exclusion.py +0 -0
  157. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_providers.py +0 -0
  158. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_queue_announce.py +0 -0
  159. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_race_poller.py +0 -0
  160. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_recently_played_hide.py +0 -0
  161. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_save_results_to_playlist.py +0 -0
  162. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_schedule_lock.py +0 -0
  163. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_search_facets.py +0 -0
  164. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_title_normalization.py +0 -0
  165. {kryten_webqueue-0.36.5 → kryten_webqueue-0.36.6}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.36.6] - 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - **`(Dubbed)` / `(Subbed)` markers no longer break art lookup.** `(dubbed)` is
10
+ stripped from both the display title and the search query (dubbed is the
11
+ default). `(subbed)`/`(subs)`/`(subtitles)` stay in the display title but are
12
+ omitted from the metadata/art search.
13
+ - **YouTube “full movie” pipe titles are cleaned properly.** Detection now matches
14
+ non-adjacent variants like “Full **Horror** Movie”, the title is taken from the
15
+ first segment before the pipe, and the year is extracted from the whole string
16
+ (it often trails after the last pipe, e.g. “… | Larry Bucha (1961)”). Both the
17
+ search query and the display title use this cleanup consistently (previously the
18
+ display title was never tidied for pipe uploads).
19
+
5
20
  ## [0.36.5] - 2026-08-14
6
21
 
7
22
  ### 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.6
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
@@ -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.6"
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"