mkv-episode-matcher 0.8.1__tar.gz → 0.9.0__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 (45) hide show
  1. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.coverage +0 -0
  2. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/tests.yml +1 -8
  3. mkv_episode_matcher-0.9.0/.python-version +1 -0
  4. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/CHANGELOG.md +17 -0
  5. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/PKG-INFO +1 -1
  6. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/__main__.py +17 -17
  7. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/config.py +2 -2
  8. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/episode_matcher.py +6 -7
  9. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/subtitle_utils.py +3 -3
  10. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/utils.py +72 -46
  11. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/PKG-INFO +1 -1
  12. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/SOURCES.txt +3 -1
  13. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/setup.cfg +1 -1
  14. mkv_episode_matcher-0.9.0/tests/test_path_handling.py +103 -0
  15. mkv_episode_matcher-0.9.0/tests/test_trailing_slash.py +64 -0
  16. mkv_episode_matcher-0.8.1/.python-version +0 -1
  17. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.gitattributes +0 -0
  18. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/funding.yml +0 -0
  19. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/documentation.yml +0 -0
  20. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/python-publish.yml +0 -0
  21. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.gitignore +0 -0
  22. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.gitmodules +0 -0
  23. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.vscode/settings.json +0 -0
  24. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/README.md +0 -0
  25. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/api/index.md +0 -0
  26. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/changelog.md +0 -0
  27. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/cli.md +0 -0
  28. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/configuration.md +0 -0
  29. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/installation.md +0 -0
  30. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/quickstart.md +0 -0
  31. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/tips.md +0 -0
  32. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkdocs.yml +0 -0
  33. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/.gitattributes +0 -0
  34. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/__init__.py +0 -0
  35. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/episode_identification.py +0 -0
  36. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/tmdb_client.py +0 -0
  37. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/dependency_links.txt +0 -0
  38. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/entry_points.txt +0 -0
  39. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/requires.txt +0 -0
  40. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/top_level.txt +0 -0
  41. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/pyproject.toml +0 -0
  42. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/setup.py +0 -0
  43. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/tests/__init__.py +0 -0
  44. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/tests/test_main.py +0 -0
  45. {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/uv.lock +0 -0
@@ -1,37 +1,30 @@
1
1
  name: Tests
2
-
3
2
  on:
4
3
  push:
5
4
  branches: [main, master]
6
5
  pull_request:
7
6
  branches: [main, master]
8
-
9
7
  jobs:
10
8
  test:
11
- runs-on: ubuntu-latest
9
+ runs-on: ${{ matrix.os }}
12
10
  strategy:
13
11
  matrix:
14
12
  os: [ubuntu-latest, windows-latest, macos-latest]
15
13
  python-version: ["3.9", "3.10", "3.11", "3.12"]
16
14
  fail-fast: false
17
-
18
15
  steps:
19
16
  - uses: actions/checkout@v4
20
-
21
17
  - name: Install uv and set the python version
22
18
  uses: astral-sh/setup-uv@v5
23
19
  with:
24
20
  enable-cache: true
25
21
  cache-dependency-glob: "uv.lock"
26
22
  python-version: ${{ matrix.python-version }}
27
-
28
23
  - name: Install the project
29
24
  run: uv sync --all-extras --dev
30
-
31
25
  - name: Run tests with pytest and coverage
32
26
  run: |
33
27
  uv run --dev pytest --cov-branch --cov-report=xml
34
-
35
28
  - name: Upload coverage reports to Codecov
36
29
  uses: codecov/codecov-action@v5
37
30
  with:
@@ -0,0 +1 @@
1
+ 3.11
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.0] - 2025-06-01
9
+
10
+ ### Changed
11
+ - Replaced all `os.path` calls with `pathlib.Path` for improved path handling
12
+ - Fixed issues with trailing slashes in directory paths
13
+ - Updated `check_filename` to handle both string paths and Path objects
14
+ - Modernized file and directory operations to use pathlib API
15
+
16
+ ### Enhanced
17
+ - Improved robustness of path manipulation operations
18
+ - Better handling of different path formats across operating systems
19
+ - More consistent behavior with paths containing trailing slashes
20
+
21
+ ### Fixed
22
+ - Fixed bug where paths with trailing slashes would result in empty show names
23
+ - Fixed incorrect handling of paths in subtitle downloads and match operations
24
+
8
25
  ## [0.7.0] - 2025-03-05
