kryten-webqueue 0.36.0__tar.gz → 0.36.2__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.36.0 → kryten_webqueue-0.36.2}/CHANGELOG.md +30 -0
  2. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/tags.py +53 -12
  4. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/catalog/browse.html +7 -0
  5. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/pyproject.toml +1 -1
  6. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/.github/workflows/gitleaks.yml +0 -0
  7. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/.github/workflows/python-publish.yml +0 -0
  8. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/.github/workflows/release.yml +0 -0
  9. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/.gitignore +0 -0
  10. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/.gitleaks.toml +0 -0
  11. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/.pre-commit-config.yaml +0 -0
  12. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/AGENT-WORKFLOW-GUIDE.md +0 -0
  13. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/README.md +0 -0
  14. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/config.example.json +0 -0
  15. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/deploy/kryten-webqueue.service +0 -0
  16. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/deploy/nginx-queue.conf +0 -0
  17. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/IMPLEMENTATION_SPEC.md +0 -0
  18. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/IMPL_API_GATE.md +0 -0
  19. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/IMPL_ECONOMY.md +0 -0
  20. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/IMPL_KRYTEN_PY.md +0 -0
  21. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/IMPL_ROBOT.md +0 -0
  22. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  23. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  24. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/Log for incorrect promo insertion problem.txt +0 -0
  25. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  26. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/PRE_PLAN_GAPS.md +0 -0
  27. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/PRODUCT_PLAN.md +0 -0
  28. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  29. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/SPEC_EMOTE_REHOST.md +0 -0
  30. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  31. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/docs/UX_POLISH_PLAN.md +0 -0
  32. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/__init__.py +0 -0
  33. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/__main__.py +0 -0
  34. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/api_gate/__init__.py +0 -0
  35. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/api_gate/client.py +0 -0
  36. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/app.py +0 -0
  37. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/auth/__init__.py +0 -0
  38. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/auth/otp.py +0 -0
  39. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/auth/rate_limit.py +0 -0
  40. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/auth/session.py +0 -0
  41. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/__init__.py +0 -0
  42. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/__init__.py +0 -0
  43. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  44. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_connection.py +0 -0
  45. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  46. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  47. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_people.py +0 -0
  48. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  49. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_queue.py +0 -0
  50. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  51. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  52. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  53. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  54. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  55. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  56. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  57. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  58. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  59. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  60. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  61. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  62. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  63. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/images.py +0 -0
  64. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/mediacms.py +0 -0
  65. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/catalog/sync.py +0 -0
  66. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/config.py +0 -0
  67. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/__init__.py +0 -0
  68. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  69. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  70. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  71. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  72. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  73. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  74. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  75. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  76. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  77. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  78. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/jobs/__init__.py +0 -0
  79. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  80. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  81. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/jobs/manager.py +0 -0
  82. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  83. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/jobs/tasks.py +0 -0
  84. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/logging_config.py +0 -0
  85. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/playlists/__init__.py +0 -0
  86. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/playlists/bulk_add.py +0 -0
  87. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/playlists/fire.py +0 -0
  88. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/playlists/importer.py +0 -0
  89. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/playlists/ordering.py +0 -0
  90. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/playlists/scheduler.py +0 -0
  91. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/promos/__init__.py +0 -0
  92. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/promos/director.py +0 -0
  93. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/__init__.py +0 -0
  94. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/completion.py +0 -0
  95. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/ordering.py +0 -0
  96. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/poller.py +0 -0
  97. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/presence.py +0 -0
  98. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/race_poller.py +0 -0
  99. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/queue/shadow.py +0 -0
  100. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/__init__.py +0 -0
  101. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_catalog.py +0 -0
  102. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_feedback.py +0 -0
  103. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  104. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_jobs.py +0 -0
  105. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_moderation.py +0 -0
  106. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_playlists.py +0 -0
  107. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_promos.py +0 -0
  108. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_queue.py +0 -0
  109. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/admin_schedules.py +0 -0
  110. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/auth.py +0 -0
  111. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/catalog.py +0 -0
  112. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/feedback.py +0 -0
  113. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/pages.py +0 -0
  114. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/queue.py +0 -0
  115. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/routes/user.py +0 -0
  116. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/css/main.css +0 -0
  117. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  118. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/js/admin-promos.js +0 -0
  119. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  120. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  121. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/js/admin.js +0 -0
  122. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/static/js/main.js +0 -0
  123. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/admin/index.html +0 -0
  124. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/admin/playlists.html +0 -0
  125. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/admin/promos.html +0 -0
  126. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  127. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/admin/schedules.html +0 -0
  128. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/auth/login.html +0 -0
  129. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/base.html +0 -0
  130. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  131. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/__init__.py +0 -0
  141. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_classify.py +0 -0
  142. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_config_persistence.py +0 -0
  143. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_feedback.py +0 -0
  144. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_fetchurls_sharepoint.py +0 -0
  145. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_people_facets.py +0 -0
  146. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_phase1.py +0 -0
  147. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_phase2_jobs.py +0 -0
  148. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_phase3_jobs.py +0 -0
  149. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_phase4_live_fixes.py +0 -0
  150. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_playlist_import.py +0 -0
  151. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_presence_refund.py +0 -0
  152. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_promo_director.py +0 -0
  153. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_promo_pool_exclusion.py +0 -0
  154. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_providers.py +0 -0
  155. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_queue_announce.py +0 -0
  156. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_race_poller.py +0 -0
  157. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_recently_played_hide.py +0 -0
  158. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_save_results_to_playlist.py +0 -0
  159. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_schedule_lock.py +0 -0
  160. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_search_facets.py +0 -0
  161. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_title_normalization.py +0 -0
  162. {kryten_webqueue-0.36.0 → kryten_webqueue-0.36.2}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.36.2] - 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - **Stop flooding MediaCMS with futile tag-push 400s.** The tags step attempted
