kryten-webqueue 0.35.3__tar.gz → 0.35.5__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.3 → kryten_webqueue-0.35.5}/CHANGELOG.md +23 -0
  2. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_catalog.py +47 -18
  4. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_enrichment.py +8 -1
  5. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/providers.py +7 -6
  6. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/meta.py +31 -15
  7. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/tags.py +34 -27
  8. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/title.py +2 -1
  9. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/pyproject.toml +1 -1
  10. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/.github/workflows/gitleaks.yml +0 -0
  11. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/.github/workflows/python-publish.yml +0 -0
  12. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/.github/workflows/release.yml +0 -0
  13. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/.gitignore +0 -0
  14. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/.gitleaks.toml +0 -0
  15. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/.pre-commit-config.yaml +0 -0
  16. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/AGENT-WORKFLOW-GUIDE.md +0 -0
  17. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/README.md +0 -0
  18. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/config.example.json +0 -0
  19. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/deploy/kryten-webqueue.service +0 -0
  20. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/deploy/nginx-queue.conf +0 -0
  21. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/IMPLEMENTATION_SPEC.md +0 -0
  22. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/IMPL_API_GATE.md +0 -0
  23. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/IMPL_ECONOMY.md +0 -0
  24. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/IMPL_KRYTEN_PY.md +0 -0
  25. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/IMPL_ROBOT.md +0 -0
  26. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  27. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  28. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/Log for incorrect promo insertion problem.txt +0 -0
  29. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  30. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/PRE_PLAN_GAPS.md +0 -0
  31. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/PRODUCT_PLAN.md +0 -0
  32. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  33. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/SPEC_EMOTE_REHOST.md +0 -0
  34. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  35. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/docs/UX_POLISH_PLAN.md +0 -0
  36. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/__init__.py +0 -0
  37. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/__main__.py +0 -0
  38. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/api_gate/__init__.py +0 -0
  39. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/api_gate/client.py +0 -0
  40. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/app.py +0 -0
  41. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/auth/__init__.py +0 -0
  42. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/auth/otp.py +0 -0
  43. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/auth/rate_limit.py +0 -0
  44. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/auth/session.py +0 -0
  45. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/__init__.py +0 -0
  46. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/__init__.py +0 -0
  47. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_connection.py +0 -0
  48. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  49. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_people.py +0 -0
  50. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  51. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_queue.py +0 -0
  52. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  53. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  54. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  55. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  56. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  57. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  58. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  59. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  60. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  61. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  62. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/images.py +0 -0
  63. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/mediacms.py +0 -0
  64. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/catalog/sync.py +0 -0
  65. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/config.py +0 -0
  66. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/__init__.py +0 -0
  67. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  68. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  69. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  70. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  71. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  72. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  73. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  74. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  75. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  76. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  77. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/jobs/__init__.py +0 -0
  78. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  79. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  80. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/jobs/manager.py +0 -0
  81. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  82. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/jobs/tasks.py +0 -0
  83. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/logging_config.py +0 -0
  84. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/playlists/__init__.py +0 -0
  85. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/playlists/bulk_add.py +0 -0
  86. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/playlists/fire.py +0 -0
  87. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/playlists/importer.py +0 -0
  88. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/playlists/ordering.py +0 -0
  89. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/playlists/scheduler.py +0 -0
  90. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/promos/__init__.py +0 -0
  91. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/promos/director.py +0 -0
  92. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/__init__.py +0 -0
  93. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/completion.py +0 -0
  94. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/ordering.py +0 -0
  95. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/poller.py +0 -0
  96. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/presence.py +0 -0
  97. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/race_poller.py +0 -0
  98. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/queue/shadow.py +0 -0
  99. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/__init__.py +0 -0
  100. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_catalog.py +0 -0
  101. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_feedback.py +0 -0
  102. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  103. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_jobs.py +0 -0
  104. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_moderation.py +0 -0
  105. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_playlists.py +0 -0
  106. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_promos.py +0 -0
  107. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_queue.py +0 -0
  108. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/admin_schedules.py +0 -0
  109. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/auth.py +0 -0
  110. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/catalog.py +0 -0
  111. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/feedback.py +0 -0
  112. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/pages.py +0 -0
  113. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/queue.py +0 -0
  114. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/routes/user.py +0 -0
  115. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/css/main.css +0 -0
  116. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  117. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/js/admin-promos.js +0 -0
  118. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  119. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  120. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/js/admin.js +0 -0
  121. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/static/js/main.js +0 -0
  122. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/admin/index.html +0 -0
  123. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/admin/playlists.html +0 -0
  124. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/admin/promos.html +0 -0
  125. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  126. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/admin/schedules.html +0 -0
  127. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/auth/login.html +0 -0
  128. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/base.html +0 -0
  129. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/catalog/browse.html +0 -0
  130. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  131. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/__init__.py +0 -0
  141. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_classify.py +0 -0
  142. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_config_persistence.py +0 -0
  143. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_feedback.py +0 -0
  144. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_fetchurls_sharepoint.py +0 -0
  145. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_people_facets.py +0 -0
  146. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_phase1.py +0 -0
  147. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_phase2_jobs.py +0 -0
  148. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_phase3_jobs.py +0 -0
  149. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_phase4_live_fixes.py +0 -0
  150. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_playlist_import.py +0 -0
  151. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_presence_refund.py +0 -0
  152. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_promo_director.py +0 -0
  153. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_promo_pool_exclusion.py +0 -0
  154. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_providers.py +0 -0
  155. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_queue_announce.py +0 -0
  156. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_race_poller.py +0 -0
  157. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_recently_played_hide.py +0 -0
  158. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_save_results_to_playlist.py +0 -0
  159. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_schedule_lock.py +0 -0
  160. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_search_facets.py +0 -0
  161. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_title_normalization.py +0 -0
  162. {kryten_webqueue-0.35.3 → kryten_webqueue-0.35.5}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.35.5] - 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - **SQL binding parameter error in enrichment pipeline.** Fixed `update_catalog`
10
+ method to support partial updates (updating only specific fields like title
11
+ or description without requiring all fields). Previously, the method required
12
+ all catalog fields to be present, causing "You did not supply a value for
13
+ binding parameter" errors when the meta and title enrichment steps attempted
14
+ partial updates. Now builds SQL dynamically based on provided fields.
15
+
16
+ ## [0.35.4] - 2026-08-13
17
+
18
+ ### Fixed
19
+
20
+ - **JSON decode error handling in enrichment pipeline.** All API calls to TMDB,
21
+ OMDB, and MediaCMS now catch `json.JSONDecodeError` in addition to HTTP errors,
22
+ preventing malformed responses from crashing enrichment jobs.
23
+ - Added error handling to `TMDBProvider` search methods
24
+ - Added error handling to `OMDBProvider` search methods
25
+ - Added error handling to CMS API calls in meta, title, and tags steps
26
+ - Enhanced `parse_meta_json` logging to capture malformed JSON for debugging
27
+
5
28
  ## [0.35.3] - 2026-08-13
6
29
 
7
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.35.3
3
+ Version: 0.35.5
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
@@ -792,25 +792,54 @@ class _CatalogMixin:
792
792
  await self._db.commit()
793
793
 
794
794
  async def update_catalog(self, friendly_token: str, row: dict):
