mkv-episode-matcher 0.3.3__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. mkv_episode_matcher/__init__.py +8 -0
  2. mkv_episode_matcher/__main__.py +2 -177
  3. mkv_episode_matcher/asr_models.py +506 -0
  4. mkv_episode_matcher/cli.py +558 -0
  5. mkv_episode_matcher/core/config_manager.py +100 -0
  6. mkv_episode_matcher/core/engine.py +577 -0
  7. mkv_episode_matcher/core/matcher.py +214 -0
  8. mkv_episode_matcher/core/models.py +91 -0
  9. mkv_episode_matcher/core/providers/asr.py +85 -0
  10. mkv_episode_matcher/core/providers/subtitles.py +341 -0
  11. mkv_episode_matcher/core/utils.py +148 -0
  12. mkv_episode_matcher/episode_identification.py +550 -118
  13. mkv_episode_matcher/subtitle_utils.py +82 -0
  14. mkv_episode_matcher/tmdb_client.py +56 -14
  15. mkv_episode_matcher/ui/flet_app.py +708 -0
  16. mkv_episode_matcher/utils.py +262 -139
  17. mkv_episode_matcher-1.0.0.dist-info/METADATA +242 -0
  18. mkv_episode_matcher-1.0.0.dist-info/RECORD +23 -0
  19. {mkv_episode_matcher-0.3.3.dist-info → mkv_episode_matcher-1.0.0.dist-info}/WHEEL +1 -1
  20. mkv_episode_matcher-1.0.0.dist-info/licenses/LICENSE +21 -0
  21. mkv_episode_matcher/config.py +0 -82
  22. mkv_episode_matcher/episode_matcher.py +0 -100
  23. mkv_episode_matcher/libraries/pgs2srt/.gitignore +0 -2
  24. mkv_episode_matcher/libraries/pgs2srt/Libraries/SubZero/SubZero.py +0 -321
  25. mkv_episode_matcher/libraries/pgs2srt/Libraries/SubZero/dictionaries/data.py +0 -16700
  26. mkv_episode_matcher/libraries/pgs2srt/Libraries/SubZero/post_processing.py +0 -260
  27. mkv_episode_matcher/libraries/pgs2srt/README.md +0 -26
  28. mkv_episode_matcher/libraries/pgs2srt/__init__.py +0 -0
  29. mkv_episode_matcher/libraries/pgs2srt/imagemaker.py +0 -89
  30. mkv_episode_matcher/libraries/pgs2srt/pgs2srt.py +0 -150
  31. mkv_episode_matcher/libraries/pgs2srt/pgsreader.py +0 -225
  32. mkv_episode_matcher/libraries/pgs2srt/requirements.txt +0 -4
  33. mkv_episode_matcher/mkv_to_srt.py +0 -302
  34. mkv_episode_matcher/speech_to_text.py +0 -90
  35. mkv_episode_matcher-0.3.3.dist-info/METADATA +0 -125
  36. mkv_episode_matcher-0.3.3.dist-info/RECORD +0 -25
  37. {mkv_episode_matcher-0.3.3.dist-info → mkv_episode_matcher-1.0.0.dist-info}/entry_points.txt +0 -0
  38. {mkv_episode_matcher-0.3.3.dist-info → mkv_episode_matcher-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,82 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+
5
+ def generate_subtitle_patterns(
6
+ series_name: str, season: int, episode: int
7
+ ) -> list[str]:
8
+ """
9
+ Generate various common subtitle filename patterns.
10
+
11
+ Args:
12
+ series_name (str): Name of the series
13
+ season (int): Season number
14
+ episode (int): Episode number
15
+
16
+ Returns:
17
+ List[str]: List of possible subtitle filenames
18
+ """
19
+ patterns = [
20
+ # Standard format: "Show Name - S01E02.srt"
21
+ f"{series_name} - S{season:02d}E{episode:02d}.srt",
22
+ # Season x Episode format: "Show Name - 1x02.srt"
23
+ f"{series_name} - {season}x{episode:02d}.srt",
24
+ # Separate season/episode: "Show Name - Season 1 Episode 02.srt"
25
+ f"{series_name} - Season {season} Episode {episode:02d}.srt",
26
+ # Compact format: "ShowName.S01E02.srt"
27
+ f"{series_name.replace(' ', '')}.S{season:02d}E{episode:02d}.srt",
28
+ # Numbered format: "Show Name 102.srt"
29
+ f"{series_name} {season:01d}{episode:02d}.srt",
30
+ # Dot format: "Show.Name.1x02.srt"
31
+ f"{series_name.replace(' ', '.')}.{season}x{episode:02d}.srt",
32
+ # Underscore format: "Show_Name_S01E02.srt"
33
+ f"{series_name.replace(' ', '_')}_S{season:02d}E{episode:02d}.srt",
34
+ ]
35
+
36
+ return patterns
37
+
38
+
39
+ def find_existing_subtitle(
40
+ series_cache_dir: str, series_name: str, season: int, episode: int
41
+ ) -> str | None:
42
+ """
43
+ Check for existing subtitle files in various naming formats.
44
+
45
+ Args:
46
+ series_cache_dir (str): Directory containing subtitle files
47
+ series_name (str): Name of the series
48
+ season (int): Season number
49
+ episode (int): Episode number
50
+
51
+ Returns:
52
+ Optional[str]: Path to existing subtitle file if found, None otherwise
53
+ """
54
+ patterns = generate_subtitle_patterns(series_name, season, episode)
55
+
56
+ for pattern in patterns:
57
+ filepath = Path(series_cache_dir) / pattern
58
+ if filepath.exists():
59
+ return filepath
60
+
61
+ return None
62
+
63
+
64
+ def sanitize_filename(filename: str) -> str:
65
+ """
66
+ Sanitize filename by removing/replacing invalid characters.
67
+
68
+ Args:
69
+ filename (str): Original filename
70
+
71
+ Returns:
72
+ str: Sanitized filename
73
+ """
74
+ # Replace problematic characters
75
+ filename = filename.replace(":", " -")
76
+ filename = filename.replace("/", "-")
77
+ filename = filename.replace("\\", "-")
78
+
79
+ # Remove any other invalid characters
80
+ filename = re.sub(r'[<>:"/\\|?*]', "", filename)
81
+
82
+ return filename.strip()
@@ -1,12 +1,51 @@
1
1
  # tmdb_client.py
2
2
  import time
3
+ from functools import wraps
3
4
  from threading import Lock
5
+ from typing import Any, Callable, TypeVar
4
6
 
5
7
  import requests
6
8
  from loguru import logger
7
9
 
8
- from mkv_episode_matcher.__main__ import CONFIG_FILE
9
- from mkv_episode_matcher.config import get_config
10
+ F = TypeVar("F", bound=Callable[..., Any])
11
+
12
+
13
+ def retry_network_operation(
14
+ max_retries: int = 3, base_delay: float = 1.0
15
+ ) -> Callable[[F], F]:
16
+ """Decorator for retrying network operations."""
17
+
18
+ def decorator(func: F) -> F:
19
+ @wraps(func)
20
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
21
+ last_exception = None
22
+ delay = base_delay
23
+
24
+ for attempt in range(max_retries + 1):
25
+ try:
26
+ return func(*args, **kwargs)
27
+ except (requests.RequestException, ConnectionError, TimeoutError) as e:
28
+ last_exception = e
29
+ if attempt == max_retries:
30
+ logger.error(
31
+ f"Max retries ({max_retries}) exceeded for {func.__name__}: {e}"
32
+ )
33
+ raise e
34
+
35
+ logger.warning(
36
+ f"Network retry {attempt + 1}/{max_retries + 1} for {func.__name__}: {e}"
37
+ )
38
+ time.sleep(delay)
39
+ delay = min(delay * 2, 30) # Cap at 30 seconds
40
+
41
+ raise last_exception
42
+
43
+ return wrapper # type: ignore
44
+
45
+ return decorator
46
+
47
+
48
+ from mkv_episode_matcher.core.config_manager import get_config_manager
10
49
 
11
50
  BASE_IMAGE_URL = "https://image.tmdb.org/t/p/original"
12
51
 
@@ -50,7 +89,7 @@ class RateLimitedRequest:
50
89
 
51
90
  self.requests_made += 1
52
91
 
53
- response = requests.get(url)
92
+ response = requests.get(url, timeout=30)
54
93
  return response
55
94
 
56
95
 
@@ -58,7 +97,8 @@ class RateLimitedRequest:
58
97
  rate_limited_request = RateLimitedRequest(rate_limit=30, period=1)
59
98
 
60
99
 
61
- def fetch_show_id(show_name):
100
+ @retry_network_operation(max_retries=3, base_delay=1.0)
101
+ def fetch_show_id(show_name: str) -> str | None:
62
102
  """
63
103
  Fetch the TMDb ID for a given show name.
64
104
 
@@ -68,8 +108,8 @@ def fetch_show_id(show_name):
68
108
  Returns:
69
109
  str: The TMDb ID of the show, or None if not found.
70
110
  """
71
- config = get_config(CONFIG_FILE)
72
- tmdb_api_key = config.get("tmdb_api_key")
111
+ config = get_config_manager().load()
112
+ tmdb_api_key = config.tmdb_api_key
73
113
  url = f"https://api.themoviedb.org/3/search/tv?query={show_name}&api_key={tmdb_api_key}"
74
114
  response = requests.get(url)
75
115
  if response.status_code == 200:
@@ -79,7 +119,8 @@ def fetch_show_id(show_name):
79
119
  return None
80
120
 
81
121
 
82
- def fetch_season_details(show_id, season_number):
122
+ @retry_network_operation(max_retries=3, base_delay=1.0)
123
+ def fetch_season_details(show_id: str, season_number: int) -> int:
83
124
  """
84
125
  Fetch the total number of episodes for a given show and season from the TMDb API.
85
126
 
@@ -91,11 +132,11 @@ def fetch_season_details(show_id, season_number):
91
132
  int: The total number of episodes in the season, or 0 if the API request failed.
92
133
  """
93
134
  logger.info(f"Fetching season details for Season {season_number}...")
94
- config = get_config(CONFIG_FILE)
95
- tmdb_api_key = config.get("tmdb_api_key")
135
+ config = get_config_manager().load()
136
+ tmdb_api_key = config.tmdb_api_key
96
137
  url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season_number}?api_key={tmdb_api_key}"