9
26
 
10
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkv-episode-matcher
3
- Version: 0.8.1
3
+ Version: 0.9.0
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
@@ -1,7 +1,7 @@
1
1
  # __main__.py (enhanced version)
2
2
  import argparse
3
- import os
4
3
  import sys
4
+ from pathlib import Path
5
5
  from typing import Optional
6
6
 
7
7
  from loguru import logger
@@ -20,33 +20,33 @@ console = Console()
20
20
  logger.info("Starting the application")
21
21
 
22
22
  # Check if the configuration directory exists, if not create it
23
- CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher")
24
- if not os.path.exists(CONFIG_DIR):
25
- os.makedirs(CONFIG_DIR)
23
+ CONFIG_DIR = Path.home() / ".mkv-episode-matcher"
24
+ if not CONFIG_DIR.exists():
25
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
26
26
 
27
27
  # Define the paths for the configuration file and cache directory
28
- CONFIG_FILE = os.path.join(CONFIG_DIR, "config.ini")
29
- CACHE_DIR = os.path.join(CONFIG_DIR, "cache")
28
+ CONFIG_FILE = CONFIG_DIR / "config.ini"
29
+ CACHE_DIR = CONFIG_DIR / "cache"
30
30
 
31
31
  # Check if the cache directory exists, if not create it
32
- if not os.path.exists(CACHE_DIR):
33
- os.makedirs(CACHE_DIR)
32
+ if not CACHE_DIR.exists():
33
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
34
34
 
35
35
  # Check if logs directory exists, if not create it
36
- log_dir = os.path.join(CONFIG_DIR, "logs")
37
- if not os.path.exists(log_dir):
38
- os.mkdir(log_dir)
36
+ log_dir = CONFIG_DIR / "logs"
37
+ if not log_dir.exists():
38
+ log_dir.mkdir(exist_ok=True)
39
39
  logger.remove()
40
40
  # Add a new handler for stdout logs
41
41
  logger.add(
42
- os.path.join(log_dir, "stdout.log"),
42
+ str(log_dir / "stdout.log"),
43
43
  format="{time} {level} {message}",
44
44
  level="INFO",
45
45
  rotation="10 MB",
46
46
  )
47
47
 
48
48
  # Add a new handler for error logs
49
- logger.add(os.path.join(log_dir, "stderr.log"), level="ERROR", rotation="10 MB")
49
+ logger.add(str(log_dir / "stderr.log"), level="ERROR", rotation="10 MB")
50
50
 
51
51
 
52
52
  def print_welcome_message():
@@ -104,7 +104,7 @@ def select_season(seasons):
104
104
  """
105
105
  console.print("[bold cyan]Available Seasons:[/bold cyan]")
106
106
  for i, season in enumerate(seasons, 1):
107
- season_num = os.path.basename(season).replace("Season ", "")
107
+ season_num = Path(season).name.replace("Season ", "")
108
108
  console.print(f" {i}. Season {season_num}")
109
109
 
110
110
  console.print(f" 0. All Seasons")
@@ -119,7 +119,7 @@ def select_season(seasons):
119
119
  return None
120
120
 
121
121
  selected_season = seasons[int(choice) - 1]
122
- return int(os.path.basename(selected_season).replace("Season ", ""))
122
+ return int(Path(selected_season).name.replace("Season ", ""))
123
123
 
124
124
 
125
125
  @logger.catch
@@ -239,7 +239,7 @@ def main():
239
239
  show_dir = Prompt.ask("Enter the main directory of the show")
240
240
 
241
241
  logger.info(f"Show Directory: {show_dir}")
242
- if not os.path.exists(show_dir):
242
+ if not Path(show_dir).exists():
243
243
  console.print(f"[bold red]Error:[/bold red] Show directory '{show_dir}' does not exist.")
244
244
  return
245
245
 
@@ -286,7 +286,7 @@ def main():
286
286
  selected_season = select_season(seasons)
287
287
 
288
288
  # Show what's going to happen
289
- show_name = os.path.basename(show_dir)
289
+ show_name = Path(show_dir).name
290
290
  season_text = f"Season {selected_season}" if selected_season else "all seasons"
291
291
 
292
292
  console.print(
@@ -1,7 +1,7 @@
1
1
  # config.py
2
2
  import configparser
3
3
  import multiprocessing
4
- import os
4
+ from pathlib import Path
5
5
 
6
6
  from loguru import logger
7
7
 
@@ -73,7 +73,7 @@ def get_config(file):
73
73
  """
