kryten-webqueue 0.34.9__tar.gz → 0.34.11__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 (141) hide show
  1. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/CHANGELOG.md +32 -0
  2. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/PKG-INFO +1 -1
  3. kryten_webqueue-0.34.11/_tmp_check_covers.py +38 -0
  4. kryten_webqueue-0.34.11/_tmp_check_covers2.py +33 -0
  5. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/app.py +2 -3
  6. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/images.py +60 -12
  7. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/sync.py +11 -2
  8. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/pyproject.toml +1 -1
  9. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/.github/workflows/gitleaks.yml +0 -0
  10. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/.github/workflows/python-publish.yml +0 -0
  11. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/.github/workflows/release.yml +0 -0
  12. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/.gitignore +0 -0
  13. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/.gitleaks.toml +0 -0
  14. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/.pre-commit-config.yaml +0 -0
  15. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/AGENT-WORKFLOW-GUIDE.md +0 -0
  16. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/README.md +0 -0
  17. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/config.example.json +0 -0
  18. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/deploy/kryten-webqueue.service +0 -0
  19. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/deploy/nginx-queue.conf +0 -0
  20. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/IMPLEMENTATION_SPEC.md +0 -0
  21. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/IMPL_API_GATE.md +0 -0
  22. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/IMPL_ECONOMY.md +0 -0
  23. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/IMPL_KRYTEN_PY.md +0 -0
  24. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/IMPL_ROBOT.md +0 -0
  25. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  26. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  27. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/Log for incorrect promo insertion problem.txt +0 -0
  28. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  29. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/PRE_PLAN_GAPS.md +0 -0
  30. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/PRODUCT_PLAN.md +0 -0
  31. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/SPEC_EMOTE_REHOST.md +0 -0
  32. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  33. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/docs/UX_POLISH_PLAN.md +0 -0
  34. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/__init__.py +0 -0
  35. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/__main__.py +0 -0
  36. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/api_gate/__init__.py +0 -0
  37. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/api_gate/client.py +0 -0
  38. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/auth/__init__.py +0 -0
  39. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/auth/otp.py +0 -0
  40. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/auth/rate_limit.py +0 -0
  41. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/auth/session.py +0 -0
  42. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/__init__.py +0 -0
  43. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/db/__init__.py +0 -0
  44. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  45. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/db/_connection.py +0 -0
  46. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  47. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  48. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/db/_queue.py +0 -0
  49. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/catalog/mediacms.py +0 -0
  50. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/config.py +0 -0
  51. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/__init__.py +0 -0
  52. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  53. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  54. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  55. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  56. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  57. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  58. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  59. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  60. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  61. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  62. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/jobs/__init__.py +0 -0
  63. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  64. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  65. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/jobs/manager.py +0 -0
  66. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  67. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/jobs/tasks.py +0 -0
  68. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/logging_config.py +0 -0
  69. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/playlists/__init__.py +0 -0
  70. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/playlists/bulk_add.py +0 -0
  71. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/playlists/fire.py +0 -0
  72. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/playlists/importer.py +0 -0
  73. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/playlists/ordering.py +0 -0
  74. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/playlists/scheduler.py +0 -0
  75. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/promos/__init__.py +0 -0
  76. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/promos/director.py +0 -0
  77. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/__init__.py +0 -0
  78. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/completion.py +0 -0
  79. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/ordering.py +0 -0
  80. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/poller.py +0 -0
  81. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/presence.py +0 -0
  82. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/race_poller.py +0 -0
  83. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/queue/shadow.py +0 -0
  84. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/__init__.py +0 -0
  85. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_catalog.py +0 -0
  86. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_feedback.py +0 -0
  87. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  88. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_jobs.py +0 -0
  89. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_moderation.py +0 -0
  90. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_playlists.py +0 -0
  91. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_promos.py +0 -0
  92. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_queue.py +0 -0
  93. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/admin_schedules.py +0 -0
  94. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/auth.py +0 -0
  95. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/catalog.py +0 -0
  96. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/feedback.py +0 -0
  97. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/pages.py +0 -0
  98. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/queue.py +0 -0
  99. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/routes/user.py +0 -0
  100. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/css/main.css +0 -0
  101. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  102. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/js/admin-promos.js +0 -0
  103. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  104. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  105. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/js/admin.js +0 -0
  106. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/static/js/main.js +0 -0
  107. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/admin/index.html +0 -0
  108. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/admin/playlists.html +0 -0
  109. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/admin/promos.html +0 -0
  110. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  111. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/admin/schedules.html +0 -0
  112. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/auth/login.html +0 -0
  113. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/base.html +0 -0
  114. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/catalog/browse.html +0 -0
  115. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  116. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  117. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/feedback/index.html +0 -0
  118. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/queue/index.html +0 -0
  119. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/race.html +0 -0
  120. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/templates/user/dashboard.html +0 -0
  121. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/ws/__init__.py +0 -0
  122. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/ws/handler.py +0 -0
  123. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/kryten_webqueue/ws/manager.py +0 -0
  124. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/__init__.py +0 -0
  125. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_config_persistence.py +0 -0
  126. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_feedback.py +0 -0
  127. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_fetchurls_sharepoint.py +0 -0
  128. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_phase1.py +0 -0
  129. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_phase2_jobs.py +0 -0
  130. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_phase3_jobs.py +0 -0
  131. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_phase4_live_fixes.py +0 -0
  132. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_playlist_import.py +0 -0
  133. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_presence_refund.py +0 -0
  134. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_promo_director.py +0 -0
  135. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_promo_pool_exclusion.py +0 -0
  136. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_queue_announce.py +0 -0
  137. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_race_poller.py +0 -0
  138. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_recently_played_hide.py +0 -0
  139. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_save_results_to_playlist.py +0 -0
  140. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_schedule_lock.py +0 -0
  141. {kryten_webqueue-0.34.9 → kryten_webqueue-0.34.11}/tests/test_search_facets.py +0 -0
