kryten-webqueue 0.36.1__tar.gz → 0.36.3__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 (163) hide show
  1. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/CHANGELOG.md +29 -0
  2. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/config.example.json +1 -0
  4. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/tags.py +50 -12
  5. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/config.py +3 -0
  6. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/pyproject.toml +1 -1
  7. kryten_webqueue-0.36.3/scripts/patch_mediacms_tags.py +90 -0
  8. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/.github/workflows/gitleaks.yml +0 -0
  9. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/.github/workflows/python-publish.yml +0 -0
  10. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/.github/workflows/release.yml +0 -0
  11. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/.gitignore +0 -0
  12. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/.gitleaks.toml +0 -0
  13. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/.pre-commit-config.yaml +0 -0
  14. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/AGENT-WORKFLOW-GUIDE.md +0 -0
  15. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/README.md +0 -0
  16. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/deploy/kryten-webqueue.service +0 -0
  17. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/deploy/nginx-queue.conf +0 -0
  18. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/IMPLEMENTATION_SPEC.md +0 -0
  19. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/IMPL_API_GATE.md +0 -0
  20. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/IMPL_ECONOMY.md +0 -0
  21. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/IMPL_KRYTEN_PY.md +0 -0
  22. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/IMPL_ROBOT.md +0 -0
  23. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  24. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  25. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/Log for incorrect promo insertion problem.txt +0 -0
  26. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  27. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/PRE_PLAN_GAPS.md +0 -0
  28. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/PRODUCT_PLAN.md +0 -0
  29. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  30. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/SPEC_EMOTE_REHOST.md +0 -0
  31. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  32. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/docs/UX_POLISH_PLAN.md +0 -0
  33. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/__init__.py +0 -0
  34. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/__main__.py +0 -0
  35. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/api_gate/__init__.py +0 -0
  36. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/api_gate/client.py +0 -0
  37. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/app.py +0 -0
  38. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/auth/__init__.py +0 -0
  39. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/auth/otp.py +0 -0
  40. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/auth/rate_limit.py +0 -0
  41. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/auth/session.py +0 -0
  42. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/__init__.py +0 -0
  43. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/__init__.py +0 -0
  44. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  45. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_connection.py +0 -0
  46. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  47. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  48. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_people.py +0 -0
  49. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  50. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_queue.py +0 -0
  51. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  52. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  53. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  54. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  55. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  56. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  57. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  58. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  59. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  60. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  61. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  62. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  63. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  64. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/images.py +0 -0
  65. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/mediacms.py +0 -0
  66. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/catalog/sync.py +0 -0
  67. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/__init__.py +0 -0
  68. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  69. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  70. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  71. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  72. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  73. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  74. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  75. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  76. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  77. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  78. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/jobs/__init__.py +0 -0
  79. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  80. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  81. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/jobs/manager.py +0 -0
  82. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  83. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/jobs/tasks.py +0 -0
  84. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/logging_config.py +0 -0
  85. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/playlists/__init__.py +0 -0
  86. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/playlists/bulk_add.py +0 -0
  87. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/playlists/fire.py +0 -0
  88. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/playlists/importer.py +0 -0
  89. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/playlists/ordering.py +0 -0
  90. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/playlists/scheduler.py +0 -0
  91. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/promos/__init__.py +0 -0
  92. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/promos/director.py +0 -0
  93. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/__init__.py +0 -0
  94. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/completion.py +0 -0
  95. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/ordering.py +0 -0
  96. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/poller.py +0 -0
  97. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/presence.py +0 -0
  98. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/race_poller.py +0 -0
  99. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/queue/shadow.py +0 -0
  100. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/__init__.py +0 -0
  101. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_catalog.py +0 -0
  102. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_feedback.py +0 -0
  103. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  104. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_jobs.py +0 -0
  105. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_moderation.py +0 -0
  106. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_playlists.py +0 -0
  107. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_promos.py +0 -0
  108. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_queue.py +0 -0
  109. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/admin_schedules.py +0 -0
  110. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/auth.py +0 -0
  111. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/catalog.py +0 -0
  112. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/feedback.py +0 -0
  113. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/pages.py +0 -0
  114. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/queue.py +0 -0
  115. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/routes/user.py +0 -0
  116. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/css/main.css +0 -0
  117. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  118. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/js/admin-promos.js +0 -0
  119. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  120. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  121. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/js/admin.js +0 -0
  122. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/static/js/main.js +0 -0
  123. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/admin/index.html +0 -0
  124. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/admin/playlists.html +0 -0
  125. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/admin/promos.html +0 -0
  126. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  127. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/admin/schedules.html +0 -0
  128. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/auth/login.html +0 -0
  129. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/base.html +0 -0
  130. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/catalog/browse.html +0 -0
  131. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  132. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  133. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/feedback/index.html +0 -0
  134. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/queue/index.html +0 -0
  135. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/race.html +0 -0
  136. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/user/dashboard.html +0 -0
  137. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/templates/user/my_list.html +0 -0
  138. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/ws/__init__.py +0 -0
  139. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/ws/handler.py +0 -0
  140. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/kryten_webqueue/ws/manager.py +0 -0
  141. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/__init__.py +0 -0
  142. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_classify.py +0 -0
  143. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_config_persistence.py +0 -0
  144. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_feedback.py +0 -0
  145. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_fetchurls_sharepoint.py +0 -0
  146. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_people_facets.py +0 -0
  147. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_phase1.py +0 -0
  148. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_phase2_jobs.py +0 -0
  149. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_phase3_jobs.py +0 -0
  150. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_phase4_live_fixes.py +0 -0
  151. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_playlist_import.py +0 -0
  152. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_presence_refund.py +0 -0
  153. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_promo_director.py +0 -0
  154. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_promo_pool_exclusion.py +0 -0
  155. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_providers.py +0 -0
  156. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_queue_announce.py +0 -0
  157. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_race_poller.py +0 -0
  158. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_recently_played_hide.py +0 -0
  159. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_save_results_to_playlist.py +0 -0
  160. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_schedule_lock.py +0 -0
  161. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_search_facets.py +0 -0
  162. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_title_normalization.py +0 -0
  163. {kryten_webqueue-0.36.1 → kryten_webqueue-0.36.3}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.36.3] - 2026-08-14
