riplex 0.9.1__tar.gz → 0.9.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 (142) hide show
  1. {riplex-0.9.1/src/riplex.egg-info → riplex-0.9.2}/PKG-INFO +1 -1
  2. {riplex-0.9.1 → riplex-0.9.2}/docs/changelog.md +6 -0
  3. {riplex-0.9.1 → riplex-0.9.2}/docs/getting-started/installation.md +3 -0
  4. {riplex-0.9.1 → riplex-0.9.2}/docs/troubleshooting.md +2 -0
  5. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/metadata/sources/tmdb.py +35 -6
  6. {riplex-0.9.1 → riplex-0.9.2/src/riplex.egg-info}/PKG-INFO +1 -1
  7. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/SOURCES.txt +1 -0
  8. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/scm_file_list.json +1 -0
  9. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/scm_version.json +2 -2
  10. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/welcome.py +3 -2
  11. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/commands/setup.py +1 -1
  12. riplex-0.9.2/tests/test_tmdb_provider.py +57 -0
  13. {riplex-0.9.1 → riplex-0.9.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {riplex-0.9.1 → riplex-0.9.2}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
  15. {riplex-0.9.1 → riplex-0.9.2}/.github/agents/riplex.agent.md +0 -0
  16. {riplex-0.9.1 → riplex-0.9.2}/.github/copilot-instructions.md +0 -0
  17. {riplex-0.9.1 → riplex-0.9.2}/.github/workflows/publish.yml +0 -0
  18. {riplex-0.9.1 → riplex-0.9.2}/.github/workflows/release.yml +0 -0
  19. {riplex-0.9.1 → riplex-0.9.2}/.gitignore +0 -0
  20. {riplex-0.9.1 → riplex-0.9.2}/.vscode/settings.json +0 -0
  21. {riplex-0.9.1 → riplex-0.9.2}/CONTRIBUTORS.md +0 -0
  22. {riplex-0.9.1 → riplex-0.9.2}/LICENSE +0 -0
  23. {riplex-0.9.1 → riplex-0.9.2}/README.md +0 -0
  24. {riplex-0.9.1 → riplex-0.9.2}/docs/architecture.md +0 -0
  25. {riplex-0.9.1 → riplex-0.9.2}/docs/cli-guide/lookup.md +0 -0
  26. {riplex-0.9.1 → riplex-0.9.2}/docs/cli-guide/orchestrate.md +0 -0
  27. {riplex-0.9.1 → riplex-0.9.2}/docs/cli-guide/organize.md +0 -0
  28. {riplex-0.9.1 → riplex-0.9.2}/docs/cli-guide/workflow.md +0 -0
  29. {riplex-0.9.1 → riplex-0.9.2}/docs/getting-started/configuration.md +0 -0
  30. {riplex-0.9.1 → riplex-0.9.2}/docs/gui-guide/gui-walkthrough.md +0 -0
  31. {riplex-0.9.1 → riplex-0.9.2}/docs/index.md +0 -0
  32. {riplex-0.9.1 → riplex-0.9.2}/docs/naming-rules.md +0 -0
  33. {riplex-0.9.1 → riplex-0.9.2}/docs/reference/cli.md +0 -0
  34. {riplex-0.9.1 → riplex-0.9.2}/issues/debug-artifacts-consolidation.md +0 -0
  35. {riplex-0.9.1 → riplex-0.9.2}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
  36. {riplex-0.9.1 → riplex-0.9.2}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  37. {riplex-0.9.1 → riplex-0.9.2}/issues/planned-features.md +0 -0
  38. {riplex-0.9.1 → riplex-0.9.2}/mkdocs.yml +0 -0
  39. {riplex-0.9.1 → riplex-0.9.2}/pyproject.toml +0 -0
  40. {riplex-0.9.1 → riplex-0.9.2}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
  41. {riplex-0.9.1 → riplex-0.9.2}/screenshots/1_Welcome_Screen.png +0 -0
  42. {riplex-0.9.1 → riplex-0.9.2}/screenshots/2_Disc_Detection_BTTF.png +0 -0
  43. {riplex-0.9.1 → riplex-0.9.2}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
  44. {riplex-0.9.1 → riplex-0.9.2}/screenshots/4_Disc_Release_BTTF.png +0 -0
  45. {riplex-0.9.1 → riplex-0.9.2}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
  46. {riplex-0.9.1 → riplex-0.9.2}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
  47. {riplex-0.9.1 → riplex-0.9.2}/setup.cfg +0 -0
  48. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/__init__.py +0 -0
  49. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/cache.py +0 -0
  50. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/config.py +0 -0
  51. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/dedup.py +0 -0
  52. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/detect.py +0 -0
  53. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/disc/__init__.py +0 -0
  54. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/disc/analysis.py +0 -0
  55. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/disc/makemkv.py +0 -0
  56. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/disc/provider.py +0 -0
  57. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/formatter.py +0 -0
  58. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/lookup.py +0 -0
  59. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/manifest.py +0 -0
  60. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/matcher.py +0 -0
  61. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/metadata/__init__.py +0 -0
  62. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/metadata/planner.py +0 -0
  63. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/metadata/provider.py +0 -0
  64. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/metadata/sources/__init__.py +0 -0
  65. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/models.py +0 -0
  66. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/normalize.py +0 -0
  67. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/organizer.py +0 -0
  68. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/scanner.py +0 -0
  69. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/snapshot.py +0 -0
  70. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/splitter.py +0 -0
  71. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/tagger.py +0 -0
  72. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/title.py +0 -0
  73. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/ui.py +0 -0
  74. {riplex-0.9.1 → riplex-0.9.2}/src/riplex/updater.py +0 -0
  75. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/dependency_links.txt +0 -0
  76. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/entry_points.txt +0 -0
  77. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/requires.txt +0 -0
  78. {riplex-0.9.1 → riplex-0.9.2}/src/riplex.egg-info/top_level.txt +0 -0
  79. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/__init__.py +0 -0
  80. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/bug_report.py +0 -0
  81. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/crash_dump.py +0 -0
  82. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/keep_awake.py +0 -0
  83. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/main.py +0 -0
  84. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/__init__.py +0 -0
  85. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/disc_detection.py +0 -0
  86. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/disc_overview.py +0 -0
  87. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/disc_swap.py +0 -0
  88. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/done.py +0 -0
  89. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/folder_picker.py +0 -0
  90. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/metadata.py +0 -0
  91. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/orchestrate_done.py +0 -0
  92. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/organize_done.py +0 -0
  93. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/organize_preview.py +0 -0
  94. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/progress.py +0 -0
  95. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/release.py +0 -0
  96. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/selection.py +0 -0
  97. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_app/screens/update.py +0 -0
  98. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/__init__.py +0 -0
  99. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/commands/__init__.py +0 -0
  100. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/commands/lookup.py +0 -0
  101. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/commands/orchestrate.py +0 -0
  102. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/commands/organize.py +0 -0
  103. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/commands/rip.py +0 -0
  104. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/formatting.py +0 -0
  105. {riplex-0.9.1 → riplex-0.9.2}/src/riplex_cli/main.py +0 -0
  106. {riplex-0.9.1 → riplex-0.9.2}/tests/__init__.py +0 -0
  107. {riplex-0.9.1 → riplex-0.9.2}/tests/fixtures/chernobyl_disc1.json +0 -0
  108. {riplex-0.9.1 → riplex-0.9.2}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  109. {riplex-0.9.1 → riplex-0.9.2}/tests/fixtures/makemkvcon_list.txt +0 -0
  110. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  111. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  112. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  113. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  114. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  115. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  116. {riplex-0.9.1 → riplex-0.9.2}/tests/snapshots/Waterworld.snapshot.json +0 -0
  117. {riplex-0.9.1 → riplex-0.9.2}/tests/test_app_main.py +0 -0
  118. {riplex-0.9.1 → riplex-0.9.2}/tests/test_cache.py +0 -0
  119. {riplex-0.9.1 → riplex-0.9.2}/tests/test_cli_utils.py +0 -0
  120. {riplex-0.9.1 → riplex-0.9.2}/tests/test_config.py +0 -0
  121. {riplex-0.9.1 → riplex-0.9.2}/tests/test_dedup.py +0 -0
  122. {riplex-0.9.1 → riplex-0.9.2}/tests/test_detect.py +0 -0
  123. {riplex-0.9.1 → riplex-0.9.2}/tests/test_disc_analysis.py +0 -0
  124. {riplex-0.9.1 → riplex-0.9.2}/tests/test_disc_detection_screen.py +0 -0
  125. {riplex-0.9.1 → riplex-0.9.2}/tests/test_disc_fixtures.py +0 -0
  126. {riplex-0.9.1 → riplex-0.9.2}/tests/test_disc_provider.py +0 -0
  127. {riplex-0.9.1 → riplex-0.9.2}/tests/test_formatter.py +0 -0
  128. {riplex-0.9.1 → riplex-0.9.2}/tests/test_lookup.py +0 -0
  129. {riplex-0.9.1 → riplex-0.9.2}/tests/test_makemkv.py +0 -0
  130. {riplex-0.9.1 → riplex-0.9.2}/tests/test_matcher.py +0 -0
  131. {riplex-0.9.1 → riplex-0.9.2}/tests/test_normalize.py +0 -0
  132. {riplex-0.9.1 → riplex-0.9.2}/tests/test_organizer.py +0 -0
  133. {riplex-0.9.1 → riplex-0.9.2}/tests/test_planner.py +0 -0
  134. {riplex-0.9.1 → riplex-0.9.2}/tests/test_release_screen.py +0 -0
  135. {riplex-0.9.1 → riplex-0.9.2}/tests/test_rip_guide.py +0 -0
  136. {riplex-0.9.1 → riplex-0.9.2}/tests/test_scanner.py +0 -0
  137. {riplex-0.9.1 → riplex-0.9.2}/tests/test_snapshot.py +0 -0
  138. {riplex-0.9.1 → riplex-0.9.2}/tests/test_splitter.py +0 -0
  139. {riplex-0.9.1 → riplex-0.9.2}/tests/test_tagger.py +0 -0
  140. {riplex-0.9.1 → riplex-0.9.2}/tests/test_ui.py +0 -0
  141. {riplex-0.9.1 → riplex-0.9.2}/tests/test_updater.py +0 -0
  142. {riplex-0.9.1 → riplex-0.9.2}/tests/test_welcome_screen.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -4,6 +4,12 @@ All notable changes to the riplex documentation are recorded here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## v0.9.2 — 2026-06-24
8
+
9
+ ### Fixed
10
+
11
+ - **TMDb credentials: accept both the API Key and the Read Access Token.** TMDb's settings page offers a v3 API Key (query parameter auth) and a v4 API Read Access Token (bearer header auth). riplex now auto-detects which credential was provided and uses the matching auth scheme, so pasting the Read Access Token no longer fails with `401 Unauthorized`. The setup wizard, GUI welcome screen, and docs now note that either credential works.
12
+
7
13
  ## v0.9.0 — 2026-06-13
8
14
 
9
15
  Summary: Plex-aligned movie version and edition support for combo-disc releases, with better organize preview matching for 4K, Blu-ray, and 3D movie rips.
@@ -243,6 +243,9 @@ The setup wizard will:
243
243
  > enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex`
244
244
  > as the URL. The rest of the form can be filled with basic info -- it
245
245
  > doesn't need to be a real business. The key is approved instantly.
246
+ >
247
+ > The settings page offers both an **API Key** (v3) and an **API Read
248
+ > Access Token** (v4). riplex accepts either one.
246
249
 
247
250
  2. Ask where your Plex library and MakeMKV rip folders are
248
251
  3. Check for required tools (MakeMKV, ffprobe, mkvmerge, mkvpropedit)
@@ -74,6 +74,8 @@ riplex setup --force
74
74
 
75
75
  **Solution:** TMDb asks for an app name and URL when you request a key. You can enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex` as the URL. The rest of the form can be filled with basic info - it doesn't need to be a real business. The key is approved instantly.
76
76
 
77
+ The settings page shows two credentials: an **API Key** (v3) and an **API Read Access Token** (v4). riplex accepts either one - paste whichever you copied. If you get a `401 Unauthorized` error, double-check that the value was copied in full.
78
+
77
79
  Sign up and request a key at: https://www.themoviedb.org/settings/api
78
80
 
79
81
  ---
@@ -28,22 +28,44 @@ TMDB_BASE_URL = "https://api.themoviedb.org/3"
28
28
  _TMDB_TTL_DAYS = 7
29
29
 
30
30
 
31
+ def _looks_like_read_access_token(key: str) -> bool:
32
+ """Return ``True`` when *key* looks like a TMDb v4 Read Access Token.
33
+
34
+ TMDb's settings page offers two credentials: a v3 ``API Key`` (a 32-char
35
+ hex string used as the ``api_key`` query parameter) and a v4 ``API Read
36
+ Access Token`` (a JWT used as an ``Authorization: Bearer`` header). The
37
+ v4 token is a JWT, so it starts with ``eyJ`` and contains exactly two
38
+ dots; a v3 key never does.
39
+ """
40
+ return key.startswith("eyJ") and key.count(".") == 2
41
+
42
+
31
43
  class TmdbProvider(MetadataProvider):
32
44
  """Metadata provider backed by TMDb API v3."""
33
45
 
34
46
  def __init__(self, api_key: str | None = None) -> None:
35
- self._api_key = api_key or os.environ.get("TMDB_API_KEY", "")
47
+ self._api_key = (api_key or os.environ.get("TMDB_API_KEY", "")).strip()
36
48
  if not self._api_key:
37
49
  raise ValueError(
38
50
  "TMDb API key is required. Pass --api-key, set the "
39
51
  "TMDB_API_KEY environment variable, or add tmdb_api_key "
40
52
  "to your config file."
41
53
  )
42
- self._client = httpx.AsyncClient(
43
- base_url=TMDB_BASE_URL,
44
- params={"api_key": self._api_key},
45
- timeout=30.0,
46
- )
54
+ # TMDb accepts either a v3 API Key (query parameter) or a v4 Read
55
+ # Access Token (Authorization header) against the v3 endpoints. Pick
56
+ # the matching auth scheme so both credential types work.
57
+ if _looks_like_read_access_token(self._api_key):
58
+ self._client = httpx.AsyncClient(
59
+ base_url=TMDB_BASE_URL,
60
+ headers={"Authorization": f"Bearer {self._api_key}"},
61
+ timeout=30.0,
62
+ )
63
+ else:
64
+ self._client = httpx.AsyncClient(
65
+ base_url=TMDB_BASE_URL,
66
+ params={"api_key": self._api_key},
67
+ timeout=30.0,
68
+ )
47
69
 
48
70
  async def close(self) -> None:
49
71
  await self._client.aclose()
@@ -61,6 +83,13 @@ class TmdbProvider(MetadataProvider):
61
83
  if cached is not None:
62
84
  return cached
63
85
  resp = await self._client.get(path, params=params)
86
+ if resp.status_code == 401:
87
+ raise ValueError(
88
+ "TMDb rejected the credential (401 Unauthorized). Check that "
89
+ "your TMDb API Key (v3) or API Read Access Token (v4) is "
90
+ "correct. Both are available at "
91
+ "https://www.themoviedb.org/settings/api."
92
+ )
64
93
  resp.raise_for_status()
65
94
  data = resp.json()
66
95
  if cache_ns and cache_key:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -124,6 +124,7 @@ tests/test_scanner.py
124
124
  tests/test_snapshot.py
125
125
  tests/test_splitter.py
126
126
  tests/test_tagger.py
127
+ tests/test_tmdb_provider.py
127
128
  tests/test_ui.py
128
129
  tests/test_updater.py
129
130
  tests/test_welcome_screen.py
@@ -101,6 +101,7 @@
101
101
  "tests/test_snapshot.py",
102
102
  "tests/test_formatter.py",
103
103
  "tests/test_updater.py",
104
+ "tests/test_tmdb_provider.py",
104
105
  "tests/test_normalize.py",
105
106
  "tests/test_disc_analysis.py",
106
107
  "tests/test_cache.py",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.9.1",
2
+ "tag": "0.9.2",
3
3
  "distance": 0,
4
- "node": "g1987f1d9a7fb9e2034793b92c4a94e7cd198682f",
4
+ "node": "g775f04026aebf5255891652cd7e5dec5dce26000",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-06-24"
@@ -164,8 +164,9 @@ class WelcomeScreen:
164
164
  ft.Container(height=4),
165
165
  self.api_key_field,
166
166
  ft.Text(
167
- "Required. Get a free API key at themoviedb.org/settings/api "
168
- "used to look up movie and TV show metadata.",
167
+ "Required. Get a free API Key or Read Access Token at "
168
+ "themoviedb.org/settings/api — used to look up movie and "
169
+ "TV show metadata.",
169
170
  size=11,
170
171
  color=ft.Colors.GREY_600,
171
172
  ),
@@ -77,7 +77,7 @@ def run_setup(force: bool = False) -> int:
77
77
  value = input(f"{label}{prompt_hint}{suffix}: ").strip()
78
78
  return value if value else current
79
79
 
80
- tmdb_key = prompt("tmdb_api_key", "TMDb API key", "free at themoviedb.org/settings/api", mask=True)
80
+ tmdb_key = prompt("tmdb_api_key", "TMDb API key or Read Access Token", "free at themoviedb.org/settings/api", mask=True)
81
81
  output_root = prompt("output_root", "Plex library root", "e.g. E:/Media")
82
82
  rip_output = prompt("rip_output", "MakeMKV rip output folder", "e.g. E:/Media/Rips")
83
83
  archive_root = prompt("archive_root", "Archive root (optional)", "move raw rips here after organizing")
@@ -0,0 +1,57 @@
1
+ import pytest
2
+
3
+ from riplex.metadata.sources.tmdb import (
4
+ TmdbProvider,
5
+ _looks_like_read_access_token,
6
+ )
7
+
8
+ # A JWT-shaped sample (header.payload.signature). Not a real credential.
9
+ _SAMPLE_READ_ACCESS_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaXBsZXgifQ.c2lnbmF0dXJl"
10
+ _SAMPLE_API_KEY = "0123456789abcdef0123456789abcdef"
11
+
12
+
13
+ class TestLooksLikeReadAccessToken:
14
+ def test_jwt_token_is_detected(self):
15
+ assert _looks_like_read_access_token(_SAMPLE_READ_ACCESS_TOKEN) is True
16
+
17
+ def test_v3_hex_key_is_not_detected(self):
18
+ assert _looks_like_read_access_token(_SAMPLE_API_KEY) is False
19
+
20
+ def test_empty_string_is_not_detected(self):
21
+ assert _looks_like_read_access_token("") is False
22
+
23
+
24
+ class TestTmdbProviderAuth:
25
+ @pytest.mark.asyncio
26
+ async def test_read_access_token_uses_authorization_header(self):
27
+ provider = TmdbProvider(api_key=_SAMPLE_READ_ACCESS_TOKEN)
28
+ try:
29
+ assert (
30
+ provider._client.headers["Authorization"]
31
+ == f"Bearer {_SAMPLE_READ_ACCESS_TOKEN}"
32
+ )
33
+ assert "api_key" not in provider._client.params
34
+ finally:
35
+ await provider.close()
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_v3_key_uses_query_parameter(self):
39
+ provider = TmdbProvider(api_key=_SAMPLE_API_KEY)
40
+ try:
41
+ assert provider._client.params["api_key"] == _SAMPLE_API_KEY
42
+ assert "Authorization" not in provider._client.headers
43
+ finally:
44
+ await provider.close()
45
+
46
+ @pytest.mark.asyncio
47
+ async def test_whitespace_is_stripped_from_credential(self):
48
+ provider = TmdbProvider(api_key=f" {_SAMPLE_API_KEY} ")
49
+ try:
50
+ assert provider._client.params["api_key"] == _SAMPLE_API_KEY
51
+ finally:
52
+ await provider.close()
53
+
54
+ def test_missing_key_raises(self, monkeypatch):
55
+ monkeypatch.delenv("TMDB_API_KEY", raising=False)
56
+ with pytest.raises(ValueError, match="TMDb API key is required"):
57
+ TmdbProvider(api_key="")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes