kryten-webqueue 0.35.8__tar.gz → 0.36.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/CHANGELOG.md +32 -0
  2. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_connection.py +4 -0
  4. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_enrichment.py +2 -1
  5. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/classify.py +33 -4
  6. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/title.py +11 -1
  7. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/css/main.css +37 -3
  8. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/catalog/item_detail.html +16 -1
  9. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/pyproject.toml +1 -1
  10. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/.github/workflows/gitleaks.yml +0 -0
  11. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/.github/workflows/python-publish.yml +0 -0
  12. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/.github/workflows/release.yml +0 -0
  13. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/.gitignore +0 -0
  14. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/.gitleaks.toml +0 -0
  15. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/.pre-commit-config.yaml +0 -0
  16. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
  17. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/README.md +0 -0
  18. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/config.example.json +0 -0
  19. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/deploy/kryten-webqueue.service +0 -0
  20. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/deploy/nginx-queue.conf +0 -0
  21. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  22. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/IMPL_API_GATE.md +0 -0
  23. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/IMPL_ECONOMY.md +0 -0
  24. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  25. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/IMPL_ROBOT.md +0 -0
  26. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  27. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  28. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  29. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  30. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/PRE_PLAN_GAPS.md +0 -0
  31. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/PRODUCT_PLAN.md +0 -0
  32. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  33. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
  34. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  35. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/docs/UX_POLISH_PLAN.md +0 -0
  36. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/__init__.py +0 -0
  37. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/__main__.py +0 -0
  38. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  39. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/api_gate/client.py +0 -0
  40. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/app.py +0 -0
  41. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/auth/__init__.py +0 -0
  42. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/auth/otp.py +0 -0
  43. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  44. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/auth/session.py +0 -0
  45. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/__init__.py +0 -0
  46. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/__init__.py +0 -0
  47. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  48. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  49. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_people.py +0 -0
  50. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  51. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  52. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  53. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  54. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  55. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  56. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  57. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  58. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  59. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  60. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  61. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  62. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  63. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  64. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/images.py +0 -0
  65. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  66. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/catalog/sync.py +0 -0
  67. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/config.py +0 -0
  68. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/__init__.py +0 -0
  69. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  70. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  71. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  72. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  73. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  74. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  75. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  76. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  77. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  78. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  79. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/jobs/__init__.py +0 -0
  80. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  81. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  82. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/jobs/manager.py +0 -0
  83. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  84. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/jobs/tasks.py +0 -0
  85. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/logging_config.py +0 -0
  86. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/playlists/__init__.py +0 -0
  87. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  88. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/playlists/fire.py +0 -0
  89. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/playlists/importer.py +0 -0
  90. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/playlists/ordering.py +0 -0
  91. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  92. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/promos/__init__.py +0 -0
  93. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/promos/director.py +0 -0
  94. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/__init__.py +0 -0
  95. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/completion.py +0 -0
  96. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/ordering.py +0 -0
  97. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/poller.py +0 -0
  98. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/presence.py +0 -0
  99. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/race_poller.py +0 -0
  100. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/queue/shadow.py +0 -0
  101. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/__init__.py +0 -0
  102. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  103. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  104. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  105. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  106. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  107. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  108. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  109. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  110. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  111. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/auth.py +0 -0
  112. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/catalog.py +0 -0
  113. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/feedback.py +0 -0
  114. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/pages.py +0 -0
  115. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/queue.py +0 -0
  116. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/routes/user.py +0 -0
  117. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  118. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  119. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  120. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  121. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/js/admin.js +0 -0
  122. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/static/js/main.js +0 -0
  123. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/admin/index.html +0 -0
  124. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  125. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  126. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  127. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  128. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/auth/login.html +0 -0
  129. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/base.html +0 -0
  130. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  131. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/__init__.py +0 -0
  141. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_classify.py +0 -0
  142. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_config_persistence.py +0 -0
  143. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_feedback.py +0 -0
  144. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_fetchurls_sharepoint.py +0 -0
  145. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_people_facets.py +0 -0
  146. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_phase1.py +0 -0
  147. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_phase2_jobs.py +0 -0
  148. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_phase3_jobs.py +0 -0
  149. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_phase4_live_fixes.py +0 -0
  150. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_playlist_import.py +0 -0
  151. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_presence_refund.py +0 -0
  152. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_promo_director.py +0 -0
  153. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_promo_pool_exclusion.py +0 -0
  154. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_providers.py +0 -0
  155. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_queue_announce.py +0 -0
  156. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_race_poller.py +0 -0
  157. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_recently_played_hide.py +0 -0
  158. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_save_results_to_playlist.py +0 -0
  159. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_schedule_lock.py +0 -0
  160. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_search_facets.py +0 -0
  161. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_title_normalization.py +0 -0
  162. {kryten_webqueue-0.35.8 → kryten_webqueue-0.36.0}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,38 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.36.0] - 2026-08-14