10
+ `POST /media/user/bulk_actions` (`add_tags`) for every enriched item, but that
11
+ endpoint filters `Media.objects.filter(user=request.user, ...)` with no manager
12
+ or superuser bypass, so pushes against media owned by other users always return
13
+ 400 "No matching media found". The step now looks up the API token's username
14
+ once via `/whoami` and only attempts the CMS push for media that user actually
15
+ owns. Local `catalog_tags` remains the source of truth for derived
16
+ genre/MPAA/hosted tags, so tags still display regardless.
17
+
18
+ ## [0.36.1] - 2026-08-14
19
+
20
+ ### Fixed
21
+
22
+ - **Tags now appear on item detail pages.** The tags enrichment step pushed tags to
23
+ MediaCMS via `PUT /media/{token}`, but that endpoint only accepts title/description
24
+ and silently ignores a `tags` field — so tags never landed in CMS and thus never
25
+ reverse-synced into the local catalog. Tag pushes now use the correct
26
+ `POST /media/user/bulk_actions` (`action: add_tags`) endpoint, and computed
27
+ genre/MPAA/hosted-show tags are also written directly to the local `catalog_tags`
28
+ table so they display regardless of CMS write success.
29
+ - **Person and Studio facet links now work.** Clicking a person or studio chip on an
30
+ item detail page dropped the filter and landed on the unfiltered browse page whenever
31
+ the user had a saved sort or hide-short preference: the `applyFacets()` redirect
32
+ rebuilt the URL from the dropdowns only and discarded the `person`/`studio` query
33
+ params. It now preserves them.
34
+
5
35
  ## [0.36.0] - 2026-08-14
6
36
 
7
37
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.36.0
3
+ Version: 0.36.2
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
@@ -42,6 +42,22 @@ class TagsStep:
42
42
  )
43
43
  self._headers = {"Authorization": f"Token {config.mediacms_token}"}
44
44
 
