kryten-webqueue 0.35.7__tar.gz → 0.35.8__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.7 → kryten_webqueue-0.35.8}/CHANGELOG.md +20 -0
  2. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_catalog.py +38 -0
  4. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/providers.py +49 -0
  5. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/art.py +14 -1
  6. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/sync.py +8 -1
  7. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/pyproject.toml +1 -1
  8. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/.github/workflows/gitleaks.yml +0 -0
  9. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/.github/workflows/python-publish.yml +0 -0
  10. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/.github/workflows/release.yml +0 -0
  11. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/.gitignore +0 -0
  12. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/.gitleaks.toml +0 -0
  13. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/.pre-commit-config.yaml +0 -0
  14. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/AGENT-WORKFLOW-GUIDE.md +0 -0
  15. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/README.md +0 -0
  16. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/config.example.json +0 -0
  17. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/deploy/kryten-webqueue.service +0 -0
  18. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/deploy/nginx-queue.conf +0 -0
  19. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/IMPLEMENTATION_SPEC.md +0 -0
  20. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/IMPL_API_GATE.md +0 -0
  21. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/IMPL_ECONOMY.md +0 -0
  22. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/IMPL_KRYTEN_PY.md +0 -0
  23. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/IMPL_ROBOT.md +0 -0
  24. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  25. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  26. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/Log for incorrect promo insertion problem.txt +0 -0
  27. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  28. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/PRE_PLAN_GAPS.md +0 -0
  29. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/PRODUCT_PLAN.md +0 -0
  30. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  31. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/SPEC_EMOTE_REHOST.md +0 -0
  32. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  33. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/docs/UX_POLISH_PLAN.md +0 -0
  34. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/__init__.py +0 -0
  35. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/__main__.py +0 -0
  36. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/api_gate/__init__.py +0 -0
  37. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/api_gate/client.py +0 -0
  38. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/app.py +0 -0
  39. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/auth/__init__.py +0 -0
  40. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/auth/otp.py +0 -0
  41. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/auth/rate_limit.py +0 -0
  42. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/auth/session.py +0 -0
  43. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/__init__.py +0 -0
  44. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/__init__.py +0 -0
  45. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_connection.py +0 -0
  46. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  47. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  48. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_people.py +0 -0
  49. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  50. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_queue.py +0 -0
  51. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  52. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  53. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  54. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  55. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  56. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  57. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  58. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  59. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  60. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  61. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  62. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  63. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/images.py +0 -0
  64. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/catalog/mediacms.py +0 -0
  65. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/config.py +0 -0
  66. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/__init__.py +0 -0
  67. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  68. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  69. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  70. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  71. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  72. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  73. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  74. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  75. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  76. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  77. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/jobs/__init__.py +0 -0
  78. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  79. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  80. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/jobs/manager.py +0 -0
  81. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  82. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/jobs/tasks.py +0 -0
  83. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/logging_config.py +0 -0
  84. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/playlists/__init__.py +0 -0
  85. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/playlists/bulk_add.py +0 -0
  86. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/playlists/fire.py +0 -0
  87. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/playlists/importer.py +0 -0
  88. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/playlists/ordering.py +0 -0
  89. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/playlists/scheduler.py +0 -0
  90. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/promos/__init__.py +0 -0
  91. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/promos/director.py +0 -0
  92. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/__init__.py +0 -0
  93. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/completion.py +0 -0
  94. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/ordering.py +0 -0
  95. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/poller.py +0 -0
  96. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/presence.py +0 -0
  97. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/race_poller.py +0 -0
  98. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/queue/shadow.py +0 -0
  99. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/__init__.py +0 -0
  100. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_catalog.py +0 -0
  101. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_feedback.py +0 -0
  102. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  103. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_jobs.py +0 -0
  104. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_moderation.py +0 -0
  105. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_playlists.py +0 -0
  106. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_promos.py +0 -0
  107. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_queue.py +0 -0
  108. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/admin_schedules.py +0 -0
  109. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/auth.py +0 -0
  110. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/catalog.py +0 -0
  111. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/feedback.py +0 -0
  112. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/pages.py +0 -0
  113. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/queue.py +0 -0
  114. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/routes/user.py +0 -0
  115. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/css/main.css +0 -0
  116. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  117. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/js/admin-promos.js +0 -0
  118. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  119. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  120. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/js/admin.js +0 -0
  121. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/static/js/main.js +0 -0
  122. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/admin/index.html +0 -0
  123. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/admin/playlists.html +0 -0
  124. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/admin/promos.html +0 -0
  125. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  126. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/admin/schedules.html +0 -0
  127. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/auth/login.html +0 -0
  128. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/base.html +0 -0
  129. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/catalog/browse.html +0 -0
  130. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  131. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/__init__.py +0 -0
  141. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_classify.py +0 -0
  142. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_config_persistence.py +0 -0
  143. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_feedback.py +0 -0
  144. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_fetchurls_sharepoint.py +0 -0
  145. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_people_facets.py +0 -0
  146. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_phase1.py +0 -0
  147. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_phase2_jobs.py +0 -0
  148. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_phase3_jobs.py +0 -0
  149. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_phase4_live_fixes.py +0 -0
  150. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_playlist_import.py +0 -0
  151. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_presence_refund.py +0 -0
  152. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_promo_director.py +0 -0
  153. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_promo_pool_exclusion.py +0 -0
  154. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_providers.py +0 -0
  155. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_queue_announce.py +0 -0
  156. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_race_poller.py +0 -0
  157. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_recently_played_hide.py +0 -0
  158. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_save_results_to_playlist.py +0 -0
  159. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_schedule_lock.py +0 -0
  160. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_search_facets.py +0 -0
  161. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_title_normalization.py +0 -0
  162. {kryten_webqueue-0.35.7 → kryten_webqueue-0.35.8}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.35.8] - 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - **TV show poster art preservation.** Fixed art enrichment step to use TV show