6
+
7
+ ### Added
8
+
9
+ - **`mediacms_manage_all_media` config flag.** When set (paired with a CMS whose
10
+ `bulk_actions` is patched to let managers/superusers tag any media), the tags
11
+ step pushes derived genre/MPAA/hosted tags to CMS for every item, not just
12
+ media the API user owns. Defaults to `false` (owner-only, safe).
13
+
14
+ ### Changed
15
+
16
+ - **MPAA rating tags are now alphanumeric** (`mpaar`, `mpaapg13`, …) to match
17
+ MediaCMS's `Tag.save()` normalization (which strips non-alphanumerics), so tags
18
+ round-trip cleanly between the local catalog and CMS without creating hyphenated
19
+ duplicates on reverse-sync.
20
+
21
+ ## [0.36.2] - 2026-08-14
22
+
23
+ ### Fixed
24
+
25
+ - **Stop flooding MediaCMS with futile tag-push 400s.** The tags step attempted
26
+ `POST /media/user/bulk_actions` (`add_tags`) for every enriched item, but that
27
+ endpoint filters `Media.objects.filter(user=request.user, ...)` with no manager
28
+ or superuser bypass, so pushes against media owned by other users always return
29
+ 400 "No matching media found". The step now looks up the API token's username
30
+ once via `/whoami` and only attempts the CMS push for media that user actually
31
+ owns. Local `catalog_tags` remains the source of truth for derived
32
+ genre/MPAA/hosted tags, so tags still display regardless.
33
+
5
34
  ## [0.36.1] - 2026-08-14
6
35
 
7
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.36.1
3
+ Version: 0.36.3
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
@@ -13,6 +13,7 @@
13
13
 
14
14
  "mediacms_url": "https://www.dropsugar.com",
15
15
  "mediacms_token": "CHANGE_ME",
16
+ "mediacms_manage_all_media": false,
16
17
 
17
18
  "tmdb_api_key": "",
18
19
  "omdb_api_key": "",
@@ -18,15 +18,15 @@ _TIMEOUT = 20.0
18
18
  _MPAA = frozenset({"G", "PG", "PG-13", "R", "NC-17", "NR", "TV-MA", "TV-14", "TV-PG"})
19
19
 
20
20
  _MPAA_SLUG = {
21
- "G": "mpaa-g",
22
- "PG": "mpaa-pg",
23
- "PG-13": "mpaa-pg13",
24
- "R": "mpaa-r",
25
- "NC-17": "mpaa-nc17",
26
- "NR": "mpaa-nr",
27
- "TV-MA": "mpaa-tvma",
28
- "TV-14": "mpaa-tv14",
29
- "TV-PG": "mpaa-tvpg",
21
+ "G": "mpaag",
22
+ "PG": "mpaapg",
23
+ "PG-13": "mpaapg13",
24
+ "R": "mpaar",
25
+ "NC-17": "mpaanc17",
26
+ "NR": "mpaanr",
27
+ "TV-MA": "mpaatvma",
28
+ "TV-14": "mpaatv14",
29
+ "TV-PG": "mpaatvpg",
30
30
  }