@@ -2,6 +2,38 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.34.11] - 2026-08-13
6
+
7
+ ### Fixed
8
+
9
+ - **Cover art now found for many more films.** Three improvements to the search
10
+ strategy in `CoverArtResolver`:
11
+
12
+ - **Retry thumbnail-source movies on every sync.** Previously any item with a
13
+ saved `cover_art_path` (including the thumbnail fallback) was never
14
+ re-checked against TMDB/OMDB. Now items with `cover_art_source='thumbnail'`
15
+ and `duration_sec > 1800` (movie-length) are retried on each catalog sync,
16
+ so a title that failed the first time around gets another chance.
17
+
18
+ - **Leading-year normalization.** Titles stored as `(1989) Godzilla vs.
19
+ Biollante` are now converted to `Godzilla vs. Biollante (1989)` before
20
+ the first TMDB/OMDB search attempt so the API sees a properly-formatted
21
+ query. (`_normalize_leading_year` helper; applied in `_search_tmdb` and
22
+ `_search_omdb`.)
23
+
24
+ - **Smarter `_clean_title`.** Year extraction now prefers a
25
+ parenthesised year over a bare leading year (fixes `1917 (2019)` extracting
26
+ year 2019, not 1917). Added noise-stripping for `(Dubbed)`, `(English)`,
27
+ `(Uncut)` and similar language/edition tags; trailing `Svengoolie`
28
+ attribution; and bare trailing quality markers like `HD`.
29
+
30
+ ## [0.34.10] - 2026-08-13
31
+
32
+ ### Fixed
33
+
34
+ - **Startup crash.** `app.py` referenced the now-removed `playlist_pass` key in
35
+ the return value of `purge_promo_hide_state()`.
36
+
5
37
  ## [0.34.9] - 2026-08-13
6
38
 
7
39
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.34.9
3
+ Version: 0.34.11
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
@@ -0,0 +1,38 @@
1
+ import sqlite3
2
+
3
+ DB = "/var/lib/kryten-webqueue/webqueue.db"
4
+ conn = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
5
+ conn.row_factory = sqlite3.Row
6
+
7
+ # Check specific item
8
+ cur = conn.execute(
9
+ "SELECT friendly_token, title, cover_art_path, cover_art_source FROM catalog WHERE friendly_token='0A9oGeit2'"
10
+ )
11
+ print("=== Deathsport item ===")
12
+ for r in cur:
13
+ print(dict(r))
14
+
15
+ # Overall cover art stats
16
+ print("\n=== Cover art stats ===")
17
+ cur2 = conn.execute(
18
+ "SELECT cover_art_source, COUNT(*) as cnt FROM catalog GROUP BY cover_art_source ORDER BY cnt DESC"
19
+ )
20
+ for r in cur2:
21
+ print(dict(r))
22
+
23
+ # How many items have no cover art at all?
24
+ cur3 = conn.execute(
25
+ "SELECT COUNT(*) as cnt FROM catalog WHERE cover_art_path IS NULL OR cover_art_path = ''"
26
+ )
27
+ row = cur3.fetchone()
28
+ print(f"\nItems with no cover art path: {row['cnt']}")
29
+
30
+ # Sample of items with no cover art (movies - duration > 3600)
31
+ print("\n=== Sample no-art items (movies) ===")
32
+ cur4 = conn.execute(
33
+ "SELECT friendly_token, title, cover_art_source FROM catalog WHERE (cover_art_path IS NULL OR cover_art_path = '') AND duration_sec > 3600 LIMIT 20"
34
+ )
35
+ for r in cur4:
36
+ print(dict(r))
37
+
38
+ conn.close()
@@ -0,0 +1,33 @@
1
+ import sqlite3
2
+
3
+ conn = sqlite3.connect("file:/var/lib/kryten-webqueue/webqueue.db?mode=ro", uri=True)
4
+ conn.row_factory = sqlite3.Row
5
+
6
+ print("=== Thumbnail-source movies (sample) ===")
7
+ cur = conn.execute(
8
+ "SELECT friendly_token, title FROM catalog "
9
+ "WHERE cover_art_source='thumbnail' AND duration_sec > 3600 "
10
+ "ORDER BY title LIMIT 40"
11
+ )
12
+ for r in cur:
13
+ print(f"{r['friendly_token']:12} {r['title']}")
14
+
15
+ print("\n=== TMDB-matched movies (sample for comparison) ===")
16
+ cur2 = conn.execute(
17
+ "SELECT friendly_token, title FROM catalog "
18
+ "WHERE cover_art_source='tmdb' AND duration_sec > 3600 "
19
+ "ORDER BY title LIMIT 10"
20
+ )
21
+ for r in cur2:
22
+ print(f"{r['friendly_token']:12} {r['title']}")
23
+
24
+ print("\n=== thumbnail-source items where title looks clean (year + normal title) ===")
25
+ cur3 = conn.execute(
26
+ "SELECT COUNT(*) as cnt FROM catalog "
27
+ "WHERE cover_art_source='thumbnail' AND duration_sec > 3600 "
28
+ "AND title GLOB '* (19[0-9][0-9])' OR title GLOB '* (20[0-9][0-9])'"
29
+ )
30
+ row = cur3.fetchone()
31
+ print(f"Items with '(YYYY)' in title: {row['cnt']}")
32
+
33
+ conn.close()
@@ -57,11 +57,10 @@ async def lifespan(app: FastAPI):
57
57
  # recently-played rules). Cheap and idempotent; also cleans rows written by
58
58
  # older builds that classified promos only by pool membership.
59
59
  purged = await db.purge_promo_hide_state()
60
- if purged["completions"] or purged["playlist_pass"]:
60
+ if purged["completions"]:
61
61
  logger.info(
62
- "Purged promo recently-played state: %d completion(s), %d pass row(s)",
62
+ "Purged promo recently-played state: %d completion(s)",
63
63
  purged["completions"],
64
- purged["playlist_pass"],
65
64
  )
66
65
  # Any job run still marked 'running' is an orphan from a prior crash/restart
67
66
  # (the running flag is in-memory only). Reconcile before registering jobs.
@@ -11,15 +11,41 @@ from PIL import Image
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
+ def _normalize_leading_year(title: str) -> str:
15
+ """Move a leading parenthesised year to standard trailing position.
16
+
17
+ ``(1989) Godzilla vs. Biollante`` → ``Godzilla vs. Biollante (1989)``
18
+ Has no effect when the year is already trailing or absent.
19
+ """
20
+ m = re.match(r"^\s*[\(\[]((?:19|20)\d{2})[\)\]]\s*(.+)", title)
21
+ if m:
22
+ return f"{m.group(2).strip()} ({m.group(1)})"
23
+ return title
24
+
25
+
14
26
  def _clean_title(title: str) -> tuple[str, str | None]:
15
27
  """Return (cleaned_title, year_or_None) stripping common noise."""
16
- # Extract 4-digit year in parens or at end: "Title (2019)" or "Title 2019"
17
- year = None
18
- m = re.search(r"\b((?:19|20)\d{2})\b", title)
28
+ # Prefer a year in parentheses/brackets — it's almost always the movie year.
29
+ # A bare leading year like "1917 (2019)" has the year as its TITLE, not its
30
+ # release year, so the parenthesised form wins when both are present.
31
+ m = re.search(r"[\(\[]((?:19|20)\d{2})[\)\]]", title)
19
32
  if m:
20
- year = m.group(1)
21
- # Remove year, episode tags, resolution tags, etc.
22
- cleaned = re.sub(r"\s*[\(\[]?(?:19|20)\d{2}[\)\]]?", "", title)
33
+ year: str | None = m.group(1)
34
+ else:
35
+ m = re.search(r"\b((?:19|20)\d{2})\b", title)
36
+ year = m.group(1) if m else None
37
+
38
+ # Remove ONLY the identified year (with its surrounding brackets if any).
39
+ if year:
40
+ cleaned = re.sub(r"\s*[\(\[]\s*" + year + r"\s*[\)\]]", "", title)
41
+ # Also remove a bare occurrence of the same year if it has word boundaries
42
+ # on both sides (handles "Title 1978" format) but NOT when the year IS
43
+ # the title ("1917").
44
+ cleaned = re.sub(r"(?<=\s)" + year + r"(?=\s|$)", "", cleaned).strip()
45
+ else:
46
+ cleaned = title
47
+
48
+ # Episode tags, resolution/encoding, dub/language noise.
23
49
  cleaned = re.sub(r"\s*[Ss]\d{1,2}[Ee]\d{1,2}.*", "", cleaned)
