mkv-episode-matcher 0.5.0__py3-none-any.whl → 0.7.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.
Potentially problematic release.
This version of mkv-episode-matcher might be problematic. Click here for more details.
- mkv_episode_matcher/__init__.py +2 -2
- mkv_episode_matcher/__main__.py +222 -76
- mkv_episode_matcher/config.py +0 -3
- mkv_episode_matcher/episode_identification.py +164 -124
- mkv_episode_matcher/episode_matcher.py +102 -55
- mkv_episode_matcher/subtitle_utils.py +26 -25
- mkv_episode_matcher/utils.py +74 -57
- {mkv_episode_matcher-0.5.0.dist-info → mkv_episode_matcher-0.7.0.dist-info}/METADATA +10 -13
- mkv_episode_matcher-0.7.0.dist-info/RECORD +14 -0
- {mkv_episode_matcher-0.5.0.dist-info → mkv_episode_matcher-0.7.0.dist-info}/WHEEL +1 -1
- mkv_episode_matcher/libraries/pgs2srt/.gitignore +0 -2
- mkv_episode_matcher/libraries/pgs2srt/Libraries/SubZero/SubZero.py +0 -321
- mkv_episode_matcher/libraries/pgs2srt/Libraries/SubZero/dictionaries/data.py +0 -16700
- mkv_episode_matcher/libraries/pgs2srt/Libraries/SubZero/post_processing.py +0 -260
- mkv_episode_matcher/libraries/pgs2srt/README.md +0 -26
- mkv_episode_matcher/libraries/pgs2srt/__init__.py +0 -0
- mkv_episode_matcher/libraries/pgs2srt/imagemaker.py +0 -89
- mkv_episode_matcher/libraries/pgs2srt/pgs2srt.py +0 -150
- mkv_episode_matcher/libraries/pgs2srt/pgsreader.py +0 -225
- mkv_episode_matcher/libraries/pgs2srt/requirements.txt +0 -4
- mkv_episode_matcher/mkv_to_srt.py +0 -302
- mkv_episode_matcher-0.5.0.dist-info/RECORD +0 -25
- {mkv_episode_matcher-0.5.0.dist-info → mkv_episode_matcher-0.7.0.dist-info}/entry_points.txt +0 -0
- {mkv_episode_matcher-0.5.0.dist-info → mkv_episode_matcher-0.7.0.dist-info}/top_level.txt +0 -0
mkv_episode_matcher/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""MKV Episode Matcher package."""
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
4
|
|
|
4
5
|
try:
|
|
5
6
|
__version__ = version("mkv-episode-matcher")
|
|
6
7
|
except PackageNotFoundError:
|
|
7
8
|
# package is not installed
|
|
8
9
|
__version__ = "unknown"
|
|
9
|
-
|
mkv_episode_matcher/__main__.py
CHANGED
|
@@ -1,40 +1,47 @@
|
|
|
1
|
-
# __main__.py
|
|
1
|
+
# __main__.py (enhanced version)
|
|
2
2
|
import argparse
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
7
|
from loguru import logger
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
from rich.prompt import Confirm, Prompt
|
|
12
|
+
|
|
7
13
|
from mkv_episode_matcher import __version__
|
|
8
14
|
from mkv_episode_matcher.config import get_config, set_config
|
|
9
15
|
|
|
16
|
+
# Initialize rich console for better output
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
10
19
|
# Log the start of the application
|
|
11
20
|
logger.info("Starting the application")
|
|
12
21
|
|
|
13
|
-
|
|
14
22
|
# Check if the configuration directory exists, if not create it
|
|
15
|
-
|
|
16
|
-
|
|
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)
|
|
17
26
|
|
|
18
27
|
# Define the paths for the configuration file and cache directory
|
|
19
|
-
CONFIG_FILE = os.path.join(
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
CACHE_DIR = os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher", "cache")
|
|
28
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.ini")
|
|
29
|
+
CACHE_DIR = os.path.join(CONFIG_DIR, "cache")
|
|
23
30
|
|
|
24
31
|
# Check if the cache directory exists, if not create it
|
|
25
32
|
if not os.path.exists(CACHE_DIR):
|
|
26
33
|
os.makedirs(CACHE_DIR)
|
|
27
34
|
|
|
28
35
|
# Check if logs directory exists, if not create it
|
|
29
|
-
log_dir = os.path.join(
|
|
36
|
+
log_dir = os.path.join(CONFIG_DIR, "logs")
|
|
30
37
|
if not os.path.exists(log_dir):
|
|
31
38
|
os.mkdir(log_dir)
|
|
32
|
-
|
|
39
|
+
logger.remove()
|
|
33
40
|
# Add a new handler for stdout logs
|
|
34
41
|
logger.add(
|
|
35
42
|
os.path.join(log_dir, "stdout.log"),
|
|
36
43
|
format="{time} {level} {message}",
|
|
37
|
-
level="
|
|
44
|
+
level="INFO",
|
|
38
45
|
rotation="10 MB",
|
|
39
46
|
)
|
|
40
47
|
|
|
@@ -42,32 +49,96 @@ logger.add(
|
|
|
42
49
|
logger.add(os.path.join(log_dir, "stderr.log"), level="ERROR", rotation="10 MB")
|
|
43
50
|
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
def print_welcome_message():
|
|
53
|
+
"""Print a stylized welcome message."""
|
|
54
|
+
console.print(
|
|
55
|
+
Panel.fit(
|
|
56
|
+
f"[bold blue]MKV Episode Matcher v{__version__}[/bold blue]\n"
|
|
57
|
+
"[cyan]Automatically match and rename your MKV TV episodes[/cyan]",
|
|
58
|
+
border_style="blue",
|
|
59
|
+
padding=(1, 4),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
console.print()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def confirm_api_key(config_value: Optional[str], key_name: str, description: str) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Confirm if the user wants to use an existing API key or enter a new one.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config_value: The current value from the config
|
|
71
|
+
key_name: The name of the key
|
|
72
|
+
description: Description of the key for user information
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The API key to use
|
|
47
76
|
"""
|
|
48
|
-
|
|
77
|
+
if config_value:
|
|
78
|
+
console.print(f"[cyan]{key_name}:[/cyan] {description}")
|
|
79
|
+
console.print(f"Current value: [green]{mask_api_key(config_value)}[/green]")
|
|
80
|
+
if Confirm.ask("Use existing key?", default=True):
|
|
81
|
+
return config_value
|
|
82
|
+
|
|
83
|
+
return Prompt.ask(f"Enter your {key_name}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def mask_api_key(key: str) -> str:
|
|
87
|
+
"""Mask the API key for display purposes."""
|
|
88
|
+
if not key:
|
|
89
|
+
return ""
|
|
90
|
+
if len(key) <= 8:
|
|
91
|
+
return "*" * len(key)
|
|
92
|
+
return key[:4] + "*" * (len(key) - 8) + key[-4:]
|
|
93
|
+
|
|
49
94
|
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
def select_season(seasons):
|
|
96
|
+
"""
|
|
97
|
+
Allow user to select a season from a list.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
seasons: List of available seasons
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Selected season number or None for all seasons
|
|
104
|
+
"""
|
|
105
|
+
console.print("[bold cyan]Available Seasons:[/bold cyan]")
|
|
106
|
+
for i, season in enumerate(seasons, 1):
|
|
107
|
+
season_num = os.path.basename(season).replace("Season ", "")
|
|
108
|
+
console.print(f" {i}. Season {season_num}")
|
|
109
|
+
|
|
110
|
+
console.print(f" 0. All Seasons")
|
|
111
|
+
|
|
112
|
+
choice = Prompt.ask(
|
|
113
|
+
"Select a season number (0 for all)",
|
|
114
|
+
choices=[str(i) for i in range(len(seasons) + 1)],
|
|
115
|
+
default="0"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if int(choice) == 0:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
selected_season = seasons[int(choice) - 1]
|
|
122
|
+
return int(os.path.basename(selected_season).replace("Season ", ""))
|
|
52
123
|
|
|
53
|
-
Command-line arguments:
|
|
54
|
-
--tmdb-api-key: The API key for the TMDb API. If not provided, the function will try to get it from the cache or prompt the user to input it.
|
|
55
|
-
--show-dir: The main directory of the show. If not provided, the function will prompt the user to input it.
|
|
56
|
-
--season: The season number to be processed. If not provided, all seasons will be processed.
|
|
57
|
-
--dry-run: A boolean flag indicating whether to perform a dry run (i.e., not rename any files). If not provided, the function will rename files.
|
|
58
|
-
--get-subs: A boolean flag indicating whether to download subtitles for the show. If not provided, the function will not download subtitles.
|
|
59
|
-
--tesseract-path: The path to the tesseract executable. If not provided, the function will try to get it from the cache or prompt the user to input it.
|
|
60
124
|
|
|
61
|
-
|
|
125
|
+
@logger.catch
|
|
126
|
+
def main():
|
|
62
127
|
"""
|
|
128
|
+
Entry point of the application with enhanced user interface.
|
|
129
|
+
"""
|
|
130
|
+
print_welcome_message()
|
|
63
131
|
|
|
64
132
|
# Parse command-line arguments
|
|
65
|
-
parser = argparse.ArgumentParser(
|
|
133
|
+
parser = argparse.ArgumentParser(
|
|
134
|
+
description="Automatically match and rename your MKV TV episodes",
|
|
135
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
136
|
+
)
|
|
66
137
|
parser.add_argument(
|
|
67
138
|
"--version",
|
|
68
139
|
action="version",
|
|
69
140
|
version=f"%(prog)s {__version__}",
|
|
70
|
-
help="Show the version number and exit"
|
|
141
|
+
help="Show the version number and exit",
|
|
71
142
|
)
|
|
72
143
|
parser.add_argument("--tmdb-api-key", help="TMDb API key")
|
|
73
144
|
parser.add_argument("--show-dir", help="Main directory of the show")
|
|
@@ -76,41 +147,46 @@ def main():
|
|
|
76
147
|
type=int,
|
|
77
148
|
default=None,
|
|
78
149
|
nargs="?",
|
|
79
|
-
help="Specify the season number to be processed (default:
|
|
150
|
+
help="Specify the season number to be processed (default: all seasons)",
|
|
80
151
|
)
|
|
81
152
|
parser.add_argument(
|
|
82
153
|
"--dry-run",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
nargs="?",
|
|
86
|
-
help="Don't rename any files (default: None)",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Don't rename any files, just show what would happen",
|
|
87
156
|
)
|
|
88
157
|
parser.add_argument(
|
|
89
158
|
"--get-subs",
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
nargs="?",
|
|
93
|
-
help="Download subtitles for the show (default: None)",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Download subtitles for the show",
|
|
94
161
|
)
|
|
95
162
|
parser.add_argument(
|
|
96
|
-
"--
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
nargs="?",
|
|
100
|
-
help="Path to the tesseract executable (default: None)",
|
|
163
|
+
"--check-gpu",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Check if GPU is available for faster processing",
|
|
101
166
|
)
|
|
102
167
|
parser.add_argument(
|
|
103
|
-
"--
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
168
|
+
"--verbose", "-v",
|
|
169
|
+
action="store_true",
|
|
170
|
+
help="Enable verbose output",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--confidence",
|
|
174
|
+
type=float,
|
|
175
|
+
default=0.7,
|
|
176
|
+
help="Set confidence threshold for episode matching (0.0-1.0)",
|
|
108
177
|
)
|
|
178
|
+
|
|
109
179
|
args = parser.parse_args()
|
|
180
|
+
if args.verbose:
|
|
181
|
+
console.print("[bold cyan]Command-line Arguments[/bold cyan]")
|
|
182
|
+
console.print(args)
|
|
110
183
|
if args.check_gpu:
|
|
111
184
|
from mkv_episode_matcher.utils import check_gpu_support
|
|
112
|
-
|
|
185
|
+
with console.status("[bold green]Checking GPU support..."):
|
|
186
|
+
check_gpu_support()
|
|
113
187
|
return
|
|
188
|
+
|
|
189
|
+
|
|
114
190
|
logger.debug(f"Command-line arguments: {args}")
|
|
115
191
|
|
|
116
192
|
# Load configuration once
|
|
@@ -118,43 +194,61 @@ def main():
|
|
|
118
194
|
|
|
119
195
|
# Get TMDb API key
|
|
120
196
|
tmdb_api_key = args.tmdb_api_key or config.get("tmdb_api_key")
|
|
121
|
-
|
|
122
|
-
tmdb_api_key = input("Enter your TMDb API key: ")
|
|
123
|
-
logger.debug(f"TMDb API Key: {tmdb_api_key}")
|
|
124
|
-
|
|
125
|
-
logger.debug("Getting OpenSubtitles API key")
|
|
197
|
+
|
|
126
198
|
open_subtitles_api_key = config.get("open_subtitles_api_key")
|
|
127
199
|
open_subtitles_user_agent = config.get("open_subtitles_user_agent")
|
|
128
200
|
open_subtitles_username = config.get("open_subtitles_username")
|
|
129
201
|
open_subtitles_password = config.get("open_subtitles_password")
|
|
130
|
-
|
|
202
|
+
|
|
131
203
|
if args.get_subs:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
204
|
+
console.print("[bold cyan]Subtitle Download Configuration[/bold cyan]")
|
|
205
|
+
|
|
206
|
+
tmdb_api_key = confirm_api_key(
|
|
207
|
+
tmdb_api_key,
|
|
208
|
+
"TMDb API key",
|
|
209
|
+
"Used to lookup show and episode information"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
open_subtitles_api_key = confirm_api_key(
|
|
213
|
+
open_subtitles_api_key,
|
|
214
|
+
"OpenSubtitles API key",
|
|
215
|
+
"Required for subtitle downloads"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
open_subtitles_user_agent = confirm_api_key(
|
|
219
|
+
open_subtitles_user_agent,
|
|
220
|
+
"OpenSubtitles User Agent",
|
|
221
|
+
"Required for subtitle downloads"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
open_subtitles_username = confirm_api_key(
|
|
225
|
+
open_subtitles_username,
|
|
226
|
+
"OpenSubtitles Username",
|
|
227
|
+
"Account username for OpenSubtitles"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
open_subtitles_password = confirm_api_key(
|
|
231
|
+
open_subtitles_password,
|
|
232
|
+
"OpenSubtitles Password",
|
|
233
|
+
"Account password for OpenSubtitles"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Use config for show directory
|
|
142
237
|
show_dir = args.show_dir or config.get("show_dir")
|
|
143
238
|
if not show_dir:
|
|
144
|
-
show_dir =
|
|
239
|
+
show_dir = Prompt.ask("Enter the main directory of the show")
|
|
240
|
+
|
|
145
241
|
logger.info(f"Show Directory: {show_dir}")
|
|
242
|
+
if not os.path.exists(show_dir):
|
|
243
|
+
console.print(f"[bold red]Error:[/bold red] Show directory '{show_dir}' does not exist.")
|
|
244
|
+
return
|
|
245
|
+
|
|
146
246
|
if not show_dir:
|
|
147
247
|
show_dir = os.getcwd()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
tesseract_path = config.get("tesseract_path")
|
|
151
|
-
if not tesseract_path:
|
|
152
|
-
tesseract_path = input(r"Enter the path to the tesseract executable: ['C:\Program Files\Tesseract-OCR\tesseract.exe']")
|
|
153
|
-
else:
|
|
154
|
-
tesseract_path = args.tesseract_path
|
|
155
|
-
logger.debug(f"Teesseract Path: {tesseract_path}")
|
|
248
|
+
console.print(f"Using current directory: [cyan]{show_dir}[/cyan]")
|
|
249
|
+
|
|
156
250
|
logger.debug(f"Show Directory: {show_dir}")
|
|
157
|
-
|
|
251
|
+
|
|
158
252
|
# Set the configuration
|
|
159
253
|
set_config(
|
|
160
254
|
tmdb_api_key,
|
|
@@ -164,17 +258,69 @@ def main():
|
|
|
164
258
|
open_subtitles_password,
|
|
165
259
|
show_dir,
|
|
166
260
|
CONFIG_FILE,
|
|
167
|
-
tesseract_path=tesseract_path,
|
|
168
261
|
)
|
|
169
262
|
logger.info("Configuration set")
|
|
170
263
|
|
|
171
264
|
# Process the show
|
|
172
265
|
from mkv_episode_matcher.episode_matcher import process_show
|
|
266
|
+
from mkv_episode_matcher.utils import get_valid_seasons
|
|
173
267
|
|
|
174
|
-
|
|
175
|
-
|
|
268
|
+
console.print()
|
|
269
|
+
if args.dry_run:
|
|
270
|
+
console.print(
|
|
271
|
+
Panel.fit(
|
|
272
|
+
"[bold yellow]DRY RUN MODE[/bold yellow]\n"
|
|
273
|
+
"Files will not be renamed, only showing what would happen.",
|
|
274
|
+
border_style="yellow",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
seasons = get_valid_seasons(show_dir)
|
|
279
|
+
if not seasons:
|
|
280
|
+
console.print("[bold red]Error:[/bold red] No seasons with .mkv files found in the show directory.")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
# If season wasn't specified and there are multiple seasons, let user choose
|
|
284
|
+
selected_season = args.season
|
|
285
|
+
if selected_season is None and len(seasons) > 1:
|
|
286
|
+
selected_season = select_season(seasons)
|
|
287
|
+
|
|
288
|
+
# Show what's going to happen
|
|
289
|
+
show_name = os.path.basename(show_dir)
|
|
290
|
+
season_text = f"Season {selected_season}" if selected_season else "all seasons"
|
|
291
|
+
|
|
292
|
+
console.print(
|
|
293
|
+
f"[bold green]Processing[/bold green] [cyan]{show_name}[/cyan], {season_text}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# # Setup progress spinner
|
|
297
|
+
# with Progress(
|
|
298
|
+
# TextColumn("[bold green]Processing...[/bold green]"),
|
|
299
|
+
# console=console,
|
|
300
|
+
# ) as progress:
|
|
301
|
+
# task = progress.add_task("", total=None)
|
|
302
|
+
process_show(
|
|
303
|
+
selected_season,
|
|
304
|
+
dry_run=args.dry_run,
|
|
305
|
+
get_subs=args.get_subs,
|
|
306
|
+
verbose=args.verbose,
|
|
307
|
+
confidence=args.confidence
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
console.print("[bold green]✓[/bold green] Processing completed successfully!")
|
|
311
|
+
|
|
312
|
+
# Show where logs are stored
|
|
313
|
+
console.print(f"\n[dim]Logs available at: {log_dir}[/dim]")
|
|
176
314
|
|
|
177
315
|
|
|
178
316
|
# Run the main function if the script is run directly
|
|
179
317
|
if __name__ == "__main__":
|
|
180
|
-
|
|
318
|
+
try:
|
|
319
|
+
main()
|
|
320
|
+
except KeyboardInterrupt:
|
|
321
|
+
console.print("\n[yellow]Process interrupted by user.[/yellow]")
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
console.print(f"\n[bold red]Error:[/bold red] {str(e)}")
|
|
325
|
+
logger.exception("Unhandled exception")
|
|
326
|
+
sys.exit(1)
|
mkv_episode_matcher/config.py
CHANGED
|
@@ -27,7 +27,6 @@ def set_config(
|
|
|
27
27
|
open_subtitles_password,
|
|
28
28
|
show_dir,
|
|
29
29
|
file,
|
|
30
|
-
tesseract_path=None,
|
|
31
30
|
):
|
|
32
31
|
"""
|
|
33
32
|
Sets the configuration values and writes them to a file.
|
|
@@ -40,7 +39,6 @@ def set_config(
|
|
|
40
39
|
open_subtitles_password (str): The password for OpenSubtitles.
|
|
41
40
|
show_dir (str): The directory where the TV show episodes are located.
|
|
42
41
|
file (str): The path to the configuration file.
|
|
43
|
-
tesseract_path (str, optional): The path to the Tesseract OCR executable.
|
|
44
42
|
|
|
45
43
|
Returns:
|
|
46
44
|
None
|
|
@@ -54,7 +52,6 @@ def set_config(
|
|
|
54
52
|
"open_subtitles_user_agent": str(open_subtitles_user_agent),
|
|
55
53
|
"open_subtitles_username": str(open_subtitles_username),
|
|
56
54
|
"open_subtitles_password": str(open_subtitles_password),
|
|
57
|
-
"tesseract_path": str(tesseract_path),
|
|
58
55
|
}
|
|
59
56
|
logger.info(
|
|
60
57
|
f"Setting config with API:{tmdb_api_key}, show_dir: {show_dir}, and max_threads: {MAX_THREADS}"
|