31
31
 
32
32
 
@@ -41,6 +41,25 @@ class TagsStep:
41
41
  config.mediacms_url.rstrip("/").removesuffix("/api/v1").removesuffix("/api")
42
42
  )
43
43
  self._headers = {"Authorization": f"Token {config.mediacms_token}"}
44
+ # When the token can manage all media (e.g. a manager/superuser on a
45
+ # patched CMS), push tags for every item instead of only owned media.
46
+ self._manage_all = bool(getattr(config, "mediacms_manage_all_media", False))
47
+
48
+ async def _get_api_username(self, client: httpx.AsyncClient) -> str | None:
49
+ """Return the API token's username, or None.
50
+
51
+ Stock MediaCMS ``/media/user/bulk_actions`` filters ``Media.objects.filter(
52
+ user=request.user, ...)``, so tag pushes only succeed on media this user
53
+ owns. When ``mediacms_manage_all_media`` is not set we use this to skip
54
+ futile pushes against media owned by others instead of flooding CMS.
55
+ """
56
+ try:
57
+ resp = await client.get(f"{self._base}/api/v1/whoami")
58
+ if resp.status_code != 200:
59
+ return None
60
+ return resp.json().get("username")
61
+ except (httpx.HTTPError, json.JSONDecodeError, ValueError):
62
+ return None
44
63
 
