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.
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.coverage +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/tests.yml +1 -8
- mkv_episode_matcher-0.9.0/.python-version +1 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/CHANGELOG.md +17 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/PKG-INFO +1 -1
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/__main__.py +17 -17
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/config.py +2 -2
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/episode_matcher.py +6 -7
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/subtitle_utils.py +3 -3
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/utils.py +72 -46
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/PKG-INFO +1 -1
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/SOURCES.txt +3 -1
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/setup.cfg +1 -1
- mkv_episode_matcher-0.9.0/tests/test_path_handling.py +103 -0
- mkv_episode_matcher-0.9.0/tests/test_trailing_slash.py +64 -0
- mkv_episode_matcher-0.8.1/.python-version +0 -1
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.gitattributes +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/funding.yml +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/documentation.yml +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/python-publish.yml +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.gitignore +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.gitmodules +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.vscode/settings.json +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/README.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/api/index.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/changelog.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/cli.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/configuration.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/installation.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/quickstart.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/docs/tips.md +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkdocs.yml +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/.gitattributes +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/__init__.py +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/episode_identification.py +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/tmdb_client.py +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/dependency_links.txt +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/entry_points.txt +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/requires.txt +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/top_level.txt +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/pyproject.toml +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/setup.py +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/tests/__init__.py +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/tests/test_main.py +0 -0
- {mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/uv.lock +0 -0
|
Binary file
|
|
@@ -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:
|
|
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.
|
|
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 =
|
|
24
|
-
if not
|
|
25
|
-
|
|
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 =
|
|
29
|
-
CACHE_DIR =
|
|
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
|
|
33
|
-
|
|
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 =
|
|
37
|
-
if not
|
|
38
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
76
|
+
if Path(file).exists():
|
|
77
77
|
config.read(file)
|
|
78
78
|
return config["Config"] if "Config" in config else None
|
|
79
79
|
return {}
|
{mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/episode_matcher.py
RENAMED
|
@@ -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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
{mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher/subtitle_utils.py
RENAMED
|
@@ -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 =
|
|
59
|
-
if
|
|
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
|
-
|
|
32
|
-
for d in
|
|
33
|
-
if
|
|
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
|
-
|
|
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 '{
|
|
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 '{
|
|
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 =
|
|
83
|
-
|
|
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 =
|
|
89
|
-
if not
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
Path: Path to the renamed file, or None if rename failed.
|
|
104
137
|
"""
|
|
105
|
-
original_dir =
|
|
106
|
-
new_file_path =
|
|
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
|
|
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 =
|
|
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 =
|
|
118
|
-
if not
|
|
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
|
-
|
|
124
|
-
logger.info(f"Renamed {
|
|
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(
|
|
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 =
|
|
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: {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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):
|
{mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkv-episode-matcher
|
|
3
|
-
Version: 0.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = mkv_episode_matcher
|
|
3
|
-
version = 0.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/.github/workflows/python-publish.yml
RENAMED
|
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
|
{mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/requires.txt
RENAMED
|
File without changes
|
{mkv_episode_matcher-0.8.1 → mkv_episode_matcher-0.9.0}/mkv_episode_matcher.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|