mkv-episode-matcher 0.6.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.

@@ -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
- if not os.path.exists(os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher")):
16
- os.makedirs(os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher"))
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
- os.path.expanduser("~"), ".mkv-episode-matcher", "config.ini"
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(os.path.expanduser("~"), ".mkv-episode-matcher", "logs")
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
- @logger.catch
46
- def main():
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
- Entry point of the application.
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
- This function is responsible for starting the application, parsing command-line arguments,
51
- setting the configuration, and processing the show.
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
- The function logs its progress to two separate log files: one for standard output and one for errors.
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(description="Process shows with TMDb API")
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: None)",
150
+ help="Specify the season number to be processed (default: all seasons)",
79
151
  )
80
152
  parser.add_argument(
81
153
  "--dry-run",
82
- type=bool,
83
- default=None,
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
- type=bool,
90
- default=None,
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
- type=bool,
97
- default=False,
98
- nargs="?",
99
- help="Check if GPU is available (default: False)",
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
- check_gpu_support()
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
- if not tmdb_api_key:
123
- tmdb_api_key = input("Enter your TMDb API key: ")
124
- logger.debug(f"TMDb API Key: {tmdb_api_key}")
125
- if not open_subtitles_api_key:
126
- open_subtitles_api_key = input("Enter your OpenSubtitles API key: ")
127
- if not open_subtitles_user_agent:
128
- open_subtitles_user_agent = input("Enter your OpenSubtitles User Agent: ")
129
- if not open_subtitles_username:
130
- open_subtitles_username = input("Enter your OpenSubtitles Username: ")
131
- if not open_subtitles_password:
132
- open_subtitles_password = input("Enter your OpenSubtitles Password: ")
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 = input("Enter the main directory of the show:")
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
- process_show(args.season, dry_run=args.dry_run, get_subs=args.get_subs)
160
- logger.info("Show processing completed")
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
- main()
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
- def process_show(season=None, dry_run=False, get_subs=False):
25
- """Process the show using streaming speech recognition."""
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
- logger.error(f"No reference subtitle files found in {reference_dir}")
36
- logger.info("Please download reference subtitles first")
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
- logger.warning("No seasons with .mkv files found")
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
- logger.warning(f"Season {season} has no .mkv files to process")
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 = [f for f in glob.glob(os.path.join(season_path, "*.mkv"))
53
- if not check_filename(f)]
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
- logger.info(f"No new files to process in {season_path}")
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
- for mkv_file in mkv_files:
70
- logger.info(f"Attempting speech recognition match for {mkv_file}")
71
- match = matcher.identify_episode(mkv_file, temp_dir, season_num)
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
- if match:
74
- new_name = f"{matcher.show_name} - S{match['season']:02d}E{match['episode']:02d}.mkv"
75
- logger.info(f"Speech matched {os.path.basename(mkv_file)} to {new_name} "
76
- f"(confidence: {match['confidence']:.2f})")
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
- if not dry_run:
79
- logger.info(f"Renaming {mkv_file} to {new_name}")
80
- rename_episode_file(mkv_file, new_name)
81
- else:
82
- logger.info(f"Speech recognition match failed for {mkv_file}")
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
+ )
@@ -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
1
  Metadata-Version: 2.2
2
2
  Name: mkv-episode-matcher
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -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,12 +47,14 @@ 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
 
@@ -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.0.dist-info/METADATA,sha256=6MTuobf7fupK2rWuEcuvkWNwuSMRNK9pHPfWz43Cc84,5383
11
+ mkv_episode_matcher-0.7.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
12
+ mkv_episode_matcher-0.7.0.dist-info/entry_points.txt,sha256=IglJ43SuCZq2eQ3shMFILCkmQASJHnDCI3ogohW2Hn4,64
13
+ mkv_episode_matcher-0.7.0.dist-info/top_level.txt,sha256=XRLbd93HUaedeWLtkyTvQjFcE5QcBRYa3V-CfHrq-OI,20
14
+ mkv_episode_matcher-0.7.0.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,,