mkv-episode-matcher 0.6.0__py3-none-any.whl → 0.7.1__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/__main__.py +215 -54
- mkv_episode_matcher/episode_identification.py +3 -2
- mkv_episode_matcher/episode_matcher.py +90 -23
- mkv_episode_matcher/utils.py +19 -2
- {mkv_episode_matcher-0.6.0.dist-info → mkv_episode_matcher-0.7.1.dist-info}/METADATA +7 -4
- mkv_episode_matcher-0.7.1.dist-info/RECORD +14 -0
- {mkv_episode_matcher-0.6.0.dist-info → mkv_episode_matcher-0.7.1.dist-info}/WHEEL +1 -1
- mkv_episode_matcher-0.6.0.dist-info/RECORD +0 -14
- {mkv_episode_matcher-0.6.0.dist-info → mkv_episode_matcher-0.7.1.dist-info}/entry_points.txt +0 -0
- {mkv_episode_matcher-0.6.0.dist-info → mkv_episode_matcher-0.7.1.dist-info}/top_level.txt +0 -0
mkv_episode_matcher/__main__.py
CHANGED
|
@@ -1,35 +1,42 @@
|
|
|
1
|
-
# __main__.py
|
|
1
|
+
# __main__.py (enhanced version)
|
|
2
2
|
import argparse
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
4
6
|
|
|
5
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
|
|
6
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"),
|
|
@@ -42,26 +49,91 @@ 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:
|
|
47
66
|
"""
|
|
48
|
-
|
|
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
|
|
76
|
+
"""
|
|
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
|
+
|
|
49
85
|
|
|
50
|
-
|
|
51
|
-
|
|
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:]
|
|
52
93
|
|
|
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
94
|
|
|
60
|
-
|
|
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
|
|
61
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 ", ""))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@logger.catch
|
|
126
|
+
def main():
|
|
127
|
+
"""
|
|
128
|
+
Entry point of the application with enhanced user interface.
|
|
129
|
+
"""
|
|
130
|
+
print_welcome_message()
|
|
62
131
|
|
|
63
132
|
# Parse command-line arguments
|
|
64
|
-
parser = argparse.ArgumentParser(
|
|
133
|
+
parser = argparse.ArgumentParser(
|
|
134
|
+
description="Automatically match and rename your MKV TV episodes",
|
|
135
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
136
|
+
)
|
|
65
137
|
parser.add_argument(
|
|
66
138
|
"--version",
|
|
67
139
|
action="version",
|
|
@@ -75,35 +147,46 @@ def main():
|
|
|
75
147
|
type=int,
|
|
76
148
|
default=None,
|
|
77
149
|
nargs="?",
|
|
78
|
-
help="Specify the season number to be processed (default:
|
|
150
|
+
help="Specify the season number to be processed (default: all seasons)",
|
|
79
151
|
)
|
|
80
152
|
parser.add_argument(
|
|
81
153
|
"--dry-run",
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
nargs="?",
|
|
85
|
-
help="Don't rename any files (default: None)",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Don't rename any files, just show what would happen",
|
|
86
156
|
)
|
|
87
157
|
parser.add_argument(
|
|
88
158
|
"--get-subs",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
nargs="?",
|
|
92
|
-
help="Download subtitles for the show (default: None)",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Download subtitles for the show",
|
|
93
161
|
)
|
|
94
162
|
parser.add_argument(
|
|
95
163
|
"--check-gpu",
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Check if GPU is available for faster processing",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--verbose", "-v",
|
|
169
|
+
action="store_true",
|
|
170
|
+
help="Enable verbose output",
|
|
100
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)",
|
|
177
|
+
)
|
|
178
|
+
|
|
101
179
|
args = parser.parse_args()
|
|
180
|
+
if args.verbose:
|
|
181
|
+
console.print("[bold cyan]Command-line Arguments[/bold cyan]")
|
|
182
|
+
console.print(args)
|
|
102
183
|
if args.check_gpu:
|
|
103
184
|
from mkv_episode_matcher.utils import check_gpu_support
|
|
104
|
-
|
|
105
|
-
|
|
185
|
+
with console.status("[bold green]Checking GPU support..."):
|
|
186
|
+
check_gpu_support()
|
|
106
187
|
return
|
|
188
|
+
|
|
189
|
+
|
|
107
190
|
logger.debug(f"Command-line arguments: {args}")
|
|
108
191
|
|
|
109
192
|
# Load configuration once
|
|
@@ -112,32 +195,57 @@ def main():
|
|
|
112
195
|
# Get TMDb API key
|
|
113
196
|
tmdb_api_key = args.tmdb_api_key or config.get("tmdb_api_key")
|
|
114
197
|
|
|
115
|
-
logger.debug("Getting OpenSubtitles API key")
|
|
116
198
|
open_subtitles_api_key = config.get("open_subtitles_api_key")
|
|
117
199
|
open_subtitles_user_agent = config.get("open_subtitles_user_agent")
|
|
118
200
|
open_subtitles_username = config.get("open_subtitles_username")
|
|
119
201
|
open_subtitles_password = config.get("open_subtitles_password")
|
|
120
202
|
|
|
121
203
|
if args.get_subs:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
)
|
|
133
235
|
|
|
134
236
|
# Use config for show directory
|
|
135
237
|
show_dir = args.show_dir or config.get("show_dir")
|
|
136
238
|
if not show_dir:
|
|
137
|
-
show_dir =
|
|
239
|
+
show_dir = Prompt.ask("Enter the main directory of the show")
|
|
240
|
+
|
|
138
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
|
+
|
|
139
246
|
if not show_dir:
|
|
140
247
|
show_dir = os.getcwd()
|
|
248
|
+
console.print(f"Using current directory: [cyan]{show_dir}[/cyan]")
|
|
141
249
|
|
|
142
250
|
logger.debug(f"Show Directory: {show_dir}")
|
|
143
251
|
|
|
@@ -155,11 +263,64 @@ def main():
|
|
|
155
263
|
|
|
156
264
|
# Process the show
|
|
157
265
|
from mkv_episode_matcher.episode_matcher import process_show
|
|
266
|
+
from mkv_episode_matcher.utils import get_valid_seasons
|
|
158
267
|
|
|
159
|
-
|
|
160
|
-
|
|
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]")
|
|
161
314
|
|
|
162
315
|
|
|
163
316
|
# Run the main function if the script is run directly
|
|
164
317
|
if __name__ == "__main__":
|
|
165
|
-
|
|
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)
|
|
@@ -2,7 +2,8 @@ import re
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import tempfile
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
|
|
5
|
+
from rich import print
|
|
6
|
+
from rich.console import Console
|
|
6
7
|
import chardet
|
|
7
8
|
import numpy as np
|
|
8
9
|
import torch
|
|
@@ -10,6 +11,7 @@ import whisper
|
|
|
10
11
|
from loguru import logger
|
|
11
12
|
from rapidfuzz import fuzz
|
|
12
13
|
|
|
14
|
+
console = Console()
|
|
13
15
|
|
|
14
16
|
class EpisodeMatcher:
|
|
15
17
|
def __init__(self, cache_dir, show_name, min_confidence=0.6):
|
|
@@ -143,7 +145,6 @@ class EpisodeMatcher:
|
|
|
143
145
|
logger.info(
|
|
144
146
|
f"No match found at {start_time} seconds (best confidence: {best_confidence:.2f})"
|
|
145
147
|
)
|
|
146
|
-
|
|
147
148
|
return None
|
|
148
149
|
|
|
149
150
|
def identify_episode(self, video_file, temp_dir, season_number):
|
|
@@ -7,6 +7,8 @@ import shutil
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from loguru import logger
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn
|
|
10
12
|
|
|
11
13
|
from mkv_episode_matcher.__main__ import CACHE_DIR, CONFIG_FILE
|
|
12
14
|
from mkv_episode_matcher.config import get_config
|
|
@@ -20,40 +22,60 @@ from mkv_episode_matcher.utils import (
|
|
|
20
22
|
rename_episode_file,
|
|
21
23
|
)
|
|
22
24
|
|
|
25
|
+
# Initialize Rich console
|
|
26
|
+
console = Console()
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
|
|
29
|
+
def process_show(season=None, dry_run=False, get_subs=False, verbose=False, confidence=0.6):
|
|
30
|
+
"""
|
|
31
|
+
Process the show using streaming speech recognition with improved UI feedback.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
season (int, optional): Season number to process. Defaults to None (all seasons).
|
|
35
|
+
dry_run (bool): If True, only simulate actions without making changes.
|
|
36
|
+
get_subs (bool): If True, download subtitles for the show.
|
|
37
|
+
verbose (bool): If True, display more detailed progress information.
|
|
38
|
+
confidence (float): Confidence threshold for episode matching (0.0-1.0).
|
|
39
|
+
"""
|
|
26
40
|
config = get_config(CONFIG_FILE)
|
|
27
41
|
show_dir = config.get("show_dir")
|
|
28
42
|
show_name = clean_text(os.path.basename(show_dir))
|
|
29
|
-
matcher = EpisodeMatcher(CACHE_DIR, show_name)
|
|
43
|
+
matcher = EpisodeMatcher(CACHE_DIR, show_name, min_confidence=confidence)
|
|
30
44
|
|
|
31
45
|
# Early check for reference files
|
|
32
46
|
reference_dir = Path(CACHE_DIR) / "data" / show_name
|
|
33
47
|
reference_files = list(reference_dir.glob("*.srt"))
|
|
34
48
|
if (not get_subs) and (not reference_files):
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
console.print(
|
|
50
|
+
f"[bold yellow]Warning:[/bold yellow] No reference subtitle files found in {reference_dir}"
|
|
51
|
+
)
|
|
52
|
+
console.print("[cyan]Tip:[/cyan] Use --get-subs to download reference subtitles")
|
|
37
53
|
return
|
|
38
54
|
|
|
39
55
|
season_paths = get_valid_seasons(show_dir)
|
|
40
56
|
if not season_paths:
|
|
41
|
-
|
|
57
|
+
console.print("[bold red]Error:[/bold red] No seasons with .mkv files found")
|
|
42
58
|
return
|
|
43
59
|
|
|
44
60
|
if season is not None:
|
|
45
61
|
season_path = os.path.join(show_dir, f"Season {season}")
|
|
46
62
|
if season_path not in season_paths:
|
|
47
|
-
|
|
63
|
+
console.print(f"[bold red]Error:[/bold red] Season {season} has no .mkv files to process")
|
|
48
64
|
return
|
|
49
65
|
season_paths = [season_path]
|
|
50
66
|
|
|
67
|
+
total_processed = 0
|
|
68
|
+
total_matched = 0
|
|
69
|
+
|
|
51
70
|
for season_path in season_paths:
|
|
52
|
-
mkv_files = [
|
|
53
|
-
|
|
71
|
+
mkv_files = [
|
|
72
|
+
f for f in glob.glob(os.path.join(season_path, "*.mkv"))
|
|
73
|
+
if not check_filename(f)
|
|
74
|
+
]
|
|
54
75
|
|
|
55
76
|
if not mkv_files:
|
|
56
|
-
|
|
77
|
+
season_num = os.path.basename(season_path).replace("Season ", "")
|
|
78
|
+
console.print(f"[dim]No new files to process in Season {season_num}[/dim]")
|
|
57
79
|
continue
|
|
58
80
|
|
|
59
81
|
season_num = int(re.search(r'Season (\d+)', season_path).group(1))
|
|
@@ -64,22 +86,67 @@ def process_show(season=None, dry_run=False, get_subs=False):
|
|
|
64
86
|
if get_subs:
|
|
65
87
|
show_id = fetch_show_id(matcher.show_name)
|
|
66
88
|
if show_id:
|
|
89
|
+
console.print(f"[bold cyan]Downloading subtitles for Season {season_num}...[/bold cyan]")
|
|
67
90
|
get_subtitles(show_id, seasons={season_num}, config=config)
|
|
91
|
+
else:
|
|
92
|
+
console.print("[bold red]Error:[/bold red] Could not find show ID. Skipping subtitle download.")
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
console.print(f"[bold cyan]Processing {len(mkv_files)} files in Season {season_num}...[/bold cyan]")
|
|
95
|
+
|
|
96
|
+
# Process files with a progress bar
|
|
97
|
+
with Progress(
|
|
98
|
+
TextColumn("[progress.description]{task.description}"),
|
|
99
|
+
BarColumn(),
|
|
100
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
101
|
+
TimeElapsedColumn(),
|
|
102
|
+
console=console,
|
|
103
|
+
) as progress:
|
|
104
|
+
task = progress.add_task(f"[cyan]Matching Season {season_num}[/cyan]", total=len(mkv_files))
|
|
105
|
+
|
|
106
|
+
for mkv_file in mkv_files:
|
|
107
|
+
file_basename = os.path.basename(mkv_file)
|
|
108
|
+
progress.update(task, description=f"[cyan]Processing[/cyan] {file_basename}")
|
|
109
|
+
|
|
110
|
+
if verbose:
|
|
111
|
+
console.print(f" Analyzing {file_basename}...")
|
|
112
|
+
|
|
113
|
+
total_processed += 1
|
|
114
|
+
match = matcher.identify_episode(mkv_file, temp_dir, season_num)
|
|
72
115
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
if match:
|
|
117
|
+
total_matched += 1
|
|
118
|
+
new_name = f"{matcher.show_name} - S{match['season']:02d}E{match['episode']:02d}.mkv"
|
|
119
|
+
|
|
120
|
+
confidence_color = "green" if match['confidence'] > 0.8 else "yellow"
|
|
121
|
+
|
|
122
|
+
if verbose or dry_run:
|
|
123
|
+
console.print(
|
|
124
|
+
f" Match: [bold]{file_basename}[/bold] → [bold cyan]{new_name}[/bold cyan] "
|
|
125
|
+
f"(confidence: [{confidence_color}]{match['confidence']:.2f}[/{confidence_color}])"
|
|
126
|
+
)
|
|
77
127
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
128
|
+
if not dry_run:
|
|
129
|
+
rename_episode_file(mkv_file, new_name)
|
|
130
|
+
else:
|
|
131
|
+
if verbose:
|
|
132
|
+
console.print(f" [yellow]No match found for {file_basename}[/yellow]")
|
|
133
|
+
|
|
134
|
+
progress.advance(task)
|
|
83
135
|
finally:
|
|
84
|
-
if not dry_run:
|
|
136
|
+
if not dry_run and temp_dir.exists():
|
|
85
137
|
shutil.rmtree(temp_dir)
|
|
138
|
+
|
|
139
|
+
# Summary
|
|
140
|
+
console.print()
|
|
141
|
+
if total_processed == 0:
|
|
142
|
+
console.print("[yellow]No files needed processing[/yellow]")
|
|
143
|
+
else:
|
|
144
|
+
console.print(f"[bold]Summary:[/bold] Processed {total_processed} files")
|
|
145
|
+
console.print(f"[bold green]Successfully matched:[/bold green] {total_matched} files")
|
|
146
|
+
|
|
147
|
+
if total_matched < total_processed:
|
|
148
|
+
console.print(f"[bold yellow]Unmatched:[/bold yellow] {total_processed - total_matched} files")
|
|
149
|
+
console.print(
|
|
150
|
+
"[cyan]Tip:[/cyan] Try downloading subtitles with --get-subs or "
|
|
151
|
+
"check that your files are named consistently"
|
|
152
|
+
)
|
mkv_episode_matcher/utils.py
CHANGED
|
@@ -7,13 +7,15 @@ import requests
|
|
|
7
7
|
import torch
|
|
8
8
|
from loguru import logger
|
|
9
9
|
from opensubtitlescom import OpenSubtitles
|
|
10
|
-
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
13
|
from mkv_episode_matcher.__main__ import CACHE_DIR, CONFIG_FILE
|
|
12
14
|
from mkv_episode_matcher.config import get_config
|
|
13
15
|
from mkv_episode_matcher.subtitle_utils import find_existing_subtitle, sanitize_filename
|
|
14
16
|
from mkv_episode_matcher.tmdb_client import fetch_season_details
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
console = Console()
|
|
17
19
|
def get_valid_seasons(show_dir):
|
|
18
20
|
"""
|
|
19
21
|
Get all season directories that contain MKV files.
|
|
@@ -391,9 +393,24 @@ def compare_text(text1, text2):
|
|
|
391
393
|
|
|
392
394
|
def check_gpu_support():
|
|
393
395
|
logger.info("Checking GPU support...")
|
|
396
|
+
console.print("[bold]Checking GPU support...[/bold]")
|
|
394
397
|
if torch.cuda.is_available():
|
|
395
398
|
logger.info(f"CUDA is available. Using GPU: {torch.cuda.get_device_name(0)}")
|
|
399
|
+
console.print(
|
|
400
|
+
Panel.fit(
|
|
401
|
+
f"CUDA is available. Using GPU: {torch.cuda.get_device_name(0)}",
|
|
402
|
+
title="GPU Support",
|
|
403
|
+
border_style="magenta",
|
|
404
|
+
)
|
|
405
|
+
)
|
|
396
406
|
else:
|
|
397
407
|
logger.warning(
|
|
398
408
|
"CUDA not available. Using CPU. Refer to https://pytorch.org/get-started/locally/ for GPU support."
|
|
399
409
|
)
|
|
410
|
+
console.print(
|
|
411
|
+
Panel.fit(
|
|
412
|
+
"CUDA not available. Using CPU. Refer to https://pytorch.org/get-started/locally/ for GPU support.",
|
|
413
|
+
title="GPU Support",
|
|
414
|
+
border_style="red",
|
|
415
|
+
)
|
|
416
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mkv-episode-matcher
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
16
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
17
|
-
Requires-Python:
|
|
17
|
+
Requires-Python: <3.13,>=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
Requires-Dist: configparser>=7.1.0
|
|
20
20
|
Requires-Dist: ffmpeg>=1.4
|
|
@@ -23,6 +23,7 @@ Requires-Dist: openai-whisper>=20240930
|
|
|
23
23
|
Requires-Dist: opensubtitlescom>=0.1.5
|
|
24
24
|
Requires-Dist: rapidfuzz>=3.10.1
|
|
25
25
|
Requires-Dist: requests>=2.32.3
|
|
26
|
+
Requires-Dist: rich[jupyter]>=13.9.4
|
|
26
27
|
Requires-Dist: tmdb-client>=0.0.1
|
|
27
28
|
Requires-Dist: torch>=2.5.1
|
|
28
29
|
Requires-Dist: torchaudio>=2.5.1
|
|
@@ -46,16 +47,18 @@ Automatically match and rename your MKV TV episodes using The Movie Database (TM
|
|
|
46
47
|
## Features
|
|
47
48
|
|
|
48
49
|
- 🎯 **Automatic Episode Matching**: Uses TMDb to accurately identify episodes
|
|
50
|
+
- 🎨 **Rich User Interface**: Color-coded output and progress indicators
|
|
49
51
|
- 📝 **Subtitle Extraction**: Extracts subtitles from MKV files
|
|
50
52
|
- 🔊 **Speech Recognition**: Uses Whisper for accurate episode identification
|
|
51
53
|
- 🚀 **Multi-threaded**: Fast processing of multiple files
|
|
52
54
|
- ⬇️ **Subtitle Downloads**: Integration with OpenSubtitles
|
|
53
55
|
- ✨ **Bulk Processing**: Handle entire seasons at once
|
|
54
56
|
- 🧪 **Dry Run Mode**: Test changes before applying
|
|
57
|
+
- 🎮 **Interactive Mode**: User-friendly season selection and configuration
|
|
55
58
|
|
|
56
59
|
## Prerequisites
|
|
57
60
|
|
|
58
|
-
- Python 3.9
|
|
61
|
+
- Python 3.9-3.12
|
|
59
62
|
- [FFmpeg](https://ffmpeg.org/download.html) installed and available in system PATH
|
|
60
63
|
- TMDb API key (optional, for subtitle downloads)
|
|
61
64
|
- OpenSubtitles account (optional, for subtitle downloads)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
mkv_episode_matcher/.gitattributes,sha256=Gh2-F2vCM7SZ01pX23UT8pQcmauXWfF3gwyRSb6ZAFs,66
|
|
2
|
+
mkv_episode_matcher/__init__.py,sha256=u3yZcpuK0ICeUjxYKePvW-zS61E5ss5q2AvqnSHuz9E,240
|
|
3
|
+
mkv_episode_matcher/__main__.py,sha256=O3GQk5R9BFuA-QNlqfBgDSS7G_W8IGSxiV8CFUbcaLc,10059
|
|
4
|
+
mkv_episode_matcher/config.py,sha256=EcJJjkekQ7oWtarUkufCYON_QWbQvq55-zMqCTOqSa4,2265
|
|
5
|
+
mkv_episode_matcher/episode_identification.py,sha256=r75AGVSQPdJwOZC1PkyPh89OCyjqDhpMbMuh0J3KWDY,12531
|
|
6
|
+
mkv_episode_matcher/episode_matcher.py,sha256=SxAbnXuTJITD1o0WohE9heE3Fm9zW_w0Nq3GzqtcIpQ,6329
|
|
7
|
+
mkv_episode_matcher/subtitle_utils.py,sha256=Hz9b4CKPV07YKTY4dcN3WbvdbvH-S3J4zcb9CiyvPlE,2551
|
|
8
|
+
mkv_episode_matcher/tmdb_client.py,sha256=LbMCgjmp7sCbrQo_CDlpcnryKPz5S7inE24YY9Pyjk4,4172
|
|
9
|
+
mkv_episode_matcher/utils.py,sha256=modXMLmt2fpny8liXwqe4ylxnwwfg_98OLOacv5izps,14501
|
|
10
|
+
mkv_episode_matcher-0.7.1.dist-info/METADATA,sha256=aldFGWAmo9NdKCgkrxI5bZB-5IwvJt78OlWXVB9xzBs,5384
|
|
11
|
+
mkv_episode_matcher-0.7.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
12
|
+
mkv_episode_matcher-0.7.1.dist-info/entry_points.txt,sha256=IglJ43SuCZq2eQ3shMFILCkmQASJHnDCI3ogohW2Hn4,64
|
|
13
|
+
mkv_episode_matcher-0.7.1.dist-info/top_level.txt,sha256=XRLbd93HUaedeWLtkyTvQjFcE5QcBRYa3V-CfHrq-OI,20
|
|
14
|
+
mkv_episode_matcher-0.7.1.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
mkv_episode_matcher/.gitattributes,sha256=Gh2-F2vCM7SZ01pX23UT8pQcmauXWfF3gwyRSb6ZAFs,66
|
|
2
|
-
mkv_episode_matcher/__init__.py,sha256=u3yZcpuK0ICeUjxYKePvW-zS61E5ss5q2AvqnSHuz9E,240
|
|
3
|
-
mkv_episode_matcher/__main__.py,sha256=-iRYoAfut3eDfV29UvobJvCKmYTpsOn8qM49QBFnMUM,5735
|
|
4
|
-
mkv_episode_matcher/config.py,sha256=EcJJjkekQ7oWtarUkufCYON_QWbQvq55-zMqCTOqSa4,2265
|
|
5
|
-
mkv_episode_matcher/episode_identification.py,sha256=rWhUzeNE5_uqsLcRuw_B6g7k3ud9Oa1oKgvXrBA-Jsc,12457
|
|
6
|
-
mkv_episode_matcher/episode_matcher.py,sha256=Yqos1hImF_QIZ8cV0IlemUxhpHwvwBn-mg89N9NDq9U,3126
|
|
7
|
-
mkv_episode_matcher/subtitle_utils.py,sha256=Hz9b4CKPV07YKTY4dcN3WbvdbvH-S3J4zcb9CiyvPlE,2551
|
|
8
|
-
mkv_episode_matcher/tmdb_client.py,sha256=LbMCgjmp7sCbrQo_CDlpcnryKPz5S7inE24YY9Pyjk4,4172
|
|
9
|
-
mkv_episode_matcher/utils.py,sha256=1-RwYn1w_YQFp4KxTmYbCSQEieK-mnToVIS34EVAZLw,13837
|
|
10
|
-
mkv_episode_matcher-0.6.0.dist-info/METADATA,sha256=LBtoWNzGS5Exd0H5q6fP5MdBSsMPOieYMOQ5uQoBZ64,5193
|
|
11
|
-
mkv_episode_matcher-0.6.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
12
|
-
mkv_episode_matcher-0.6.0.dist-info/entry_points.txt,sha256=IglJ43SuCZq2eQ3shMFILCkmQASJHnDCI3ogohW2Hn4,64
|
|
13
|
-
mkv_episode_matcher-0.6.0.dist-info/top_level.txt,sha256=XRLbd93HUaedeWLtkyTvQjFcE5QcBRYa3V-CfHrq-OI,20
|
|
14
|
-
mkv_episode_matcher-0.6.0.dist-info/RECORD,,
|
{mkv_episode_matcher-0.6.0.dist-info → mkv_episode_matcher-0.7.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|