74
74
  logger.info(f"Loading config from {file}")
75
75
  config = configparser.ConfigParser()
76
- if os.path.exists(file):
76
+ if Path(file).exists():
77
77
  config.read(file)
78
78
  return config["Config"] if "Config" in config else None
79
79
  return {}
@@ -1,7 +1,5 @@
1
1
  # mkv_episode_matcher/episode_matcher.py
2
2
 
3
- import glob
4
- import os
5
3
  import re
6
4
  import shutil
7
5
  from pathlib import Path
@@ -19,6 +17,7 @@ from mkv_episode_matcher.utils import (
19
17
  clean_text,
20
18
  get_subtitles,
21
19
  get_valid_seasons,
20
+ normalize_path,
22
21
  rename_episode_file,
23
22
  )
24
23
 
@@ -39,7 +38,7 @@ def process_show(season=None, dry_run=False, get_subs=False, verbose=False, conf
39
38
  """
40
39
  config = get_config(CONFIG_FILE)
41
40
  show_dir = config.get("show_dir")
42
- show_name = clean_text(os.path.basename(show_dir))
41
+ show_name = clean_text(normalize_path(show_dir).name)
43
42
  matcher = EpisodeMatcher(CACHE_DIR, show_name, min_confidence=confidence)
44
43
 
45
44
  # Early check for reference files
@@ -58,7 +57,7 @@ def process_show(season=None, dry_run=False, get_subs=False, verbose=False, conf
58
57
  return
59
58
 
60
59
  if season is not None:
61
- season_path = os.path.join(show_dir, f"Season {season}")
60
+ season_path = str(Path(show_dir) / f"Season {season}")
62
61
  if season_path not in season_paths:
63
62
  console.print(f"[bold red]Error:[/bold red] Season {season} has no .mkv files to process")
64
63
  return
@@ -69,12 +68,12 @@ def process_show(season=None, dry_run=False, get_subs=False, verbose=False, conf
69
68
 
70
69
  for season_path in season_paths:
71
70
  mkv_files = [
72
- f for f in glob.glob(os.path.join(season_path, "*.mkv"))
71
+ f for f in Path(season_path).glob("*.mkv")
73
72
  if not check_filename(f)
74
73
  ]
75
74
 
76
75
  if not mkv_files:
77
- season_num = os.path.basename(season_path).replace("Season ", "")
76
+ season_num = Path(season_path).name.replace("Season ", "")
78
77
  console.print(f"[dim]No new files to process in Season {season_num}[/dim]")
79
78
  continue
80
79
 
@@ -104,7 +103,7 @@ def process_show(season=None, dry_run=False, get_subs=False, verbose=False, conf
104
103
  task = progress.add_task(f"[cyan]Matching Season {season_num}[/cyan]", total=len(mkv_files))
105
104
 
106
105
  for mkv_file in mkv_files:
107
- file_basename = os.path.basename(mkv_file)
106
+ file_basename = Path(mkv_file).name
108
107
  progress.update(task, description=f"[cyan]Processing[/cyan] {file_basename}")
109
108
 
110
109
  if verbose:
@@ -1,5 +1,5 @@
1
- import os
2
1
  import re
2
+ from pathlib import Path
3
3
  from typing import Optional
4
4
 
5
5
 
@@ -55,8 +55,8 @@ def find_existing_subtitle(
55
55
  patterns = generate_subtitle_patterns(series_name, season, episode)
56
56
 
57
57
  for pattern in patterns:
58
- filepath = os.path.join(series_cache_dir, pattern)
59
- if os.path.exists(filepath):
58
+ filepath = Path(series_cache_dir) / pattern
59
+ if filepath.exists():
60
60
  return filepath
61
61
 
62
62
  return None
@@ -1,7 +1,8 @@
1
1
  # utils.py
2
- import os
3
2
  import re
4
3
  import shutil
4
+ import os
5
+ from pathlib import Path
5
6
 
6
7
  import requests
7
8
  import torch
@@ -16,6 +17,35 @@ from mkv_episode_matcher.subtitle_utils import find_existing_subtitle, sanitize_
16
17
  from mkv_episode_matcher.tmdb_client import fetch_season_details
17
18
 
18
19
  console = Console()
20
+
21
+
22
+ def normalize_path(path_str):
23
+ """
24
+ Normalize a path string to handle cross-platform path issues.
25
+ Properly handles trailing slashes and backslashes in both Windows and Unix paths.
26
+
27
+ Args:
28
+ path_str (str): The path string to normalize
29
+
30
+ Returns:
31
+ pathlib.Path: A normalized Path object
32
+ """
33
+ # Convert to string if it's a Path object
34
+ if isinstance(path_str, Path):
35
+ path_str = str(path_str)
36
+
37
+ # Remove trailing slashes or backslashes
38
+ path_str = path_str.rstrip('/').rstrip('\\')
39
+
40
+ # Handle Windows paths on non-Windows platforms
41
+ if os.name != 'nt' and '\\' in path_str and ':' in path_str[:2]:
42
+ # This looks like a Windows path on a non-Windows system
43
+ # Extract the last component which should be the directory/file name
44
+ components = path_str.split('\\')
45
+ return Path(components[-1])
46
+
47
+ return Path(path_str)
48
+
19
49
  def get_valid_seasons(show_dir):
20
50
  """
21
51
  Get all season directories that contain MKV files.
@@ -27,26 +57,28 @@ def get_valid_seasons(show_dir):
27
57
  list: List of paths to valid season directories
28
58
  """
29
59
  # Get all season directories
60
+ show_path = normalize_path(show_dir)
30
61
  season_paths = [
31
- os.path.join(show_dir, d)
32
- for d in os.listdir(show_dir)
33
- if os.path.isdir(os.path.join(show_dir, d))
62
+ str(show_path / d.name)
63
+ for d in show_path.iterdir()
64
+ if d.is_dir()
34
65
  ]
35
66
 
36
67
  # Filter seasons to only include those with .mkv files
37
68
  valid_season_paths = []
38
69
  for season_path in season_paths:
39
- mkv_files = [f for f in os.listdir(season_path) if f.endswith(".mkv")]
70
+ season_path_obj = Path(season_path)
71
+ mkv_files = [f for f in season_path_obj.iterdir() if f.name.endswith(".mkv")]
40
72
  if mkv_files:
41
73
  valid_season_paths.append(season_path)
42
74
 
43
75
  if not valid_season_paths:
44
76
  logger.warning(
45
- f"No seasons with .mkv files found in show '{os.path.basename(show_dir)}'"
77
+ f"No seasons with .mkv files found in show '{normalize_path(show_dir).name}'"
46
78
  )
47
79
  else:
48
80
  logger.info(
49
- f"Found {len(valid_season_paths)} seasons with .mkv files in '{os.path.basename(show_dir)}'"
81
+ f"Found {len(valid_season_paths)} seasons with .mkv files in '{normalize_path(show_dir).name}'"
50
82
  )
51
83
 
52
84
  return valid_season_paths
@@ -57,11 +89,14 @@ def check_filename(filename):
57
89
  Check if the filename is in the correct format (S01E02).
58
90
 
59
91
  Args:
60
- filename (str): The filename to check.
92
+ filename (str or Path): The filename to check.
61
93
 
62
94
  Returns:
63
95
  bool: True if the filename matches the expected pattern.
64
96
  """
97
+ # Convert Path object to string if needed
98
+ if isinstance(filename, Path):
99
+ filename = str(filename)
65
100
  # Check if the filename matches the expected format
66
101
  match = re.search(r".*S\d+E\d+", filename)
67
102
  return bool(match)
@@ -79,16 +114,14 @@ def scramble_filename(original_file_path, file_number):
79
114
  None
80
115
  """
81
116
  logger.info(f"Scrambling {original_file_path}")
82
- series_title = os.path.basename(
83
- os.path.dirname(os.path.dirname(original_file_path))
84
- )
85
- original_file_name = os.path.basename(original_file_path)
86
- extension = os.path.splitext(original_file_path)[-1]
117
+ series_title = normalize_path(original_file_path).parent.parent.name
118
+ original_file_name = Path(original_file_path).name
119
+ extension = Path(original_file_path).suffix
87
120
  new_file_name = f"{series_title} - {file_number:03d}{extension}"
88
- new_file_path = os.path.join(os.path.dirname(original_file_path), new_file_name)
89
- if not os.path.exists(new_file_path):
121
+ new_file_path = Path(original_file_path).parent / new_file_name
122
+ if not new_file_path.exists():
90
123
  logger.info(f"Renaming {original_file_name} -> {new_file_name}")
91
- os.rename(original_file_path, new_file_path)
124
+ Path(original_file_path).rename(new_file_path)
92
125
 
93
126
 
94
127
  def rename_episode_file(original_file_path, new_filename):
@@ -96,32 +129,32 @@ def rename_episode_file(original_file_path, new_filename):
96
129
  Rename an episode file with a standardized naming convention.
97
130
 
98
131
  Args:
99
- original_file_path (str): The original file path of the episode.
100
- new_filename (str): The new filename including season/episode info.
132
+ original_file_path (str or Path): The original file path of the episode.
133
+ new_filename (str or Path): The new filename including season/episode info.
101
134
 
102
135
  Returns:
103
- str: Path to the renamed file, or None if rename failed.
136
+ Path: Path to the renamed file, or None if rename failed.
104
137
  """
105
- original_dir = os.path.dirname(original_file_path)
106
- new_file_path = os.path.join(original_dir, new_filename)
138
+ original_dir = Path(original_file_path).parent
139
+ new_file_path = original_dir / new_filename
107
140
 
108
141
  # Check if new filepath already exists
109
- if os.path.exists(new_file_path):
142
+ if new_file_path.exists():
110
143
  logger.warning(f"File already exists: {new_filename}")
111
144
 
112
145
  # Add numeric suffix if file exists
113
- base, ext = os.path.splitext(new_filename)
146
+ base, ext = Path(new_filename).stem, Path(new_filename).suffix
114
147
  suffix = 2
115
148
  while True:
116
149
  new_filename = f"{base}_{suffix}{ext}"
117
- new_file_path = os.path.join(original_dir, new_filename)
118
- if not os.path.exists(new_file_path):
150
+ new_file_path = original_dir / new_filename
151
+ if not new_file_path.exists():
119
152
  break
120
153
  suffix += 1
121
154
 
122
155
  try:
123
- os.rename(original_file_path, new_file_path)
124
- logger.info(f"Renamed {os.path.basename(original_file_path)} -> {new_filename}")
156
+ Path(original_file_path).rename(new_file_path)
157
+ logger.info(f"Renamed {Path(original_file_path).name} -> {new_filename}")
125
158
  return new_file_path
126
159
  except OSError as e:
127
160
  logger.error(f"Failed to rename file: {e}")
@@ -143,7 +176,7 @@ def get_subtitles(show_id, seasons: set[int], config=None):
143
176
  if config is None:
144
177
  config = get_config(CONFIG_FILE)
145
178
  show_dir = config.get("show_dir")
146
- series_name = sanitize_filename(os.path.basename(show_dir))
179
+ series_name = sanitize_filename(normalize_path(show_dir).name)
147
180
  tmdb_api_key = config.get("tmdb_api_key")
148
181
  open_subtitles_api_key = config.get("open_subtitles_api_key")
149
182
  open_subtitles_user_agent = config.get("open_subtitles_user_agent")
@@ -175,7 +208,7 @@ def get_subtitles(show_id, seasons: set[int], config=None):
175
208
  for episode in range(1, episodes + 1):
176
209
  logger.info(f"Processing Season {season}, Episode {episode}...")
177
210
 
178
- series_cache_dir = os.path.join(CACHE_DIR, "data", series_name)
211
+ series_cache_dir = Path(CACHE_DIR) / "data" / series_name
179
212
  os.makedirs(series_cache_dir, exist_ok=True)
180
213
 
181
214
  # Check for existing subtitle in any supported format
@@ -185,15 +218,12 @@ def get_subtitles(show_id, seasons: set[int], config=None):
185
218
 
186
219
  if existing_subtitle:
187
220
  logger.info(
188
- f"Subtitle already exists: {os.path.basename(existing_subtitle)}"
221
+ f"Subtitle already exists: {Path(existing_subtitle).name}"
189
222
  )
190
223
  continue
191
224
 
192
225
  # Default to standard format for new downloads
193
- srt_filepath = os.path.join(
194
- series_cache_dir,
195
- f"{series_name} - S{season:02d}E{episode:02d}.srt",
196
- )
226
+ srt_filepath = str(series_cache_dir / f"{series_name} - S{season:02d}E{episode:02d}.srt")
197
227
 
198
228
  # get the episode info from TMDB
199
229
  url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season}/episode/{episode}?api_key={tmdb_api_key}"