10
+ searches instead of movie searches for TV episodes. Previously, searching for
11
+ "The X-Files" as a movie would find the 1998 X-Files movie poster instead of
12
+ the TV series poster, causing ~90% of TV show posters to be replaced with
13
+ incorrect movie posters or wiped out entirely. Now uses `search_tv_show()` for
14
+ TV content to get the correct series poster art.
15
+ - **Deleted items removed from catalog.** Catalog sync now tracks and removes
16
+ items that have been deleted from MediaCMS. Items with `synced_at` timestamps
17
+ older than the sync start time are identified as deleted and removed from the
18
+ catalog along with their category, tag, people, and studio associations.
19
+
20
+ ### Added
21
+
22
+ - **TMDBProvider.search_tv_show() and OMDBProvider.search_tv_show().** New methods
23
+ to search for TV series and return show posters (for use with TV episode items).
24
+
5
25
  ## [0.35.7] - 2026-08-14
6
26
 
7
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.35.7
3
+ Version: 0.35.8
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
@@ -848,6 +848,44 @@ class _CatalogMixin:
848
848
  [path, source, friendly_token],
849
849
  )
850
850
 
851
+ async def delete_stale_catalog_items(self, sync_started_at: str) -> int:
852
+ """Delete catalog items that weren't updated during the sync.
853
+
854
+ Items with synced_at older than sync_started_at are no longer present
855
+ in MediaCMS and should be removed. Returns count of deleted items.
856
+
857
+ Foreign key CASCADE will automatically remove related entries from:
858
+ - catalog_fts
859
+ - catalog_categories
860
+ - catalog_tags
861
+ - catalog_people
862
+ - catalog_studios
863
+ """
864
+ cursor = await self._db.execute(
865
+ "SELECT friendly_token FROM catalog WHERE synced_at < ? OR synced_at IS NULL",
866
+ [sync_started_at],
867
+ )
868
+ tokens = [row[0] for row in await cursor.fetchall()]
869
+
870
+ if not tokens:
871
+ return 0
872
+
873
+ # Delete from FTS first (not automatically cascaded)
874
+ placeholders = ",".join("?" * len(tokens))
875
+ await self._db.execute(
876
+ f"DELETE FROM catalog_fts WHERE friendly_token IN ({placeholders})",
877
+ tokens,
878
+ )
879
+
880
+ # Delete from catalog (cascades to join tables via ON DELETE CASCADE)
881
+ await self._db.execute(
882
+ f"DELETE FROM catalog WHERE friendly_token IN ({placeholders})",
883
+ tokens,
884
+ )
885
+
886
+ await self._db.commit()
887
+ return len(tokens)
888
+
851
889
  async def find_catalog_by_title(self, title: str) -> dict | None:
852
890
  """Best-effort lookup of a catalog item whose title matches ``title``.
853
891
 
@@ -149,6 +149,36 @@ class TMDBProvider:
149
149
  logger.debug("TMDB search error for %r: %s", title, exc)
150
150
  return MovieMetadata()
151
151
 
152
+ async def search_tv_show(self, show: str) -> MovieMetadata:
153
+ """Search for a TV show and return its poster (for episode art)."""
154
+ if not self._key:
155
+ return MovieMetadata()
156
+ headers, auth = _tmdb_auth(self._key)
157
+ try:
158
+ resp = await self._client.get(
159
+ f"{TMDB_BASE}/search/tv",
160
+ headers=headers,
161
+ params={"query": show, **auth},
162
+ )
163
+ if resp.status_code != 200:
164
+ return MovieMetadata()
165
+ results = resp.json().get("results", [])
166
+ if not results:
167
+ return MovieMetadata()
168
+ # Pick the best match by popularity
169
+ tv_show = results[0]
170
+ meta = MovieMetadata()
171
+ meta.title = tv_show.get("name", "")
172
+ meta.synopsis = tv_show.get("overview", "")
173
+ first_air = tv_show.get("first_air_date", "")
174
+ meta.year = first_air[:4] if first_air else None
175
+ if tv_show.get("poster_path"):
176
+ meta.poster_url = f"https://image.tmdb.org/t/p/w780{tv_show['poster_path']}"
177
+ return meta
178
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
179
+ logger.debug("TMDB TV show search error for %r: %s", show, exc)
180
+ return MovieMetadata()
181
+
152
182
  async def search_tv_episode(
153
183
  self, show: str, season: int, episode: int
154
184
  ) -> MovieMetadata:
@@ -311,6 +341,25 @@ class OMDBProvider:
311
341
  logger.debug("OMDB error for %r: %s", title, exc)
312
342
  return MovieMetadata()
313
343
 
344
+ async def search_tv_show(self, show: str) -> MovieMetadata:
345
+ """Search for a TV show and return its poster (for episode art)."""
346
+ if not self._key:
347
+ return MovieMetadata()
348
+ try:
349
+ resp = await self._client.get(
350
+ OMDB_BASE,
351
+ params={
352
+ "apikey": self._key,
353
+ "t": show,
354
+ "type": "series",
355
+ },
356
+ )
357
+ if resp.status_code == 200 and resp.json().get("Response") == "True":
358
+ return self._parse(resp.json())
359
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
360
+ pass
361
+ return MovieMetadata()
362
+
314
363
  async def search_tv_episode(
315
364
  self, show: str, season: int, episode: int
316
365
  ) -> MovieMetadata:
@@ -114,9 +114,22 @@ class ArtStep:
114
114
  return (meta or {}).get("poster_url")
115
115
 
116
116
  async def _resolve_poster(self, cls: ItemClassification) -> str | None:
117
- """Query TMDB then OMDB using lookup_title (the key fix for hosted movies)."""
117
+ """Query TMDB then OMDB using lookup_title (the key fix for hosted movies).
118
+
119
+ For TV episodes, search for the show poster (not episode-specific art).
120
+ """
118
121
  if not cls.lookup_title:
119
122
  return None
123
+
124
+ # TV episodes: get the show poster
125
+ if cls.content_type == "tv_episode":
126
+ meta = await self._tmdb.search_tv_show(cls.lookup_title)
127
+ if meta.poster_url:
128
+ return meta.poster_url
129
+ meta = await self._omdb.search_tv_show(cls.lookup_title)
130
+ return meta.poster_url
131
+
132
+ # Movies and other content: search as movie
120
133
  meta = await self._tmdb.search_movie(cls.lookup_title, cls.lookup_year)
121
134
  if meta.poster_url:
122
135
  return meta.poster_url
@@ -57,7 +57,8 @@ class CatalogSync:
57
57
  async def sync(self):
58
58
  """Full catalog sync from MediaCMS API."""
59
59
  log_id = await self._db.start_sync_log()
60
- stats = {"seen": 0, "new": 0, "updated": 0, "errors": 0}
60
+ stats = {"seen": 0, "new": 0, "updated": 0, "errors": 0, "deleted": 0}
61
+ sync_started_at = datetime.now(UTC).isoformat()
61
62
 
62
63
  try:
63
64
  # /manage_media returns all items (9k+); /media is capped at 1000
@@ -106,6 +107,12 @@ class CatalogSync:
106
107
  f"Catalog sync page {page}: seen={stats['seen']} next={next_url!r}"
107
108
  )
108
109
 
110
+ # Remove items deleted from MediaCMS (not seen during this sync)
111
+ deleted_count = await self._db.delete_stale_catalog_items(sync_started_at)
112
+ stats["deleted"] = deleted_count
113
+ if deleted_count > 0:
114
+ logger.info(f"Removed {deleted_count} items deleted from MediaCMS")
115
+
109
116
  await self._db.finish_sync_log(log_id, stats, "completed")
110
117
  logger.info(f"Catalog sync complete: {stats} ({page} pages)")
111
118
  except asyncio.CancelledError:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.35.7"
3
+ version = "0.35.8"
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"