mkv-episode-matcher 0.9.5__tar.gz → 0.9.7__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.

Potentially problematic release.


This version of mkv-episode-matcher might be problematic. Click here for more details.

Files changed (52) hide show
  1. mkv_episode_matcher-0.9.7/.claude/settings.local.json +20 -0
  2. mkv_episode_matcher-0.9.7/.coverage +0 -0
  3. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/PKG-INFO +1 -1
  4. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/episode_identification.py +5 -3
  5. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/utils.py +4 -0
  6. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher.egg-info/PKG-INFO +1 -1
  7. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher.egg-info/SOURCES.txt +3 -0
  8. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/setup.cfg +1 -1
  9. mkv_episode_matcher-0.9.7/tests/test_episode_identification.py +286 -0
  10. mkv_episode_matcher-0.9.7/tests/test_path_spaces_quotes.py +309 -0
  11. mkv_episode_matcher-0.9.5/.coverage +0 -0
  12. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.gitattributes +0 -0
  13. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.github/funding.yml +0 -0
  14. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.github/workflows/claude-code-review.yml +0 -0
  15. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.github/workflows/claude.yml +0 -0
  16. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.github/workflows/documentation.yml +0 -0
  17. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.github/workflows/python-publish.yml +0 -0
  18. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.github/workflows/tests.yml +0 -0
  19. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.gitignore +0 -0
  20. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.gitmodules +0 -0
  21. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.python-version +0 -0
  22. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/.vscode/settings.json +0 -0
  23. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/CHANGELOG.md +0 -0
  24. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/LICENSE +0 -0
  25. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/README.md +0 -0
  26. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/api/index.md +0 -0
  27. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/changelog.md +0 -0
  28. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/cli.md +0 -0
  29. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/configuration.md +0 -0
  30. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/installation.md +0 -0
  31. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/quickstart.md +0 -0
  32. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/docs/tips.md +0 -0
  33. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkdocs.yml +0 -0
  34. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/.gitattributes +0 -0
  35. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/__init__.py +0 -0
  36. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/__main__.py +0 -0
  37. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/config.py +0 -0
  38. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/episode_matcher.py +0 -0
  39. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/subtitle_utils.py +0 -0
  40. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher/tmdb_client.py +0 -0
  41. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher.egg-info/dependency_links.txt +0 -0
  42. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher.egg-info/entry_points.txt +0 -0
  43. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher.egg-info/requires.txt +0 -0
  44. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/mkv_episode_matcher.egg-info/top_level.txt +0 -0
  45. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/pyproject.toml +0 -0
  46. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/setup.py +0 -0
  47. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/tests/__init__.py +0 -0
  48. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/tests/test_config_special_characters.py +0 -0
  49. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/tests/test_main.py +0 -0
  50. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/tests/test_path_handling.py +0 -0
  51. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/tests/test_trailing_slash.py +0 -0
  52. {mkv_episode_matcher-0.9.5 → mkv_episode_matcher-0.9.7}/uv.lock +0 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh issue view:*)",
5
+ "Bash(git checkout:*)",
6
+ "Bash(python -m pytest tests/ -v)",
7
+ "Bash(uv run pytest:*)",
8
+ "Bash(gh pr view:*)",
9
+ "Bash(gh pr checks:*)",
10
+ "WebFetch(domain:github.com)",
11
+ "Bash(gh run view:*)",
12
+ "Bash(uv run:*)",
13
+ "Bash(git add:*)",
14
+ "Bash(git commit:*)",
15
+ "Bash(git push:*)",
16
+ "Bash(rm:*)"
17
+ ],
18
+ "deny": []
19
+ }
20
+ }
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkv-episode-matcher
3
- Version: 0.9.5
3
+ Version: 0.9.7
4
4
  Summary: The MKV Episode Matcher is a tool for identifying TV series episodes from MKV files and renaming the files accordingly.
5
5
  Home-page: https://github.com/Jsakkos/mkv-episode-matcher
6
6
  Author: Jonathan Sakkos
@@ -155,11 +155,13 @@ class EpisodeMatcher:
155
155
  ]
156
156
 
157
157
  reference_files = []