795
- sql = """
796
- UPDATE catalog SET title=:title, description=:description,
797
- duration_sec=:duration_sec, manifest_url=:manifest_url,
798
- thumbnail_url=:thumbnail_url,
799
- added_at=COALESCE(:added_at, added_at),
800
- synced_at=:synced_at, updated_at=:synced_at
801
- WHERE friendly_token=:friendly_token
795
+ """Update catalog item with partial or full field set.
796
+
797
+ Only fields present in *row* are updated. Always adds friendly_token
798
+ and updates updated_at to current time when any field changes.
802
799
  """
803
- row = {"added_at": None, **row}
804
- await self._db.execute(sql, row)
805
- # Rebuild FTS for this row
806
- await self._db.execute(
807
- "DELETE FROM catalog_fts WHERE friendly_token = ?", [friendly_token]
808
- )
809
- await self._db.execute(
810
- "INSERT INTO catalog_fts(rowid, friendly_token, title, description) "
811
- "SELECT rowid, friendly_token, title, description FROM catalog WHERE friendly_token = ?",
812
- [friendly_token],
813
- )
800
+ if not row:
801
+ return
802
+
803
+ # Build SET clause from provided fields
804
+ allowed = {
805
+ "title", "description", "duration_sec", "manifest_url",
806
+ "thumbnail_url", "added_at", "synced_at"
807
+ }
808
+ updates = []
809
+ params = {"friendly_token": friendly_token}
810
+
811
+ for key in row:
812
+ if key in allowed:
813
+ # added_at uses COALESCE to preserve existing if None
814
+ if key == "added_at":
815
+ updates.append("added_at=COALESCE(:added_at, added_at)")
816
+ else:
817
+ updates.append(f"{key}=:{key}")
818
+ params[key] = row[key]
819
+
820
+ # Always update updated_at when modifying
821
+ updates.append("updated_at=:updated_at")
822
+ params["updated_at"] = row.get("synced_at") or params.get("synced_at")
823
+ if not params["updated_at"]:
824
+ from datetime import datetime, UTC
825
+ params["updated_at"] = datetime.now(UTC).isoformat()
826
+
827
+ if not updates:
828
+ return
829
+
830
+ sql = f"UPDATE catalog SET {', '.join(updates)} WHERE friendly_token=:friendly_token"
831
+ await self._db.execute(sql, params)
832
+
833
+ # Rebuild FTS for this row if title or description changed
834
+ if "title" in row or "description" in row:
835
+ await self._db.execute(
836
+ "DELETE FROM catalog_fts WHERE friendly_token = ?", [friendly_token]
837
+ )
838
+ await self._db.execute(
839
+ "INSERT INTO catalog_fts(rowid, friendly_token, title, description) "
840
+ "SELECT rowid, friendly_token, title, description FROM catalog WHERE friendly_token = ?",
841
+ [friendly_token],
842
+ )
814
843
  await self._db.commit()
815
844
 
816
845
  async def update_cover_art(self, friendly_token: str, path: str, source: str):
@@ -110,5 +110,12 @@ class _EnrichmentMixin:
110
110
  return None
111
111
  try:
112
112
  return json.loads(raw)
113
- except (ValueError, TypeError):
113
+ except (ValueError, TypeError) as exc:
114
+ import logging
115
+ logger = logging.getLogger(__name__)
116
+ token = (row or {}).get("friendly_token", "?")
117
+ logger.warning(
118
+ "Failed to parse meta_json for %s: %s. Raw value (first 200 chars): %r",
119
+ token, exc, raw[:200] if isinstance(raw, str) else raw
120
+ )
114
121
  return None
@@ -8,6 +8,7 @@ includes poster_url so art and meta steps can share a single provider call.
8
8
  from __future__ import annotations
9
9
 
10
10
  import difflib
11
+ import json
11
12
  import logging
12
13
  import re
13
14
  from dataclasses import dataclass, field
@@ -144,7 +145,7 @@ class TMDBProvider:
144
145
  movie = self._pick(resp2.json().get("results", []), title, None)
145
146
  if movie:
146
147
  return await self._fetch_details(movie, headers, auth)
147
- except httpx.HTTPError as exc:
148
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
148
149
  logger.debug("TMDB search error for %r: %s", title, exc)
149
150
  return MovieMetadata()
150
151
 
@@ -180,7 +181,7 @@ class TMDBProvider:
180
181
  meta.year = air_date[:4] if air_date else None
181
182
  meta.cast = [c["name"] for c in data.get("guest_stars", [])[:8]]
182
183
  return meta
183
- except httpx.HTTPError as exc:
184
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
184
185
  logger.debug(
185
186
  "TMDB TV search error for %r S%02dE%02d: %s", show, season, episode, exc
186
187
  )
@@ -226,7 +227,7 @@ class TMDBProvider:
226
227
  meta.tmdb_rating = f"{vote:.1f}/10"
227
228
  meta.imdb_id = d.get("imdb_id", "")
228
229
  meta.studio = [c["name"] for c in d.get("production_companies", [])[:3]]
229
- except httpx.HTTPError:
230
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
230
231
  pass
231
232
 
232
233
  try:
@@ -253,7 +254,7 @@ class TMDBProvider:
253
254
  x["name"] for x in crew if x.get("job") == "Original Music Composer"
254
255
  ][:3]
255
256
  meta.editor = [x["name"] for x in crew if x.get("job") == "Editor"][:3]
256
- except httpx.HTTPError:
257
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
257
258
  pass
258
259
 
259
260
  return meta
@@ -306,7 +307,7 @@ class OMDBProvider:
306
307
  ):
307
308
  return MovieMetadata()
308
309
  return self._parse(data)
309
- except httpx.HTTPError as exc:
310
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
310
311
  logger.debug("OMDB error for %r: %s", title, exc)
311
312
  return MovieMetadata()
312
313
 
@@ -327,7 +328,7 @@ class OMDBProvider:
327
328
  )
328
329
  if resp.status_code == 200 and resp.json().get("Response") == "True":
329
330
  return self._parse(resp.json())
330
- except httpx.HTTPError:
331
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
331
332
  pass
332
333
  return MovieMetadata()
333
334
 
@@ -154,6 +154,19 @@ class MetaStep:
154
154
  new_score = score_description(desc)
155
155
  meta_json = json.dumps(self._meta_to_dict(meta))
156
156
 
157
+ # Validate round-trip before saving
158
+ try:
159
+ json.loads(meta_json)
160
+ except (ValueError, TypeError) as e:
161
+ logger.error(
162
+ "[meta] %s: generated malformed meta_json: %s. First 300 chars: %r",
163
+ cls.friendly_token,
164
+ e,
165
+ meta_json[:300],
166
+ )
167
+ result.record_error(f"{cls.friendly_token}: malformed meta_json ({e})")
168
+ continue
169
+
157
170
  logger.info(
158
171
  "[meta] %s: %r → score %d",
159
172
  cls.friendly_token,
@@ -206,21 +219,24 @@ class MetaStep:
206
219
 
207
220
  async def _push_description(self, token: str, desc: str) -> None:
208
221
  url = f"{self._base}/api/v1/media/{token}"
209
- async with httpx.AsyncClient(
210
- headers=self._cms_headers, timeout=_TIMEOUT
211
- ) as client:
212
- resp = await client.get(url)
213
- owner = resp.json().get("user") if resp.status_code == 200 else None
214
- await client.put(url, data={"description": desc})
215
- if owner:
216
- await client.post(
217
- f"{self._base}/api/v1/media/user/bulk_actions",
218
- json={
219
- "action": "change_owner",
220
- "media_ids": [token],
221
- "owner": owner,
222
- },
223
- )
222
+ try:
223
+ async with httpx.AsyncClient(
224
+ headers=self._cms_headers, timeout=_TIMEOUT
225
+ ) as client:
226
+ resp = await client.get(url)
227
+ owner = resp.json().get("user") if resp.status_code == 200 else None
228
+ await client.put(url, data={"description": desc})
229
+ if owner:
230
+ await client.post(
231
+ f"{self._base}/api/v1/media/user/bulk_actions",
232
+ json={
233
+ "action": "change_owner",
234
+ "media_ids": [token],
235
+ "owner": owner,
236
+ },
237
+ )
238
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
239
+ logger.warning("[meta] Failed to push description for %s: %s", token, exc)
224
240
 
225
241
  async def _write_people(self, token: str, meta: MovieMetadata) -> None:
226
242
  people: list[dict] = []
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import logging
6
7
  import re
7
8
  from datetime import datetime, UTC
@@ -108,35 +109,41 @@ class TagsStep:
108
109
  self, client: httpx.AsyncClient, token: str, new_tags: list[str]
109
110
  ) -> bool:
110
111
  url = f"{self._base}/api/v1/media/{token}"
111
- resp = await client.get(url)
112
- if resp.status_code != 200:
112
+ try:
113
+ resp = await client.get(url)
114
+ if resp.status_code != 200:
115
+ return False
116
+ data = resp.json()
117
+ owner = data.get("user")
118
+ current = {
119
+ (t.get("title") if isinstance(t, dict) else t)
120
+ for t in (data.get("tags_info") or [])
121
+ }
122
+ to_add = [t for t in new_tags if t not in current]
123
+ if not to_add:
124
+ return False
125
+ merged = list(current | set(to_add))
126
+ put = await client.put(url, data={"tags": ",".join(merged)})
127
+ if put.status_code in (200, 201) and owner:
128
+ await client.post(
129
+ f"{self._base}/api/v1/media/user/bulk_actions",
130
+ json={"action": "change_owner", "media_ids": [token], "owner": owner},
131
+ )
132
+ return put.status_code in (200, 201)
133
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
113
134
  return False
114
- data = resp.json()
115
- owner = data.get("user")
116
- current = {
117
- (t.get("title") if isinstance(t, dict) else t)
118
- for t in (data.get("tags_info") or [])
119
- }
120
- to_add = [t for t in new_tags if t not in current]
121
- if not to_add:
122
- return False
123
- merged = list(current | set(to_add))
124
- put = await client.put(url, data={"tags": ",".join(merged)})
125
- if put.status_code in (200, 201) and owner:
126
- await client.post(
127
- f"{self._base}/api/v1/media/user/bulk_actions",
128
- json={"action": "change_owner", "media_ids": [token], "owner": owner},
129
- )
130
- return put.status_code in (200, 201)
131
135
 
132
136
  async def _reverse_sync(self, client: httpx.AsyncClient, token: str) -> None:
133
137
  """Insert any CMS-only tags into the local catalog_tags table."""
134
138
  url = f"{self._base}/api/v1/media/{token}"
135
- resp = await client.get(url)
136
- if resp.status_code != 200:
137
- return
138
- data = resp.json()
139
- for t in data.get("tags_info") or []:
140
- name = t.get("title") if isinstance(t, dict) else t
141
- if name:
142
- await self._db.add_catalog_tag(token, str(name))
139
+ try:
140
+ resp = await client.get(url)
141
+ if resp.status_code != 200:
142
+ return
143
+ data = resp.json()
144
+ for t in data.get("tags_info") or []:
145
+ name = t.get("title") if isinstance(t, dict) else t
146
+ if name:
147
+ await self._db.add_catalog_tag(token, str(name))
148
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
149
+ logger.warning("[tags] Failed reverse-sync for %s: %s", token, exc)
@@ -1,5 +1,6 @@
1
1
  """Step: title — normalise and reformat CMS titles per content type."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from datetime import datetime, UTC
5
6
 
@@ -122,5 +123,5 @@ class TitleStep:
122
123
  },
123
124
  )
124
125
  return ok
125
- except httpx.HTTPError:
126
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
126
127
  return False
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.35.3"
3
+ version = "0.35.5"
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"