45
64
  async def run(
46
65
  self,
@@ -54,6 +73,11 @@ class TagsStep:
54
73
  now = datetime.now(UTC).isoformat()
55
74
 
56
75
  async with httpx.AsyncClient(headers=self._headers, timeout=_TIMEOUT) as client:
76
+ api_user = (
77
+ None
78
+ if dry_run or self._manage_all
79
+ else await self._get_api_username(client)
80
+ )
57
81
  for cls in classifications:
58
82
  result.processed += 1
59
83
  try:
@@ -88,7 +112,10 @@ class TagsStep:
88
112
  for tag in desired:
89
113
  await self._db.add_catalog_tag(cls.friendly_token, tag)
90
114
  changed = await self._push_tags(
91
- client, cls.friendly_token, desired
115
+ client,
116
+ cls.friendly_token,
117
+ desired,
118
+ api_user=api_user,
92
119
  )
93
120
  # Reverse sync: pull any CMS-only tags back to local DB
94
121
  await self._reverse_sync(client, cls.friendly_token)
@@ -110,13 +137,21 @@ class TagsStep:
110
137
  return result
111
138
 
112
139
  async def _push_tags(
113
- self, client: httpx.AsyncClient, token: str, new_tags: list[str]
140
+ self,
141
+ client: httpx.AsyncClient,
142
+ token: str,
143
+ new_tags: list[str],
144
+ *,
145
+ api_user: str | None = None,
114
146
  ) -> bool:
115
147
  """Add tags to a CMS media item via the bulk_actions ``add_tags`` action.
116
148
 
117
149
  The ``PUT /media/{token}`` endpoint only accepts title/description/media_file
118
150
  and silently ignores a ``tags`` field, so tag changes must go through
119
- ``POST /media/user/bulk_actions`` with ``action: add_tags``.
151
+ ``POST /media/user/bulk_actions`` with ``action: add_tags``. On stock
152
+ MediaCMS that endpoint is owner-scoped, so unless ``manage_all`` is set we
153
+ skip the push for media the API user does not own rather than flooding
154
+ CMS with 400s.
120
155
  """
121
156
  detail_url = f"{self._base}/api/v1/media/{token}"
122
157
  try:
@@ -124,6 +159,9 @@ class TagsStep:
124
159
  if resp.status_code != 200:
125
160
  return False
126
161
  data = resp.json()
162
+ owner = data.get("user")
163
+ if not self._manage_all and (api_user is None or owner != api_user):
164
+ return False
127
165
  current = {
128
166
  (t.get("title") if isinstance(t, dict) else t)
129
167
  for t in (data.get("tags_info") or [])
@@ -151,6 +151,9 @@ class Config(BaseModel):
151
151
  # MediaCMS
152
152
  mediacms_url: str = "https://www.dropsugar.com"
153
153
  mediacms_token: str
154
+ # Set true when the token can tag any media (manager/superuser on a CMS whose
155
+ # bulk_actions is patched to allow it); pushes derived tags for all items.
156
+ mediacms_manage_all_media: bool = False
154
157
 
155
158
  # Cover art APIs
156
159
  tmdb_api_key: str = ""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.36.1"
3
+ version = "0.36.3"
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"
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ """Patch MediaCMS MediaBulkUserActions so editors/superusers can tag any media,
3
+ and add_tags creates missing tags (normalized like Tag.save)."""
4
+
5
+ import sys
6
+
7
+ PATH = "/home/mediacms.io/mediacms/files/views/media.py"
8
+
9
+ QS_OLD = (
10
+ " media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)\n"
11
+ )
12
+ QS_NEW = (
13
+ " if is_mediacms_editor(request.user):\n"
14
+ " media = Media.objects.filter(friendly_token__in=media_ids)\n"
15
+ " else:\n"
16
+ " media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)\n"
17
+ )
18
+
19
+ TAGS_OLD = (
20
+ ' elif action == "add_tags":\n'
21
+ " tag_titles = request.data.get('tag_titles', [])\n"
22
+ " if not tag_titles:\n"
23
+ ' return Response({"detail": "tag_titles is required for add_tags action"}, status=status.HTTP_400_BAD_REQUEST)\n'
24
+ "\n"
25
+ " tags = Tag.objects.filter(title__in=tag_titles)\n"
26
+ " if not tags:\n"
27
+ ' return Response({"detail": "No matching tags found"}, status=status.HTTP_400_BAD_REQUEST)\n'
28
+ "\n"
29
+ " added_count = 0\n"
30
+ " for tag in tags:\n"
31
+ " for m in media:\n"
32
+ " if not m.tags.filter(title=tag.title).exists():\n"
33
+ " m.tags.add(tag)\n"
34
+ " added_count += 1\n"
35
+ "\n"
36
+ ' return Response({"detail": f"Added {added_count} media items to {tags.count()} tags"})\n'
37
+ )
38
+ TAGS_NEW = (
39
+ ' elif action == "add_tags":\n'
40
+ " tag_titles = request.data.get('tag_titles', [])\n"
41
+ " if not tag_titles:\n"
42
+ ' return Response({"detail": "tag_titles is required for add_tags action"}, status=status.HTTP_400_BAD_REQUEST)\n'
43
+ "\n"
44
+ " tags = []\n"
45
+ " for raw in tag_titles:\n"
46
+ " title = helpers.get_alphanumeric_only(raw)[:100]\n"
47
+ " if not title:\n"
48
+ " continue\n"
49
+ " tag, _ = Tag.objects.get_or_create(title=title, defaults={\"user\": request.user})\n"
50
+ " tags.append(tag)\n"
51
+ " if not tags:\n"
52
+ ' return Response({"detail": "No matching tags found"}, status=status.HTTP_400_BAD_REQUEST)\n'
53
+ "\n"
54
+ " added_count = 0\n"
55
+ " for tag in tags:\n"
56
+ " for m in media:\n"
57
+ " if not m.tags.filter(title=tag.title).exists():\n"
58
+ " m.tags.add(tag)\n"
59
+ " added_count += 1\n"
60
+ "\n"
61
+ ' return Response({"detail": f"Added {added_count} media items to {len(tags)} tags"})\n'
62
+ )
63
+
64
+
65
+ def main() -> int:
66
+ with open(PATH, encoding="utf-8") as f:
67
+ src = f.read()
68
+
69
+ if QS_NEW in src and TAGS_NEW in src:
70
+ print("Already patched; nothing to do.")
71
+ return 0
72
+
73
+ if src.count(QS_OLD) != 1:
74
+ print(f"ERROR: queryset anchor found {src.count(QS_OLD)} times (expected 1)")
75
+ return 2
76
+ if src.count(TAGS_OLD) != 1:
77
+ print(f"ERROR: add_tags anchor found {src.count(TAGS_OLD)} times (expected 1)")
78
+ return 3
79
+
80
+ src = src.replace(QS_OLD, QS_NEW, 1)
81
+ src = src.replace(TAGS_OLD, TAGS_NEW, 1)
82
+
83
+ with open(PATH, "w", encoding="utf-8") as f:
84
+ f.write(src)
85
+ print("Patched OK.")
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__":
90
+ sys.exit(main())