97
138
  try:
98
- response = requests.get(url)
139
+ response = requests.get(url, timeout=30)
99
140
  response.raise_for_status()
100
141
  season_data = response.json()
101
142
  total_episodes = len(season_data.get("episodes", []))
@@ -110,7 +151,8 @@ def fetch_season_details(show_id, season_number):
110
151
  return 0
111
152
 
112
153
 
113
- def get_number_of_seasons(show_id):
154
+ @retry_network_operation(max_retries=3, base_delay=1.0)
155
+ def get_number_of_seasons(show_id: str) -> int:
114
156
  """
115
157
  Retrieves the number of seasons for a given TV show from the TMDB API.
116
158
 
@@ -123,10 +165,10 @@ def get_number_of_seasons(show_id):
123
165
  Raises:
124
166
  - requests.HTTPError: If there is an error while making the API request.
125
167
  """
126
- config = get_config(CONFIG_FILE)
127
- tmdb_api_key = config.get("tmdb_api_key")
168
+ config = get_config_manager().load()
169
+ tmdb_api_key = config.tmdb_api_key
128
170
  url = f"https://api.themoviedb.org/3/tv/{show_id}?api_key={tmdb_api_key}"
129
- response = requests.get(url)
171
+ response = requests.get(url, timeout=30)
130
172
  response.raise_for_status()
131
173
  show_data = response.json()
132
174
  num_seasons = show_data.get("number_of_seasons", 0)