45
+ async def _get_api_username(self, client: httpx.AsyncClient) -> str | None:
46
+ """Return the API token's username, or None.
47
+
48
+ MediaCMS ``/media/user/bulk_actions`` filters ``Media.objects.filter(
49
+ user=request.user, ...)`` with no manager/superuser bypass, so tag
50
+ pushes only succeed on media this user owns. We use this to skip futile
51
+ pushes against media owned by others instead of flooding CMS with 400s.
52
+ """
53
+ try:
54
+ resp = await client.get(f"{self._base}/api/v1/whoami")
55
+ if resp.status_code != 200:
56
+ return None
57
+ return resp.json().get("username")
58
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
59
+ return None
60
+
45
61
  async def run(
46
62
  self,
47
63
  *,
@@ -54,6 +70,7 @@ class TagsStep:
54
70
  now = datetime.now(UTC).isoformat()
55
71
 
56
72
  async with httpx.AsyncClient(headers=self._headers, timeout=_TIMEOUT) as client:
73
+ api_user = None if dry_run else await self._get_api_username(client)
57
74
  for cls in classifications:
58
75
  result.processed += 1
59
76
  try:
@@ -83,10 +100,17 @@ class TagsStep:
83
100
  continue
84
101
 
85
102
  if not dry_run:
103
+ # Persist computed tags locally first so item detail shows
104
+ # them even if the CMS write below fails (owner/permission).
105
+ for tag in desired:
106
+ await self._db.add_catalog_tag(cls.friendly_token, tag)
86
107
  changed = await self._push_tags(
87
- client, cls.friendly_token, desired
108
+ client,
109
+ cls.friendly_token,
110
+ desired,
111
+ api_user=api_user,
88
112
  )
89
- # Reverse sync: pull CMS tags back to local DB
113
+ # Reverse sync: pull any CMS-only tags back to local DB
90
114
  await self._reverse_sync(client, cls.friendly_token)
91
115
  if changed:
92
116
  result.changed += 1
@@ -106,15 +130,31 @@ class TagsStep:
106
130
  return result
107
131
 
108
132
  async def _push_tags(
109
- self, client: httpx.AsyncClient, token: str, new_tags: list[str]
133
+ self,
134
+ client: httpx.AsyncClient,
135
+ token: str,
136
+ new_tags: list[str],
137
+ *,
138
+ api_user: str | None = None,
110
139
  ) -> bool:
111
- url = f"{self._base}/api/v1/media/{token}"
140
+ """Add tags to a CMS media item via the bulk_actions ``add_tags`` action.
141
+
142
+ The ``PUT /media/{token}`` endpoint only accepts title/description/media_file
143
+ and silently ignores a ``tags`` field, so tag changes must go through
144
+ ``POST /media/user/bulk_actions`` with ``action: add_tags``. That endpoint
145
+ filters ``Media.objects.filter(user=request.user, ...)`` with no manager or
146
+ superuser bypass, so we skip the push unless the API user owns the media
147
+ rather than flooding CMS with 400s.
148
+ """
149
+ detail_url = f"{self._base}/api/v1/media/{token}"
112
150
  try:
113
- resp = await client.get(url)
151
+ resp = await client.get(detail_url)
114
152
  if resp.status_code != 200:
115
153
  return False
116
154
  data = resp.json()
117
155
  owner = data.get("user")
156
+ if api_user is None or owner != api_user:
157
+ return False
118
158
  current = {
119
159
  (t.get("title") if isinstance(t, dict) else t)
120
160
  for t in (data.get("tags_info") or [])
@@ -122,13 +162,14 @@ class TagsStep:
122
162
  to_add = [t for t in new_tags if t not in current]
123
163
  if not to_add:
124
164
  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
- )
165
+ put = await client.post(
166
+ f"{self._base}/api/v1/media/user/bulk_actions",
167
+ json={
168
+ "action": "add_tags",
169
+ "media_ids": [token],
170
+ "tag_titles": to_add,
171
+ },
172
+ )
132
173
  return put.status_code in (200, 201)
133
174
  except (httpx.HTTPError, json.JSONDecodeError, ValueError):
134
175
  return False
@@ -203,6 +203,13 @@ function applyFacets() {
203
203
  }
204
204
  if (cat) params.set('category', cat);
205
205
  if (tag) params.set('tag', tag);
206
+ // Preserve person/studio facets — these come from the URL, not the dropdowns,
207
+ // and must survive a sort/hide_short change or the filter is silently dropped.
208
+ const current = new URLSearchParams(window.location.search);
209
+ const person = current.get('person');
210
+ const studio = current.get('studio');
211
+ if (person) params.set('person', person);
212
+ if (studio) params.set('studio', studio);
206
213
  if (sort && sort !== 'default') params.set('sort', sort);
207
214
  // Only include hide_short in URL when non-default (default = 1, hide).
208
215
  if (!hideShort) params.set('hide_short', '0');
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.36.0"
3
+ version = "0.36.2"
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"