158
- for _pattern in patterns:
158
+ for pattern in patterns:
159
+ # Use case-insensitive file extension matching by checking both .srt and .SRT
160
+ srt_files = list(reference_dir.glob("*.srt")) + list(reference_dir.glob("*.SRT"))
159
161
  files = [
160
162
  f
161
- for f in reference_dir.glob("*.srt")
162
- if any(re.search(f"{p}\\d+", f.name, re.IGNORECASE) for p in patterns)
163
+ for f in srt_files
164
+ if re.search(f"{pattern}\\d+", f.name, re.IGNORECASE)
163
165
  ]
164
166
  reference_files.extend(files)
165
167
 
@@ -24,6 +24,7 @@ def normalize_path(path_str):
24
24
  """
25
25
  Normalize a path string to handle cross-platform path issues.
26
26
  Properly handles trailing slashes and backslashes in both Windows and Unix paths.
27
+ Also strips surrounding quotes that might be present in command line arguments.
27
28
 
28
29
  Args:
29
30
  path_str (str): The path string to normalize
@@ -35,6 +36,9 @@ def normalize_path(path_str):
35
36
  if isinstance(path_str, Path):
36
37
  path_str = str(path_str)
37
38
 
39
+ # Strip surrounding quotes (both single and double)
40
+ path_str = path_str.strip().strip('"').strip("'")
41
+
38
42
  # Remove trailing slashes or backslashes
39
43
  path_str = path_str.rstrip("/").rstrip("\\")
40
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkv-episode-matcher
3
- Version: 0.9.5
3
+ Version: 0.9.7
4
4
  Summary: The MKV Episode Matcher is a tool for identifying TV series episodes from MKV files and renaming the files accordingly.
5
5
  Home-page: https://github.com/Jsakkos/mkv-episode-matcher
6
6
  Author: Jonathan Sakkos
@@ -11,6 +11,7 @@ pyproject.toml
11
11
  setup.cfg
12
12
  setup.py
13
13
  uv.lock
14
+ .claude/settings.local.json
14
15
  .github/funding.yml
15
16
  .github/workflows/claude-code-review.yml
16
17
  .github/workflows/claude.yml
@@ -42,6 +43,8 @@ mkv_episode_matcher.egg-info/requires.txt
42
43
  mkv_episode_matcher.egg-info/top_level.txt
43
44
  tests/__init__.py
44
45
  tests/test_config_special_characters.py
46
+ tests/test_episode_identification.py
45
47
  tests/test_main.py
46
48
  tests/test_path_handling.py
49
+ tests/test_path_spaces_quotes.py
47
50
  tests/test_trailing_slash.py
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = mkv_episode_matcher
3
- version = 0.9.5
3
+ version = 0.9.7
4
4
  author = Jonathan Sakkos
5
5
  author_email = jonathansakkos@gmail.com
6
6
  description = The MKV Episode Matcher is a tool for identifying TV series episodes from MKV files and renaming the files accordingly.
@@ -0,0 +1,286 @@
1
+ import unittest
2
+ import tempfile
3
+ import re
4
+ from pathlib import Path
5
+ from unittest.mock import Mock, patch
6
+
7
+ from mkv_episode_matcher.episode_identification import EpisodeMatcher
8
+
9
+
10
+ class TestEpisodeIdentificationPatternMatching(unittest.TestCase):
11
+ """Test the pattern matching functionality in episode identification."""
12
+
13
+ def setUp(self):
14
+ """Set up test fixtures."""
15
+ self.temp_dir = tempfile.TemporaryDirectory()
16
+ self.cache_dir = Path(self.temp_dir.name)
17
+
18
+ # Mock subtitle cache
19
+ self.mock_subtitle_cache = Mock()
20
+
21
+ # Create identifier instance
22
+ self.identifier = EpisodeMatcher(
23
+ cache_dir=self.cache_dir,
24
+ show_name="Test Show (2023)"
25
+ )
26
+
27
+ def tearDown(self):
28
+ """Clean up test fixtures."""
29
+ self.temp_dir.cleanup()
30
+
31
+ def _create_test_files(self, filenames, show_name=None):
32
+ """Create test subtitle files in the cache directory."""
33
+ if show_name is None:
34
+ show_name = self.identifier.show_name
35
+
36
+ show_dir = self.cache_dir / "data" / show_name
37
+ show_dir.mkdir(parents=True, exist_ok=True)
38
+
39
+ created_files = []
40
+ for filename in filenames:
41
+ file_path = show_dir / filename
42
+ file_path.write_text("dummy subtitle content")
43
+ created_files.append(file_path)
44
+
45
+ return created_files
46
+
47
+ def test_pattern_matching_s01e_format(self):
48
+ """Test pattern matching for S01E format files."""
49
+ # Create test files with S01E format
50
+ test_files = [
51
+ "Test Show (2023) - S01E01.srt",
52
+ "Test Show (2023) - S01E02.srt",
53
+ "Test Show (2023) - S01E10.srt",
54
+ "Test Show (2023) - S02E01.srt", # Should not match season 1
55
+ ]
56
+
57
+ self._create_test_files(test_files)
58
+
59
+ # Test season 1 matching
60
+ reference_files = self.identifier.get_reference_files(1)
61
+
62
+ # Should find 3 files for season 1
63
+ self.assertEqual(len(reference_files), 3)
64
+
65
+ # Verify correct files are matched
66
+ matched_names = [f.name for f in reference_files]
67
+ self.assertIn("Test Show (2023) - S01E01.srt", matched_names)
68
+ self.assertIn("Test Show (2023) - S01E02.srt", matched_names)
69
+ self.assertIn("Test Show (2023) - S01E10.srt", matched_names)
70
+ self.assertNotIn("Test Show (2023) - S02E01.srt", matched_names)
71
+
72
+ def test_pattern_matching_s1e_format(self):
73
+ """Test pattern matching for S1E format files."""
74
+ test_files = [
75
+ "Show - S1E01.srt",
76
+ "Show - S1E05.srt",
77
+ "Show - S2E01.srt", # Should not match season 1
78
+ ]
79
+
80
+ self._create_test_files(test_files)
81
+
82
+ reference_files = self.identifier.get_reference_files(1)
83
+
84
+ # Should find 2 files for season 1
85
+ self.assertEqual(len(reference_files), 2)
86
+
87
+ matched_names = [f.name for f in reference_files]
88
+ self.assertIn("Show - S1E01.srt", matched_names)
89
+ self.assertIn("Show - S1E05.srt", matched_names)
90
+ self.assertNotIn("Show - S2E01.srt", matched_names)
91
+
92
+ def test_pattern_matching_01x_format(self):
93
+ """Test pattern matching for 01x format files."""
94
+ test_files = [
95
+ "Show - 01x01.srt",
96
+ "Show - 01x15.srt",
97
+ "Show - 02x01.srt", # Should not match season 1
98
+ ]
99
+
100
+ self._create_test_files(test_files)
101
+
102
+ reference_files = self.identifier.get_reference_files(1)
103
+
104
+ # Should find 2 files for season 1
105
+ self.assertEqual(len(reference_files), 2)
106
+
107
+ matched_names = [f.name for f in reference_files]
108
+ self.assertIn("Show - 01x01.srt", matched_names)
109
+ self.assertIn("Show - 01x15.srt", matched_names)
110
+ self.assertNotIn("Show - 02x01.srt", matched_names)
111
+
112
+ def test_pattern_matching_1x_format(self):
113
+ """Test pattern matching for 1x format files."""
114
+ test_files = [
115
+ "Show - 1x01.srt",
116
+ "Show - 1x08.srt",
117
+ "Show - 2x01.srt", # Should not match season 1
118
+ ]
119
+
120
+ self._create_test_files(test_files)
121
+
122
+ reference_files = self.identifier.get_reference_files(1)
123
+
124
+ # Should find 2 files for season 1
125
+ self.assertEqual(len(reference_files), 2)
126
+
127
+ matched_names = [f.name for f in reference_files]
128
+ self.assertIn("Show - 1x01.srt", matched_names)
129
+ self.assertIn("Show - 1x08.srt", matched_names)
130
+ self.assertNotIn("Show - 2x01.srt", matched_names)
131
+
132
+ def test_pattern_matching_mixed_formats(self):
133
+ """Test pattern matching with mixed file formats."""
134
+ test_files = [
135
+ "Show - S01E01.srt",
136
+ "Show - S1E02.srt",
137
+ "Show - 01x03.srt",
138
+ "Show - 1x04.srt",
139
+ "Show - S02E01.srt", # Different season
140
+ "Show - episode1.srt", # No matching pattern
141
+ ]
142
+
143
+ self._create_test_files(test_files)
144
+
145
+ reference_files = self.identifier.get_reference_files(1)
146
+
147
+ # Should find 4 files for season 1 (all matching patterns)
148
+ self.assertEqual(len(reference_files), 4)
149
+
150
+ matched_names = [f.name for f in reference_files]
151
+ self.assertIn("Show - S01E01.srt", matched_names)
152
+ self.assertIn("Show - S1E02.srt", matched_names)
153
+ self.assertIn("Show - 01x03.srt", matched_names)
154
+ self.assertIn("Show - 1x04.srt", matched_names)
155
+ self.assertNotIn("Show - S02E01.srt", matched_names)
156
+ self.assertNotIn("Show - episode1.srt", matched_names)
157
+
158
+ def test_case_insensitive_matching(self):
159
+ """Test that pattern matching is case insensitive."""
160
+ test_files = [
161
+ "show - s01e01.srt", # lowercase s and e
162
+ "SHOW - S01E02.SRT", # uppercase everything
163
+ "Show - S01e03.srt", # mixed case
164
+ ]
165
+
166
+ self._create_test_files(test_files)
167
+
168
+ reference_files = self.identifier.get_reference_files(1)
169
+
170
+ # Should find all 3 files regardless of case
171
+ self.assertEqual(len(reference_files), 3)
172
+
173
+ def test_dexter_new_blood_issue_reproduction(self):
174
+ """Test the specific issue from bug report with Dexter New Blood files."""
175
+ # Create files matching the exact pattern from the bug report
176
+ test_files = [
177
+ "Dexter - New Blood (2021) - S01E01.srt",
178
+ "Dexter - New Blood (2021) - S01E02.srt",
179
+ "Dexter - New Blood (2021) - S01E03.srt",
180
+ "Dexter - New Blood (2021) - S01E04.srt",
181
+ "Dexter - New Blood (2021) - S01E05.srt",
182
+ "Dexter - New Blood (2021) - S01E06.srt",
183
+ "Dexter - New Blood (2021) - S01E07.srt",
184
+ "Dexter - New Blood (2021) - S01E08.srt",
185
+ "Dexter - New Blood (2021) - S01E09.srt",
186
+ "Dexter - New Blood (2021) - S01E10.srt",
187
+ ]
188
+
189
+ # Update the identifier to use the exact show name from the bug report
190
+ show_name = "Dexter - New Blood (2021)"
191
+ self.identifier.show_name = show_name
192
+
193
+ self._create_test_files(test_files, show_name)
194
+
195
+ reference_files = self.identifier.get_reference_files(1)
196
+
197
+ # Should find all 10 files - this was the bug that was failing
198
+ self.assertEqual(len(reference_files), 10,
199
+ f"Expected 10 files but found {len(reference_files)}. "
200
+ f"Files found: {[f.name for f in reference_files]}")
201
+
202
+ def test_no_duplicate_files(self):
203
+ """Test that duplicate matches are removed properly."""
204
+ # Create a file that could match multiple patterns
205
+ test_files = [
206
+ "Show - S01E01.srt", # Could match both S01E and 01x patterns if logic is wrong
207
+ ]
208
+
209
+ self._create_test_files(test_files)
210
+
211
+ reference_files = self.identifier.get_reference_files(1)
212
+
213
+ # Should find only 1 unique file, not duplicates
214
+ self.assertEqual(len(reference_files), 1)
215
+
216
+ # Verify it's the correct file
217
+ self.assertEqual(reference_files[0].name, "Show - S01E01.srt")
218
+
219
+ def test_empty_directory(self):
220
+ """Test behavior when no matching files exist."""
221
+ # Create the directory but no files
222
+ show_dir = self.cache_dir / "data" / "Test Show (2023)"
223
+ show_dir.mkdir(parents=True, exist_ok=True)
224
+
225
+ reference_files = self.identifier.get_reference_files(1)
226
+
227
+ # Should find no files
228
+ self.assertEqual(len(reference_files), 0)
229
+
230
+ def test_directory_does_not_exist(self):
231
+ """Test behavior when the show directory doesn't exist."""
232
+ # Don't create the directory
233
+ reference_files = self.identifier.get_reference_files(1)
234
+
235
+ # Should find no files (glob returns empty on non-existent directory)
236
+ self.assertEqual(len(reference_files), 0)
237
+
238
+ def test_caching_functionality(self):
239
+ """Test that results are properly cached."""
240
+ test_files = ["Show - S01E01.srt"]
241
+ self._create_test_files(test_files)
242
+
243
+ # First call
244
+ reference_files_1 = self.identifier.get_reference_files(1)
245
+
246
+ # Second call should return cached result
247
+ reference_files_2 = self.identifier.get_reference_files(1)
248
+
249
+ # Should be the same result
250
+ self.assertEqual(reference_files_1, reference_files_2)
251
+
252
+ # Verify it's actually using the cache by checking the cache directly
253
+ cache_key = ("Test Show (2023)", 1)
254
+ self.assertIn(cache_key, self.identifier.reference_files_cache)
255
+
256
+ def test_different_seasons(self):
257
+ """Test that different seasons are handled correctly."""
258
+ test_files = [
259
+ "Show - S01E01.srt",
260
+ "Show - S01E02.srt",
261
+ "Show - S02E01.srt",
262
+ "Show - S02E02.srt",
263
+ "Show - S03E01.srt",
264
+ ]
265
+
266
+ self._create_test_files(test_files)
267
+
268
+ # Test season 1
269
+ season1_files = self.identifier.get_reference_files(1)
270
+ self.assertEqual(len(season1_files), 2)
271
+
272
+ # Test season 2
273
+ season2_files = self.identifier.get_reference_files(2)
274
+ self.assertEqual(len(season2_files), 2)
275
+
276
+ # Test season 3
277
+ season3_files = self.identifier.get_reference_files(3)
278
+ self.assertEqual(len(season3_files), 1)
279
+
280
+ # Test non-existent season
281
+ season4_files = self.identifier.get_reference_files(4)
282
+ self.assertEqual(len(season4_files), 0)
283
+
284
+
285
+ if __name__ == '__main__':
286
+ unittest.main()
@@ -0,0 +1,309 @@
1
+ import unittest
2
+ import tempfile
3
+ import argparse
4
+ from pathlib import Path
5
+ from unittest.mock import Mock, patch, MagicMock
6
+
7
+ from mkv_episode_matcher.utils import normalize_path, get_subtitles, get_valid_seasons
8
+ from mkv_episode_matcher.config import get_config, set_config
9
+
10
+
11
+ class TestPathHandlingWithSpacesAndQuotes(unittest.TestCase):
12
+ """Test path handling for issues 61 and 62 - paths with spaces and quotes."""
13
+
14
+ def setUp(self):
15
+ """Set up test fixtures."""
16
+ self.temp_dir = tempfile.TemporaryDirectory()
17
+ self.temp_path = Path(self.temp_dir.name)
18
+
19
+ def tearDown(self):
20
+ """Clean up test fixtures."""
21
+ self.temp_dir.cleanup()
22
+
23
+ def test_normalize_path_with_spaces(self):
24
+ """Test that normalize_path handles paths with spaces correctly."""
25
+ paths_with_spaces = [
26
+ "C:\\my rips\\show name",
27
+ "/home/user/TV Shows/My Show",
28
+ "C:/Program Files/My TV Shows/Show With Spaces",
29
+ "/mnt/c/Users/John Doe/Shows/Breaking Bad",
30
+ "D:\\Users\\Jane Smith\\My Videos\\Game of Thrones\\",
31
+ ]
32
+
33
+ for path_str in paths_with_spaces:
34
+ with self.subTest(path=path_str):
35
+ normalized = normalize_path(path_str)
36
+ # Should return a valid Path object
37
+ self.assertIsInstance(normalized, Path)
38
+ # Should preserve the directory name with spaces
39
+ self.assertIn(" ", normalized.name,
40
+ f"Path with spaces should preserve spaces: {path_str}")
41
+
42
+ def test_normalize_path_with_quotes(self):
43
+ """Test that normalize_path handles paths with quotes correctly."""
44
+ paths_with_quotes = [
45
+ '"C:\\my rips\\show name"',
46
+ '"/home/user/TV Shows/My Show"',
47
+ "'C:/Program Files/My TV Shows/Show With Spaces'",
48
+ '"D:\\Users\\Jane Smith\\My Videos\\Game of Thrones\\"',
49
+ ]
50
+
51
+ for path_str in paths_with_quotes:
52
+ with self.subTest(path=path_str):
53
+ normalized = normalize_path(path_str)
54
+ # Should return a valid Path object
55
+ self.assertIsInstance(normalized, Path)
56
+ # Should not contain quotes in the final path
57
+ self.assertNotIn('"', str(normalized))
58
+ self.assertNotIn("'", str(normalized))
59
+
60
+ def test_get_valid_seasons_with_spaces(self):
61
+ """Test that get_valid_seasons works with directories containing spaces."""
62
+ # Create test directory structure with spaces
63
+ show_dir = self.temp_path / "My Test Show"
64
+ show_dir.mkdir()
65
+
66
+ season1_dir = show_dir / "Season 1"
67
+ season2_dir = show_dir / "Season 2 - Special Edition"
68
+ season1_dir.mkdir()
69
+ season2_dir.mkdir()
70
+
71
+ # Create some .mkv files
72
+ (season1_dir / "Episode 1.mkv").touch()
73
+ (season1_dir / "Episode 2.mkv").touch()
74
+ (season2_dir / "Special Episode.mkv").touch()
75
+
76
+ # Test with path containing spaces
77
+ seasons = get_valid_seasons(str(show_dir))
78
+
79
+ # Should find both seasons
80
+ self.assertEqual(len(seasons), 2)
81
+ self.assertTrue(any("Season 1" in str(season) for season in seasons))
82
+ self.assertTrue(any("Season 2 - Special Edition" in str(season) for season in seasons))
83
+
84
+ def test_command_line_parsing_with_spaces(self):
85
+ """Test that command line argument parsing handles paths with spaces."""
86
+ from mkv_episode_matcher.__main__ import main
87
+
88
+ test_cases = [
89
+ ['--show-dir', 'C:\\my rips\\show name'],
90
+ ['--show-dir', '/home/user/TV Shows/My Show'],
91
+ ['--show-dir', 'D:\\Users\\Jane Smith\\My Videos'],
92
+ ]
93
+
94
+ for args in test_cases:
95
+ with self.subTest(args=args):
96
+ parser = argparse.ArgumentParser()
97
+ parser.add_argument('--show-dir', help='Main directory of the show')
98
+
99
+ # This should not raise an exception
100
+ parsed_args = parser.parse_args(args)
101
+ self.assertIsNotNone(parsed_args.show_dir)
102
+ # Spaces should be preserved
103
+ self.assertIn(" ", parsed_args.show_dir)
104
+
105
+ def test_command_line_parsing_with_quotes(self):
106
+ """Test that command line argument parsing handles quoted paths."""
107
+ test_cases = [
108
+ ['--show-dir', '"C:\\my rips\\show name"'],
109
+ ['--show-dir', '"/home/user/TV Shows/My Show"'],
110
+ ['--show-dir', "'D:\\Users\\Jane Smith\\My Videos'"],
111
+ ]
112
+
113
+ for args in test_cases:
114
+ with self.subTest(args=args):
115
+ parser = argparse.ArgumentParser()
116
+ parser.add_argument('--show-dir', help='Main directory of the show')
117
+
118
+ # This should not raise an exception
119
+ parsed_args = parser.parse_args(args)
120
+ self.assertIsNotNone(parsed_args.show_dir)
121
+ # Should handle quotes appropriately
122
+ # Note: argparse typically strips outer quotes automatically
123
+
124
+ @patch('mkv_episode_matcher.utils.get_config')
125
+ def test_config_storage_with_spaces(self, mock_get_config):
126
+ """Test that configuration correctly stores and retrieves paths with spaces."""
127
+ # Test path with spaces
128
+ test_path = "C:\\Users\\John Doe\\My TV Shows\\Breaking Bad"
129
+
130
+ # Create a temporary config file
131
+ config_file = self.temp_path / "test_config.ini"
132
+
133
+ # Set config with path containing spaces
134
+ set_config(
135
+ tmdb_api_key="test_key",
136
+ open_subtitles_api_key="test_key",
137
+ open_subtitles_user_agent="test_agent",
138
+ open_subtitles_username="test_user",
139
+ open_subtitles_password="test_pass",
140
+ show_dir=test_path,
141
+ file=config_file
142
+ )
143
+
144
+ # Read back the config
145
+ config = get_config(config_file)
146
+
147
+ # Should preserve spaces in the path
148
+ self.assertEqual(config.get("show_dir"), test_path)
149
+ self.assertIn(" ", config.get("show_dir"))
150
+
151
+ @patch('mkv_episode_matcher.utils.get_config')
152
+ @patch('mkv_episode_matcher.utils.OpenSubtitles')
153
+ @patch('mkv_episode_matcher.tmdb_client.fetch_season_details')
154
+ def test_get_subtitles_with_spaces_in_path(self, mock_fetch_season, mock_opensubtitles, mock_get_config):
155
+ """Test that get_subtitles function works with show directories containing spaces."""
156
+ # Mock configuration with path containing spaces
157
+ mock_config = {
158
+ "show_dir": "C:\\Users\\John Doe\\My TV Shows\\Breaking Bad",
159
+ "tmdb_api_key": "test_tmdb_key",
160
+ "open_subtitles_api_key": "test_os_key",
161
+ "open_subtitles_user_agent": "test_agent",
162
+ "open_subtitles_username": "test_user",
163
+ "open_subtitles_password": "test_pass"
164
+ }
165
+ mock_get_config.return_value = mock_config
166
+
167
+ # Mock OpenSubtitles client
168
+ mock_client = Mock()
169
+ mock_opensubtitles.return_value = mock_client
170
+
171
+ # Mock season details
172
+ mock_fetch_season.return_value = {
173
+ 'season_number': 1,
174
+ 'episodes': [
175
+ {'episode_number': 1, 'name': 'Pilot'},
176
+ {'episode_number': 2, 'name': 'Cat\'s in the Bag...'}
177
+ ]
178
+ }
179
+
180
+ # This should not raise an exception with spaces in the path
181
+ try:
182
+ get_subtitles(show_id=1396, seasons={1}, config=mock_config)
183
+ except Exception as e:
184
+ # If there's an exception, it shouldn't be related to path handling
185
+ # OpenSubtitles exceptions are expected in a test environment
186
+ self.assertNotIn("path", str(e).lower())
187
+ self.assertNotIn("space", str(e).lower())
188
+
189
+ def test_path_edge_cases(self):
190
+ """Test edge cases for path handling."""
191
+ edge_cases = [
192
+ # Multiple spaces
193
+ "C:\\My TV Shows\\Show With Multiple Spaces",
194
+ # Leading/trailing spaces
195
+ " C:\\My TV Shows\\Show With Spaces ",
196
+ # Mixed quotes and spaces
197
+ '"C:\\My TV Shows"\\Show Name',
198
+ # Unicode characters with spaces
199
+ "C:\\My TV Shows\\Café Français",
200
+ # Very long path with spaces
201
+ "C:\\A Very Long Path Name With Many Spaces\\And Another Very Long Directory Name\\Show Name",
202
+ ]
203
+
204
+ for path_str in edge_cases:
205
+ with self.subTest(path=path_str):
206
+ # normalize_path should handle these without raising exceptions
207
+ try:
208
+ result = normalize_path(path_str)
209
+ self.assertIsInstance(result, Path)
210
+ except Exception as e:
211
+ self.fail(f"normalize_path failed on edge case '{path_str}': {e}")
212
+
213
+ def test_issue_61_reproduction(self):
214
+ """Reproduce issue 61: Show-dir path cannot contain spaces."""
215
+ # This test reproduces the exact issue described in #61
216
+ problem_path = "C:\\my rips\\show_name"
217
+
218
+ # The issue is that --get-subs fails when the path has spaces
219
+ # Let's test the path normalization that's used in get_subtitles
220
+ normalized = normalize_path(problem_path)
221
+
222
+ # Should successfully normalize without losing information
223
+ self.assertIsInstance(normalized, Path)
224
+ self.assertEqual(normalized.name, "show_name")
225
+
226
+ # The normalized path should be usable for directory operations
227
+ self.assertTrue(str(normalized)) # Should not be empty
228
+
229
+ def test_issue_62_reproduction(self):
230
+ """Reproduce issue 62: show-dir cannot have quotes."""
231
+ # This test reproduces the exact issue described in #62
232
+ quoted_paths = [
233
+ '"C:\\my rips\\show name"',
234
+ "'C:\\my rips\\show name'",
235
+ '"C:/my rips/show name"',
236
+ ]
237
+
238
+ for quoted_path in quoted_paths:
239
+ with self.subTest(path=quoted_path):
240
+ # The issue is that quoted paths aren't handled properly
241
+ normalized = normalize_path(quoted_path)
242
+
243
+ # Should successfully normalize and remove quotes
244
+ self.assertIsInstance(normalized, Path)
245
+ self.assertNotIn('"', str(normalized))
246
+ self.assertNotIn("'", str(normalized))
247
+
248
+
249
+ class TestPathIntegration(unittest.TestCase):
250
+ """Integration tests for path handling across the application."""
251
+
252
+ def setUp(self):
253
+ """Set up test fixtures."""
254
+ self.temp_dir = tempfile.TemporaryDirectory()
255
+ self.temp_path = Path(self.temp_dir.name)
256
+
257
+ def tearDown(self):
258
+ """Clean up test fixtures."""
259
+ self.temp_dir.cleanup()
260
+
261
+ @patch('mkv_episode_matcher.episode_matcher.get_config')
262
+ def test_episode_matcher_with_spaces(self, mock_get_config):
263
+ """Test that EpisodeMatcher handles show names with spaces correctly."""
264
+ from mkv_episode_matcher.episode_identification import EpisodeMatcher
265
+
266
+ # Mock config with path containing spaces
267
+ mock_config = {
268
+ "show_dir": "C:\\Users\\John Doe\\My TV Shows\\Breaking Bad"
269
+ }
270
+ mock_get_config.return_value = mock_config
271
+
272
+ # This should not raise an exception
273
+ try:
274
+ matcher = EpisodeMatcher(
275
+ cache_dir=self.temp_path,
276
+ show_name="Breaking Bad"
277
+ )
278
+ self.assertIsNotNone(matcher)
279
+ self.assertEqual(matcher.show_name, "Breaking Bad")
280
+ except Exception as e:
281
+ self.fail(f"EpisodeMatcher failed with spaces in show name: {e}")
282
+
283
+ def test_end_to_end_path_handling(self):
284
+ """Test end-to-end path handling from command line to processing."""
285
+ # Create a test directory structure with spaces
286
+ show_dir = self.temp_path / "My Test Show With Spaces"
287
+ show_dir.mkdir()
288
+
289
+ season_dir = show_dir / "Season 1"
290
+ season_dir.mkdir()
291
+
292
+ # Create a test .mkv file
293
+ test_file = season_dir / "Test Episode.mkv"
294
+ test_file.touch()
295
+
296
+ # Test that the path can be processed through the whole pipeline
297
+ normalized = normalize_path(str(show_dir))
298
+ self.assertIsInstance(normalized, Path)
299
+
300
+ # Should be able to find seasons
301
+ seasons = get_valid_seasons(str(show_dir))
302
+ self.assertEqual(len(seasons), 1)
303
+
304
+ # Season path should also contain spaces
305
+ self.assertIn("Season 1", str(seasons[0]))
306
+
307
+
308
+ if __name__ == '__main__':
309
+ unittest.main()
Binary file