6
+
7
+ ### Added
8
+
9
+ - **New hosted movie shows.** Added support for "13 Nights of Elvira", "JBB" (shorthand
10
+ for Joe Bob Briggs), and "TLDI" (shorthand for The Last Drive-In).
11
+ - **Hosted movie title formatting.** Hosted movies now display as "Movie Title (YYYY) - Show Name"
12
+ (e.g., "Friday the 13th (1980) - Svengoolie"), ensuring year is always in parentheses
13
+ and show name is appended as a suffix.
14
+ - **Collapsible cast & crew in descriptions.** When person/role/studio data exists, the
15
+ "Cast & Crew" section in item descriptions is now collapsed by default to avoid
16
+ duplication. Users can expand it with a single click.
17
+ - **Improved YouTube title cleanup.** Enhanced detection of YouTube imports with pipe
18
+ characters and "full movie" patterns (e.g., "Title | Full Movie HD | Description"),
19
+ extracting the clean title before the first pipe.
20
+ - **Distinct tag styling.** Tags now display with orange/accent color borders to
21
+ differentiate them from categories (purple) and persons/studios (green/gold).
22
+
23
+ ### Fixed
24
+
25
+ - **Better Monstervision title extraction.** Monstervision titles like "Monstervision 01
26
+ Friday the 13th Part 1" now extract to clean movie titles with proper database lookups.
27
+
28
+ ## [0.35.9] - 2026-08-14
29
+
30
+ ### Fixed
31
+
32
+ - **Missing database column for categories step.** Added `last_categories_at` column
33
+ to `item_enrichment_state` table (migration v20) to support the categories enrichment
34
+ step. This fixes `OperationalError: no such column: e.last_categories_at` that blocked
35
+ enrichment jobs when the categories step was included.
36
+
5
37
  ## [0.35.8] - 2026-08-14
6
38
 
7
39
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.35.8
3
+ Version: 0.36.0
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
@@ -395,6 +395,10 @@ MIGRATIONS = [
395
395
  last_tags_at TEXT
396
396
  );
397
397
  """,
398
+ # v20: Add last_categories_at for categories enrichment step
399
+ """
400
+ ALTER TABLE item_enrichment_state ADD COLUMN last_categories_at TEXT;
401
+ """,
398
402
  ]
399
403
 
400
404
 
@@ -43,6 +43,7 @@ class _EnrichmentMixin:
43
43
  "last_meta_at",
44
44
  "last_art_at",
45
45
  "last_tags_at",
46
+ "last_categories_at",
46
47
  }
47
48
  safe = {k: v for k, v in fields.items() if k in allowed}
48
49
  if not safe:
@@ -77,7 +78,7 @@ class _EnrichmentMixin:
77
78
  e.tv_show, e.tv_season, e.tv_episode_num,
78
79
  e.description_score, e.tmdb_id, e.imdb_id, e.meta_json,
79
80
  e.last_classify_at, e.last_title_at, e.last_meta_at,
80
- e.last_art_at, e.last_tags_at
81
+ e.last_art_at, e.last_tags_at, e.last_categories_at
81
82
  FROM catalog c
82
83
  LEFT JOIN item_enrichment_state e ON e.friendly_token = c.friendly_token
83
84
  """
@@ -35,6 +35,15 @@ HOSTED_SHOW_REGISTRY: list[HostedShowEntry] = [
35
35
  None,
36
36
  "joebobbriggs",
37
37
  ),
38
+ # "JBB" shorthand for Joe Bob Briggs
39
+ HostedShowEntry(
40
+ re.compile(r"\bJBB\b", re.I),
41
+ "Joe Bob Briggs",
42
+ "hosted_movie",
43
+ "keep",
44
+ None,
45
+ "joebobbriggs",
46
+ ),
38
47
  HostedShowEntry(
39
48
  re.compile(r"JBBTLDI|Joe\s*Bob\s+TLDI", re.I),
40
49
  "The Last Drive-In with Joe Bob Briggs",
@@ -43,6 +52,15 @@ HOSTED_SHOW_REGISTRY: list[HostedShowEntry] = [
43
52
  None,
44
53
  "lastdrivein",
45
54
  ),
55
+ # "TLDI" shorthand for The Last Drive-In
56
+ HostedShowEntry(
57
+ re.compile(r"\bTLDI\b", re.I),
58
+ "The Last Drive-In with Joe Bob Briggs",
59
+ "hosted_movie",
60
+ "keep",
61
+ None,
62
+ "lastdrivein",
63
+ ),
46
64
  HostedShowEntry(
47
65
  re.compile(r"(?:The\s+)?Last\s+Drive[\s\-]*In", re.I),
48
66
  "The Last Drive-In with Joe Bob Briggs",
@@ -67,6 +85,14 @@ HOSTED_SHOW_REGISTRY: list[HostedShowEntry] = [
67
85
  None,
68
86
  "svengoolie",
69
87
  ),
88
+ HostedShowEntry(
89
+ re.compile(r"13\s+Nights?\s+of\s+Elvira", re.I),
90
+ "13 Nights of Elvira",
91
+ "hosted_movie",
92
+ "keep",
93
+ None,
94
+ "elvira",
95
+ ),
70
96
  HostedShowEntry(
71
97
  re.compile(r"Riff\s*Trax(?:\s+Live)?", re.I),
72
98
  "RiffTrax",
@@ -225,8 +251,9 @@ class ItemClassification:
225
251
 
226
252
 
227
253
  # YouTube "Title | English Full Movie | Genre1 Genre2" pattern
254
+ # Also handles variants like "Title | Full Movie HD | Description"
228
255
  _YT_FULL_MOVIE_RE = re.compile(
229
- r"^(?P<title>.+?)\s*\|\s*(?:\w+\s+)?Full\s+Movie\s*(?:\|\s*(?P<genres>.+))?$", re.I
256
+ r"^(?P<title>.+?)\s*\|.*(Full\s+(?:Movie|Film|Length|Feature)).*$", re.I
230
257
  )
231
258
 
232
259
 
@@ -299,18 +326,20 @@ def classify_item(
299
326
 
300
327
  # 4. Movie (>= 30 min)
301
328
  if duration_sec >= 1800:
329
+ # YouTube title cleanup: extract title before first pipe if "full movie" appears
302
330
  yt = _YT_FULL_MOVIE_RE.match(raw_title.strip())
303
331
  if yt:
304
332
  clean_title = yt.group("title").strip()
305
- genres = (yt.group("genres") or "").split() if yt.group("genres") else []
333
+ # Extract year from the clean title part
334
+ clean_title, year = normalize_and_clean(clean_title)
306
335
  return ItemClassification(
307
336
  friendly_token=friendly_token,
308
337
  raw_title=raw_title,
309
338
  content_type="movie",
310
339
  hosted=None,
311
340
  lookup_title=clean_title,
312
- lookup_year=None,
313
- genre_hints=genres,
341
+ lookup_year=year,
342
+ genre_hints=[],
314
343
  duration_sec=duration_sec,
315
344
  description_score=description_score,
316
345
  has_real_art=has_real_art,
@@ -89,6 +89,16 @@ class TitleStep:
89
89
  else:
90
90
  candidate = clean
91
91
  return candidate if candidate != cls.raw_title else None
92
+ if ct == "hosted_movie" and cls.hosted:
93
+ # Format as "Movie Title (YYYY) - Show Name"
94
+ t = cls.hosted.movie_title
95
+ y = cls.hosted.movie_year
96
+ show = cls.hosted.show_name
97
+ if y:
98
+ candidate = f"{t} ({y}) - {show}"
99
+ else:
100
+ candidate = f"{t} - {show}"
101
+ return candidate if candidate != cls.raw_title else None
92
102
  if ct == "riffed_movie" and cls.hosted:
93
103
  entry_template = cls.hosted # HostedInfo has show_name
94
104
  if "{title}" in (cls.hosted.show_name or ""):
@@ -100,7 +110,7 @@ class TitleStep:
100
110
  return (
101
111
  f"RiffTrax Presents: {t} ({y})" if y else f"RiffTrax Presents: {t}"
102
112
  )
103
- # hosted_movie / archive / unknown / tv_episode: no change
113
+ # archive / unknown / tv_episode: no change
104
114
  return None
105
115
 
106
116
  async def _push_title(
@@ -740,9 +740,9 @@ a.np-chip {
740
740
  border: 1px solid var(--accent);
741
741
  }
742
742
  .np-chip-tag {
743
- background: var(--bg-elevated, #2a2a38);
744
- color: var(--text-secondary);
745
- border: 1px solid var(--border);
743
+ background: rgba(232, 150, 16, 0.15);
744
+ color: var(--accent);
745
+ border: 1px solid var(--accent);
746
746
  }
747
747
  .np-chip-person {
748
748
  background: rgba(0, 168, 120, 0.15);
@@ -768,6 +768,40 @@ a.np-chip {
768
768
  font-weight: 600;
769
769
  }
770
770
 
771
+ /* Cast & Crew collapsible section in item detail */
772
+ .item-detail-cast-crew-collapse {
773
+ margin-top: 1rem;
774
+ padding: 0.5rem 0;
775
+ }
776
+ .item-detail-cast-crew-collapse summary {
777
+ cursor: pointer;
778
+ padding: 0.5rem;
779
+ background: var(--bg-secondary);
780
+ border-radius: var(--radius);
781
+ color: var(--text-secondary);
782
+ font-size: 0.9rem;
783
+ user-select: none;
784
+ list-style: none;
785
+ display: flex;
786
+ align-items: center;
787
+ }
788
+ .item-detail-cast-crew-collapse summary::-webkit-details-marker {
789
+ display: none;
790
+ }
791
+ .item-detail-cast-crew-collapse summary::before {
792
+ content: "▸";
793
+ display: inline-block;
794
+ margin-right: 0.5rem;
795
+ transition: transform 0.2s;
796
+ }
797
+ .item-detail-cast-crew-collapse[open] summary::before {
798
+ transform: rotate(90deg);
799
+ }
800
+ .item-detail-cast-crew-collapse summary:hover {
801
+ background: var(--bg-hover);
802
+ color: var(--text-primary);
803
+ }
804
+
771
805
  /* WebSocket status */
772
806
  .ws-connected {
773
807
  color: var(--success);
@@ -41,7 +41,22 @@
41
41
  </div>
42
42
 
43
43
  {% if item.description %}
44
- <p class="item-detail-description">{{ item.description }}</p>
44
+ {% set has_people_data = people.cast or people.director or people.producer or people.writer or studios %}
45
+ {% if has_people_data %}
46
+ {# Split description at "Cast & Crew" and make that section collapsible #}
47
+ {% set description_parts = item.description.split('Cast & Crew') %}
48
+ {% if description_parts|length > 1 %}
49
+ <div class="item-detail-description">{{ description_parts[0] }}</div>
50
+ <details class="item-detail-cast-crew-collapse">
51
+ <summary>Cast & Crew (from description)</summary>
52
+ <div class="item-detail-description">{{ description_parts[1] }}</div>
53
+ </details>
54
+ {% else %}
55
+ <p class="item-detail-description">{{ item.description }}</p>
56
+ {% endif %}
57
+ {% else %}
58
+ <p class="item-detail-description">{{ item.description }}</p>
59
+ {% endif %}
45
60
  {% else %}
46
61
  <p class="item-detail-description empty-state">No description available.</p>
47
62
  {% endif %}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.35.8"
3
+ version = "0.36.0"
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"