@@ -241,17 +271,15 @@ def process_reference_srt_files(series_name):
241
271
  dict: A dictionary containing the reference files where the keys are the MKV filenames
242
272
  and the values are the corresponding SRT texts.
243
273
  """
244
- import os
245
-
246
274
  from mkv_episode_matcher.__main__ import CACHE_DIR
247
275
 
248
276
  reference_files = {}
249
- reference_dir = os.path.join(CACHE_DIR, "data", series_name)
277
+ reference_dir = Path(CACHE_DIR) / "data" / series_name
250
278
 
251
279
  for dirpath, _, filenames in os.walk(reference_dir):
252
280
  for filename in filenames:
253
281
  if filename.lower().endswith(".srt"):
254
- srt_file = os.path.join(dirpath, filename)
282
+ srt_file = Path(dirpath) / filename
255
283
  logger.info(f"Processing {srt_file}")
256
284
  srt_text = extract_srt_text(srt_file)
257
285
  season, episode = extract_season_episode(filename)
@@ -333,7 +361,7 @@ def process_srt_files(show_dir):
333
361
  for dirpath, _, filenames in os.walk(show_dir):
334
362
  for filename in filenames:
335
363
  if filename.lower().endswith(".srt"):
336
- srt_file = os.path.join(dirpath, filename)
364
+ srt_file = Path(dirpath) / filename
337
365
  logger.info(f"Processing {srt_file}")
338
366
  srt_text = extract_srt_text(srt_file)
339
367
  srt_files[srt_file] = srt_text
@@ -353,22 +381,20 @@ def compare_and_rename_files(srt_files, reference_files, dry_run=False):
353
381
  f"Comparing {len(srt_files)} srt files with {len(reference_files)} reference files"
354
382
  )
355
383
  for srt_text in srt_files.keys():
356
- parent_dir = os.path.dirname(os.path.dirname(srt_text))
384
+ parent_dir = Path(srt_text).parent.parent
357
385
  for reference in reference_files.keys():
358
386
  _season, _episode = extract_season_episode(reference)
359
- mkv_file = os.path.join(
360
- parent_dir, os.path.basename(srt_text).replace(".srt", ".mkv")
361
- )
387
+ mkv_file = str(parent_dir / Path(srt_text).name.replace(".srt", ".mkv"))
362
388
  matching_lines = compare_text(
363
389
  reference_files[reference], srt_files[srt_text]
364
390
  )
365
391
  if matching_lines >= int(len(reference_files[reference]) * 0.1):
366
392
  logger.info(f"Matching lines: {matching_lines}")
367
393
  logger.info(f"Found matching file: {mkv_file} ->{reference}")
368
- new_filename = os.path.join(parent_dir, reference)
394
+ new_filename = parent_dir / reference
369
395
  if not dry_run:
370
- logger.info(f"Renaming {mkv_file} to {new_filename}")
371
- rename_episode_file(mkv_file, new_filename)
396
+ logger.info(f"Renaming {mkv_file} to {str(new_filename)}")
397
+ rename_episode_file(mkv_file, reference)
372
398
 
373
399
 
374
400
  def compare_text(text1, text2):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkv-episode-matcher
3
- Version: 0.8.1
3
+ Version: 0.9.0
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
@@ -38,4 +38,6 @@ mkv_episode_matcher.egg-info/entry_points.txt
38
38
  mkv_episode_matcher.egg-info/requires.txt
39
39
  mkv_episode_matcher.egg-info/top_level.txt
40
40
  tests/__init__.py
41
- tests/test_main.py
41
+ tests/test_main.py
42
+ tests/test_path_handling.py
43
+ tests/test_trailing_slash.py
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = mkv_episode_matcher
3
- version = 0.8.1
3
+ version = 0.9.0
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,103 @@
1
+ # test_path_handling.py
2
+ import sys
3
+ import pytest
4
+ from pathlib import Path
5
+ import importlib
6
+ import unittest
7
+ from unittest import mock
8
+
9
+ # Add the parent directory to the path so we can import the project modules
10
+ sys.path.append(str(Path(__file__).parent.parent.absolute()))
11
+
12
+ # Import the modules we want to test
13
+ from mkv_episode_matcher.__main__ import main as main_module
14
+ from mkv_episode_matcher.episode_matcher import process_show
15
+ from mkv_episode_matcher.utils import check_filename, rename_episode_file, normalize_path
16
+
17
+ # Test paths to use in tests
18
+ TEST_PATHS = [
19
+ ("/mnt/c/Shows/Breaking Bad", "Breaking Bad"), # Normal path
20
+ ("/mnt/c/Shows/Breaking Bad/", "Breaking Bad"), # Linux trailing slash
21
+ ("/mnt/c/Shows/Breaking Bad\\", "Breaking Bad"), # Windows trailing backslash
22
+ ("/mnt/c/Shows/Breaking Bad//", "Breaking Bad"), # Double trailing slash
23
+ ("C:\\Shows\\The Office\\", "The Office"), # Windows path with trailing slash
24
+ ("/home/user/Shows/Game of Thrones/", "Game of Thrones"), # Another Linux path
25
+ ]
26
+
27
+ class TestPathLibImplementation(unittest.TestCase):
28
+ """Test the pathlib implementation used throughout the codebase"""
29
+
30
+ def test_show_name_extraction_with_pathlib(self):
31
+ """Test that normalize_path.name correctly extracts the show name even with trailing slash"""
32
+ # Path with trailing slash that previously caused bugs with os.path.basename
33
+ path_with_trailing_slash = "/mnt/c/Shows/Breaking Bad/"
34
+
35
+ # Expected correct show name
36
+ expected_show_name = "Breaking Bad"
37
+
38
+ # Our new implementation using normalize_path
39
+ extracted_name = normalize_path(path_with_trailing_slash).name
40
+
41
+ # This should succeed with our normalized path
42
+ self.assertEqual(extracted_name, expected_show_name,
43
+ "normalize_path.name should correctly extract the show name with trailing slash")
44
+
45
+ def test_check_filename_with_path_objects(self):
46
+ """Test that check_filename works with both Path objects and strings"""
47
+ # Test with string path
48
+ string_path = "/path/to/Show.S01E01.mkv"
49
+ self.assertTrue(check_filename(string_path), "check_filename should work with string paths")
50
+
51
+ # Test with Path object
52
+ path_object = Path("/path/to/Show.S01E01.mkv")
53
+ self.assertTrue(check_filename(path_object), "check_filename should work with Path objects")
54
+
55
+ def test_path_operations(self):
56
+ """Test various Path operations used in the codebase"""
57
+ base_path = Path("/mnt/c/Shows/Breaking Bad")
58
+
59
+ # Test Path joining with / operator
60
+ episode_path = base_path / "Season 1" / "Episode 1.mkv"
61
+ expected_path = Path("/mnt/c/Shows/Breaking Bad/Season 1/Episode 1.mkv")
62
+ self.assertEqual(episode_path, expected_path, "Path joining with / operator should work correctly")
63
+
64
+ # Test parent directory
65
+ self.assertEqual(episode_path.parent, Path("/mnt/c/Shows/Breaking Bad/Season 1"),
66
+ "Parent directory should be correctly identified")
67
+
68
+ # Test name extraction
69
+ self.assertEqual(episode_path.name, "Episode 1.mkv", "Filename should be correctly extracted")
70
+
71
+ # Test stem and suffix
72
+ self.assertEqual(episode_path.stem, "Episode 1", "File stem should be correctly extracted")
73
+ self.assertEqual(episode_path.suffix, ".mkv", "File extension should be correctly extracted")
74
+
75
+
76
+ class TestEpisodeMatcherShowNameExtraction(unittest.TestCase):
77
+ """Test the show name extraction in episode_matcher.py"""
78
+
79
+ @mock.patch('mkv_episode_matcher.config.get_config')
80
+ def test_episode_matcher_show_name_with_trailing_slash(self, mock_get_config):
81
+ """Test that process_show extracts show_name incorrectly with trailing slash"""
82
+ # Create a mock config that returns a path with trailing slash
83
+ mock_config = mock.MagicMock()
84
+ mock_config.get.return_value = "/mnt/c/Shows/Breaking Bad/"
85
+ mock_get_config.return_value = mock_config
86
+
87
+ # Import the module under test
88
+ import mkv_episode_matcher.episode_matcher as episode_matcher
89
+
90
+ # Access the function that should be affected by the bug
91
+ # This line simulates what happens in process_show() but we're just testing the show_name extraction
92
+ show_dir = mock_config.get("show_dir")
93
+
94
+ # How the code would extract show_name with normalize_path - this would work
95
+ fixed_show_name = normalize_path(show_dir).name
96
+ self.assertEqual(fixed_show_name, "Breaking Bad",
97
+ "normalize_path.name should extract correct show name even with trailing slash")
98
+
99
+ def test_pathlib_works_with_trailing_slashes():
100
+ """Test that normalize_path.name works correctly with trailing slashes"""
101
+ for path, expected in TEST_PATHS:
102
+ result = normalize_path(path).name
103
+ assert result == expected, f"Normalized path should extract correct name for: {path}"
@@ -0,0 +1,64 @@
1
+ # tests/test_trailing_slash.py
2
+
3
+ """
4
+ This test verifies that pathlib.Path.name correctly handles paths with trailing slashes.
5
+ """
6
+
7
+ from pathlib import Path
8
+ import sys
9
+ import os
10
+
11
+ # Add the parent directory to the path so we can import the project modules
12
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ from mkv_episode_matcher.utils import normalize_path
14
+
15
+ # Test paths with their expected basename
16
+ TEST_PATHS = [
17
+ ("/mnt/c/Shows/Breaking Bad", "Breaking Bad"), # Unix normal path
18
+ ("/mnt/c/Shows/Breaking Bad/", "Breaking Bad"), # Unix trailing slash
19
+ ("X:\\Shows\\Breaking Bad", "Breaking Bad"), # Windows normal path
20
+ ("X:\\Shows\\Breaking Bad\\", "Breaking Bad"), # Windows trailing backslash
21
+ ]
22
+
23
+ def test_pathlib_works_with_trailing_slash():
24
+ """Verify that pathlib.Path.name correctly handles paths with trailing slash."""
25
+ path_with_slash = "/mnt/c/Shows/Breaking Bad/"
26
+ result = normalize_path(path_with_slash).name
27
+ assert result == "Breaking Bad", "Path.name should extract correct name even with trailing slash"
28
+
29
+ def test_pathlib_works_with_trailing_backslash():
30
+ """Verify that pathlib.Path.name correctly handles paths with trailing backslash."""
31
+ path_with_backslash = "X:\\Shows\\Breaking Bad\\"
32
+ result = normalize_path(path_with_backslash).name
33
+ assert result == "Breaking Bad", "Path.name should extract correct name even with trailing backslash"
34
+
35
+ def test_pathlib_works_without_trailing_slash():
36
+ """Verify pathlib works correctly for paths without trailing slash."""
37
+ normal_path = "/mnt/c/Shows/Breaking Bad"
38
+ result = normalize_path(normal_path).name
39
+ assert result == "Breaking Bad", "Path.name should work for paths without trailing slash"
40
+
41
+ def test_path_parent_behavior():
42
+ """Test that Path.parent correctly handles paths with trailing slashes."""
43
+ path_with_slash = "/mnt/c/Shows/Breaking Bad/"
44
+ path_without_slash = "/mnt/c/Shows/Breaking Bad"
45
+
46
+ assert normalize_path(path_with_slash).parent == Path("/mnt/c/Shows"), "Parent should be correctly extracted"
47
+ assert normalize_path(path_without_slash).parent == Path("/mnt/c/Shows"), "Parent should be correctly extracted"
48
+
49
+ def test_path_stem_suffix():
50
+ """Test Path.stem and Path.suffix functionality."""
51
+ path = "/mnt/c/Shows/Breaking Bad/episode.mkv"
52
+ path_obj = Path(path)
53
+
54
+ assert path_obj.stem == "episode", "Stem should be correctly extracted"
55
+ assert path_obj.suffix == ".mkv", "Suffix should be correctly extracted"
56
+
57
+ def test_all_paths_with_pathlib():
58
+ """Test all the path formats with pathlib."""
59
+ for path, expected in TEST_PATHS:
60
+ # Check normalize_path.name always works
61
+ pathlib_result = normalize_path(path).name
62
+ assert pathlib_result == expected, f"Expected '{expected}' for normalize_path('{path}').name but got '{pathlib_result}'"
63
+ rstrip_result = normalize_path(path).name
64
+ assert rstrip_result == expected, f"Expected '{expected}' for normalize_path approach on '{path}' but got '{rstrip_result}'"
@@ -1 +0,0 @@
1
- 3.9