24
50
  cleaned = re.sub(
25
51
  r"\s*\b(?:720p|1080p|2160p|4K|HDR|BluRay|BDRip|WEB[-.]?DL|HDTV)\b.*",
@@ -27,6 +53,22 @@ def _clean_title(title: str) -> tuple[str, str | None]:
27
53
  cleaned,
28
54
  flags=re.IGNORECASE,
29
55
  )
56
+ cleaned = re.sub(
57
+ r"\s*[\(\[]\s*(?:Dubbed|English Dubbed|English Dub|Dub|Sub(?:bed)?|"
58
+ r"English|Uncut|Extended(?: Cut)?|Director'?s? Cut|Remastered|HD|SD)\s*[\)\]]",
59
+ "",
60
+ cleaned,
61
+ flags=re.IGNORECASE,
62
+ )
63
+ # Strip trailing Svengoolie attribution (e.g. "Tarantula Svengoolie").
64
+ cleaned = re.sub(r"\s+Svengoolie\b.*", "", cleaned, flags=re.IGNORECASE)
65
+ # Strip trailing bare quality/edition markers (e.g. "Title HD" or "Title Uncut").
66
+ cleaned = re.sub(
67
+ r"\s+\b(?:HD|SD|4K|Uncut|Remastered|Extended)\b\s*$",
68
+ "",
69
+ cleaned,
70
+ flags=re.IGNORECASE,
71
+ )
30
72
  cleaned = cleaned.strip(" .-")
31
73
  return cleaned or title, year
32
74
 
@@ -149,12 +191,15 @@ class CoverArtResolver:
149
191
 
150
192
  async def _search_tmdb(self, title: str) -> str | None:
151
193
  """Search TMDB for a poster: tries movie+TV in parallel, retries with cleaned title."""
152
- result = await self._tmdb_search_both(title)
194
+ # Normalise leading-year format before the first attempt so TMDB sees
195
+ # 'Title (Year)' rather than '(Year) Title'.
196
+ normalized = _normalize_leading_year(title)
197
+ result = await self._tmdb_search_both(normalized)
153
198
  if result:
154
199
  return result
155
- # Retry with cleaned title if it differs
156
- cleaned, year = _clean_title(title)
157
- if cleaned != title:
200
+ # Retry with fully-cleaned title + year filter.
201
+ cleaned, year = _clean_title(normalized)
202
+ if cleaned != normalized:
158
203
  result = await self._tmdb_search_both(cleaned, year=year)
159
204
  return result
160
205
 
@@ -205,8 +250,11 @@ class CoverArtResolver:
205
250
  return None, 0.0
206
251
 
207
252
  async def _search_omdb(self, title: str) -> str | None:
208
- cleaned, year = _clean_title(title)
209
- for t in dict.fromkeys([title, cleaned]): # try original then cleaned, deduped
253
+ normalized = _normalize_leading_year(title)
254
+ cleaned, year = _clean_title(normalized)
255
+ for t in dict.fromkeys(
256
+ [normalized, cleaned]
257
+ ): # try normalized then cleaned, deduped
210
258
  params: dict = {"apikey": self._omdb_key, "t": t}
211
259
  if year:
212
260
  params["y"] = year
@@ -160,8 +160,17 @@ class CatalogSync:
160
160
  except Exception as e:
161
161
  logger.debug(f"Facet sync failed for {token}: {e}")
162
162
 
163
- # Fetch TMDB/OMDB cover art if not already cached
164
- if self._cover_art and not (existing and existing.get("cover_art_path")):
163
+ # Fetch TMDB/OMDB cover art.
164
+ # Skip only if we already have a real poster (tmdb/omdb source).
165
+ # Retry thumbnail-source items that are long enough to be movies — the
166
+ # thumbnail fallback fires when TMDB/OMDB return nothing, but a better
167
+ # title match on a subsequent sync may now succeed.
168
+ has_real_art = existing and existing.get("cover_art_source") in ("tmdb", "omdb")
169
+ is_movie_length = (row.get("duration_sec") or 0) > 1800
170
+ needs_cover = not has_real_art and (
171
+ not existing or is_movie_length or not existing.get("cover_art_path")
172
+ )
173
+ if self._cover_art and needs_cover:
165
174
  try:
166
175
  await self._cover_art.resolve(token, row["title"], self._db)
167
176
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.34.9"
3
+ version = "0.34.11"
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"