mkv-episode-matcher 0.1.2__tar.gz → 0.1.3__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.1.3/.coverage.DESKTOP-NTJ52LL.19040.XkHNEbEx +0 -0
- mkv_episode_matcher-0.1.3/.coverage.DESKTOP-NTJ52LL.24340.XjsBEKWx +0 -0
- mkv_episode_matcher-0.1.3/.gitattributes +2 -0
- mkv_episode_matcher-0.1.3/.github/workflows/ci.yml +29 -0
- mkv_episode_matcher-0.1.3/.gitmodules +3 -0
- mkv_episode_matcher-0.1.3/.vscode/settings.json +11 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3}/PKG-INFO +1 -1
- mkv_episode_matcher-0.1.3/docs/index.md +17 -0
- mkv_episode_matcher-0.1.3/mkdocs.yml +6 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/.gitattributes +2 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/__init__.py +1 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/__main__.py +179 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/config.py +82 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/episode_matcher.py +237 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/mkv_to_srt.py +179 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/requirements.txt +8 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/tmdb_client.py +134 -0
- mkv_episode_matcher-0.1.3/mkv_episode_matcher/utils.py +228 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3}/pyproject.toml +0 -5
- mkv_episode_matcher-0.1.3/tests/__init__.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3}/.gitignore +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3}/README.md +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/.git +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/.gitignore +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/Libraries/SubZero/SubZero.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/Libraries/SubZero/dictionaries/data.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/Libraries/SubZero/post_processing.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/README.md +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/__init__.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/imagemaker.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/pgs2srt.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/pgsreader.py +0 -0
- {mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/requirements.txt +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
- main
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
jobs:
|
|
10
|
+
deploy:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Configure Git Credentials
|
|
15
|
+
run: |
|
|
16
|
+
git config user.name github-actions[bot]
|
|
17
|
+
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: 3.x
|
|
21
|
+
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
|
22
|
+
- uses: actions/cache@v4
|
|
23
|
+
with:
|
|
24
|
+
key: mkdocs-material-${{ env.cache_id }}
|
|
25
|
+
path: .cache
|
|
26
|
+
restore-keys: |
|
|
27
|
+
mkdocs-material-
|
|
28
|
+
- run: pip install mkdocs-material
|
|
29
|
+
- run: mkdocs gh-deploy --force
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mkv-episode-matcher
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
Project-URL: Documentation, https://github.com/Jsakkos/mkv-episode-matcher#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/Jsakkos/mkv-episode-matcher/issues
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Welcome to MkDocs
|
|
2
|
+
|
|
3
|
+
For full documentation visit [mkdocs.org](https://www.mkdocs.org).
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
* `mkdocs new [dir-name]` - Create a new project.
|
|
8
|
+
* `mkdocs serve` - Start the live-reloading docs server.
|
|
9
|
+
* `mkdocs build` - Build the documentation site.
|
|
10
|
+
* `mkdocs -h` - Print help message and exit.
|
|
11
|
+
|
|
12
|
+
## Project layout
|
|
13
|
+
|
|
14
|
+
mkdocs.yml # The configuration file.
|
|
15
|
+
docs/
|
|
16
|
+
index.md # The documentation homepage.
|
|
17
|
+
... # Other markdown pages, images and other files.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.1.3"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# __main__.py
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from .config import get_config, set_config
|
|
8
|
+
|
|
9
|
+
# Log the start of the application
|
|
10
|
+
logger.info("Starting the application")
|
|
11
|
+
|
|
12
|
+
# Check if logs directory exists, if not create it
|
|
13
|
+
if not os.path.exists('./logs'):
|
|
14
|
+
os.mkdir('./logs')
|
|
15
|
+
|
|
16
|
+
# Add a new handler for stdout logs
|
|
17
|
+
logger.add("./logs/file_stdout.log", format="{time} {level} {message}", level="DEBUG", rotation="10 MB")
|
|
18
|
+
|
|
19
|
+
# Add a new handler for error logs
|
|
20
|
+
logger.add("./logs/file_errors.log", level="ERROR", rotation="10 MB")
|
|
21
|
+
|
|
22
|
+
# Check if the configuration directory exists, if not create it
|
|
23
|
+
if not os.path.exists(os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher")):
|
|
24
|
+
os.makedirs(os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher"))
|
|
25
|
+
|
|
26
|
+
# Define the paths for the configuration file and cache directory
|
|
27
|
+
CONFIG_FILE = os.path.join(
|
|
28
|
+
os.path.expanduser("~"), ".mkv-episode-matcher", "config.ini"
|
|
29
|
+
)
|
|
30
|
+
CACHE_DIR = os.path.join(os.path.expanduser("~"), ".mkv-episode-matcher", "cache")
|
|
31
|
+
|
|
32
|
+
# Check if the cache directory exists, if not create it
|
|
33
|
+
if not os.path.exists(CACHE_DIR):
|
|
34
|
+
os.makedirs(CACHE_DIR)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@logger.catch
|
|
38
|
+
def main():
|
|
39
|
+
"""
|
|
40
|
+
Entry point of the application.
|
|
41
|
+
|
|
42
|
+
This function is responsible for starting the application, parsing command-line arguments,
|
|
43
|
+
setting the configuration, and processing the show.
|
|
44
|
+
|
|
45
|
+
Command-line arguments:
|
|
46
|
+
--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.
|
|
47
|
+
--show-dir: The main directory of the show. If not provided, the function will prompt the user to input it.
|
|
48
|
+
--season: The season number to be processed. If not provided, all seasons will be processed.
|
|
49
|
+
--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.
|
|
50
|
+
--get-subs: A boolean flag indicating whether to download subtitles for the show. If not provided, the function will not download subtitles.
|
|
51
|
+
--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.
|
|
52
|
+
|
|
53
|
+
The function logs its progress to two separate log files: one for standard output and one for errors.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Parse command-line arguments
|
|
58
|
+
parser = argparse.ArgumentParser(description="Process shows with TMDb API")
|
|
59
|
+
parser.add_argument("--tmdb-api-key", help="TMDb API key")
|
|
60
|
+
parser.add_argument("--show-dir", help="Main directory of the show")
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--season",
|
|
63
|
+
type=int,
|
|
64
|
+
default=None,
|
|
65
|
+
nargs="?",
|
|
66
|
+
help="Specify the season number to be processed (default: None)",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--dry-run",
|
|
70
|
+
type=bool,
|
|
71
|
+
default=None,
|
|
72
|
+
nargs="?",
|
|
73
|
+
help="Don't rename any files (default: None)",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--get-subs",
|
|
77
|
+
type=bool,
|
|
78
|
+
default=None,
|
|
79
|
+
nargs="?",
|
|
80
|
+
help="Download subtitles for the show (default: None)",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--tesseract-path",
|
|
84
|
+
type=str,
|
|
85
|
+
default=None,
|
|
86
|
+
nargs="?",
|
|
87
|
+
help="Path to the tesseract executable (default: None)",
|
|
88
|
+
)
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
logger.debug(f"Command-line arguments: {args}")
|
|
91
|
+
open_subtitles_api_key = ""
|
|
92
|
+
open_subtitles_user_agent = ""
|
|
93
|
+
open_subtitles_username = ""
|
|
94
|
+
open_subtitles_password = ""
|
|
95
|
+
# Check if API key is provided via command-line argument
|
|
96
|
+
tmdb_api_key = args.tmdb_api_key
|
|
97
|
+
|
|
98
|
+
# If API key is not provided, try to get it from the cache
|
|
99
|
+
if not tmdb_api_key:
|
|
100
|
+
cached_config = get_config(CONFIG_FILE)
|
|
101
|
+
if cached_config:
|
|
102
|
+
tmdb_api_key = cached_config.get("tmdb_api_key")
|
|
103
|
+
|
|
104
|
+
# If API key is still not available, prompt the user to input it
|
|
105
|
+
if not tmdb_api_key:
|
|
106
|
+
tmdb_api_key = input("Enter your TMDb API key: ")
|
|
107
|
+
# Cache the API key
|
|
108
|
+
|
|
109
|
+
logger.debug(f"TMDb API Key: {tmdb_api_key}")
|
|
110
|
+
if args.get_subs:
|
|
111
|
+
logger.debug("Getting OpenSubtitles API key")
|
|
112
|
+
cached_config = get_config(CONFIG_FILE)
|
|
113
|
+
try:
|
|
114
|
+
open_subtitles_api_key = cached_config.get("open_subtitles_api_key")
|
|
115
|
+
open_subtitles_user_agent = cached_config.get("open_subtitles_user_agent")
|
|
116
|
+
open_subtitles_username = cached_config.get("open_subtitles_username")
|
|
117
|
+
open_subtitles_password = cached_config.get("open_subtitles_password")
|
|
118
|
+
except:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
if not open_subtitles_api_key:
|
|
122
|
+
open_subtitles_api_key = input("Enter your OpenSubtitles API key: ")
|
|
123
|
+
|
|
124
|
+
if not open_subtitles_user_agent:
|
|
125
|
+
open_subtitles_user_agent = input("Enter your OpenSubtitles User Agent: ")
|
|
126
|
+
|
|
127
|
+
if not open_subtitles_username:
|
|
128
|
+
open_subtitles_username = input("Enter your OpenSubtitles Username: ")
|
|
129
|
+
|
|
130
|
+
if not open_subtitles_password:
|
|
131
|
+
open_subtitles_password = input("Enter your OpenSubtitles Password: ")
|
|
132
|
+
|
|
133
|
+
# If show directory is provided via command-line argument, use it
|
|
134
|
+
show_dir = args.show_dir
|
|
135
|
+
if not show_dir:
|
|
136
|
+
show_dir = cached_config.get("show_dir")
|
|
137
|
+
if not show_dir:
|
|
138
|
+
# If show directory is not provided, prompt the user to input it
|
|
139
|
+
show_dir = input("Enter the main directory of the show:")
|
|
140
|
+
logger.info(f"Show Directory: {show_dir}")
|
|
141
|
+
# if the user does not provide a show directory, make the default show directory the current working directory
|
|
142
|
+
if not show_dir:
|
|
143
|
+
show_dir = os.getcwd()
|
|
144
|
+
if not args.tesseract_path:
|
|
145
|
+
tesseract_path = cached_config.get("tesseract_path")
|
|
146
|
+
|
|
147
|
+
if not tesseract_path:
|
|
148
|
+
tesseract_path = input(
|
|
149
|
+
r"Enter the path to the tesseract executable: ['C:\Program Files\Tesseract-OCR\tesseract.exe']"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
tesseract_path = args.tesseract_path
|
|
154
|
+
logger.debug(f"Teesseract Path: {tesseract_path}")
|
|
155
|
+
logger.debug(f"Show Directory: {show_dir}")
|
|
156
|
+
|
|
157
|
+
# Set the configuration
|
|
158
|
+
set_config(
|
|
159
|
+
tmdb_api_key,
|
|
160
|
+
open_subtitles_api_key,
|
|
161
|
+
open_subtitles_user_agent,
|
|
162
|
+
open_subtitles_username,
|
|
163
|
+
open_subtitles_password,
|
|
164
|
+
show_dir,
|
|
165
|
+
CONFIG_FILE,
|
|
166
|
+
tesseract_path=tesseract_path,
|
|
167
|
+
)
|
|
168
|
+
logger.info("Configuration set")
|
|
169
|
+
|
|
170
|
+
# Process the show
|
|
171
|
+
from .episode_matcher import process_show
|
|
172
|
+
|
|
173
|
+
process_show(args.season, dry_run=args.dry_run, get_subs=args.get_subs)
|
|
174
|
+
logger.info("Show processing completed")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Run the main function if the script is run directly
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# config.py
|
|
2
|
+
import configparser
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
MAX_THREADS = 4
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_total_threads():
|
|
12
|
+
return multiprocessing.cpu_count()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
total_threads = get_total_threads()
|
|
16
|
+
|
|
17
|
+
if total_threads < MAX_THREADS:
|
|
18
|
+
MAX_THREADS = total_threads
|
|
19
|
+
logger.info(f"Total available threads: {total_threads} -> Setting max to {MAX_THREADS}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def set_config(
|
|
23
|
+
tmdb_api_key,
|
|
24
|
+
open_subtitles_api_key,
|
|
25
|
+
open_subtitles_user_agent,
|
|
26
|
+
open_subtitles_username,
|
|
27
|
+
open_subtitles_password,
|
|
28
|
+
show_dir,
|
|
29
|
+
file,
|
|
30
|
+
tesseract_path=None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Sets the configuration values and writes them to a file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
tmdb_api_key (str): The API key for TMDB (The Movie Database).
|
|
37
|
+
open_subtitles_api_key (str): The API key for OpenSubtitles.
|
|
38
|
+
open_subtitles_user_agent (str): The user agent for OpenSubtitles.
|
|
39
|
+
open_subtitles_username (str): The username for OpenSubtitles.
|
|
40
|
+
open_subtitles_password (str): The password for OpenSubtitles.
|
|
41
|
+
show_dir (str): The directory where the TV show episodes are located.
|
|
42
|
+
file (str): The path to the configuration file.
|
|
43
|
+
tesseract_path (str, optional): The path to the Tesseract OCR executable.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
None
|
|
47
|
+
"""
|
|
48
|
+
config = configparser.ConfigParser()
|
|
49
|
+
config["Config"] = {
|
|
50
|
+
"tmdb_api_key": str(tmdb_api_key),
|
|
51
|
+
"show_dir": show_dir,
|
|
52
|
+
"max_threads": int(MAX_THREADS),
|
|
53
|
+
"open_subtitles_api_key": str(open_subtitles_api_key),
|
|
54
|
+
"open_subtitles_user_agent": str(open_subtitles_user_agent),
|
|
55
|
+
"open_subtitles_username": str(open_subtitles_username),
|
|
56
|
+
"open_subtitles_password": str(open_subtitles_password),
|
|
57
|
+
"tesseract_path": str(tesseract_path),
|
|
58
|
+
}
|
|
59
|
+
logger.info(
|
|
60
|
+
f"Setting config with API:{tmdb_api_key}, show_dir: {show_dir}, and max_threads: {MAX_THREADS}"
|
|
61
|
+
)
|
|
62
|
+
with open(file, "w") as configfile:
|
|
63
|
+
config.write(configfile)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_config(file):
|
|
67
|
+
"""
|
|
68
|
+
Read and return the configuration from the specified file.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
file (str): The path to the configuration file.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict: The configuration settings as a dictionary.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
logger.info(f"Loading config from {file}")
|
|
78
|
+
config = configparser.ConfigParser()
|
|
79
|
+
if os.path.exists(file):
|
|
80
|
+
config.read(file)
|
|
81
|
+
return config["Config"] if "Config" in config else None
|
|
82
|
+
return {}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# episode_matcher.py
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from mkv_episode_matcher.__main__ import CACHE_DIR, CONFIG_FILE
|
|
8
|
+
from mkv_episode_matcher.config import get_config
|
|
9
|
+
from mkv_episode_matcher.mkv_to_srt import convert_mkv_to_srt
|
|
10
|
+
from mkv_episode_matcher.tmdb_client import fetch_show_id
|
|
11
|
+
from mkv_episode_matcher.utils import check_filename, cleanup_ocr_files, get_subtitles
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# hash_data = {}
|
|
15
|
+
@logger.catch
|
|
16
|
+
def process_show(season=None, dry_run=False, get_subs=False):
|
|
17
|
+
"""
|
|
18
|
+
Process the show by downloading episode images and finding matching episodes.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
season (int, optional): The season number to process. If provided, only that season will be processed. Defaults to None.
|
|
22
|
+
force (bool, optional): Whether to force re-processing of episodes even if they already exist. Defaults to False.
|
|
23
|
+
dry_run (bool, optional): Whether to perform a dry run without actually processing the episodes. Defaults to False.
|
|
24
|
+
threshold (float, optional): The threshold value for matching episodes. Defaults to None.
|
|
25
|
+
"""
|
|
26
|
+
config = get_config(CONFIG_FILE)
|
|
27
|
+
show_dir = config.get("show_dir")
|
|
28
|
+
show_name = os.path.basename(show_dir)
|
|
29
|
+
logger.info(f"Processing show '{show_name}'...")
|
|
30
|
+
show_id = fetch_show_id(show_name)
|
|
31
|
+
|
|
32
|
+
if show_id is None:
|
|
33
|
+
logger.error(f"Could not find show '{os.path.basename(show_dir)}' on TMDb.")
|
|
34
|
+
return
|
|
35
|
+
season_paths = [
|
|
36
|
+
os.path.join(show_dir, d)
|
|
37
|
+
for d in os.listdir(show_dir)
|
|
38
|
+
if os.path.isdir(os.path.join(show_dir, d))
|
|
39
|
+
]
|
|
40
|
+
logger.info(
|
|
41
|
+
f"Found {len(season_paths)} seasons for show '{os.path.basename(show_dir)}'"
|
|
42
|
+
)
|
|
43
|
+
seasons_to_process = [
|
|
44
|
+
int(os.path.basename(season_path).split()[-1]) for season_path in season_paths
|
|
45
|
+
]
|
|
46
|
+
if get_subs:
|
|
47
|
+
get_subtitles(show_id, seasons=set(seasons_to_process))
|
|
48
|
+
if season is not None:
|
|
49
|
+
mkv_files = [
|
|
50
|
+
os.path.join(show_dir, season)
|
|
51
|
+
for f in os.listdir(show_dir)
|
|
52
|
+
if f.endswith(".mkv")
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
season_path = os.path.join(show_dir, f"Season {season}")
|
|
56
|
+
else:
|
|
57
|
+
for season_path in os.listdir(show_dir):
|
|
58
|
+
season_path = os.path.join(show_dir, season_path)
|
|
59
|
+
mkv_files = [
|
|
60
|
+
os.path.join(season_path, f)
|
|
61
|
+
for f in os.listdir(season_path)
|
|
62
|
+
if f.endswith(".mkv")
|
|
63
|
+
]
|
|
64
|
+
# Filter out files that have already been processed
|
|
65
|
+
for f in mkv_files:
|
|
66
|
+
if check_filename(f):
|
|
67
|
+
logger.info(f"Skipping {f}, already processed")
|
|
68
|
+
mkv_files.remove(f)
|
|
69
|
+
if len(mkv_files) == 0:
|
|
70
|
+
logger.info("No new files to process")
|
|
71
|
+
return
|
|
72
|
+
convert_mkv_to_srt(season_path, mkv_files)
|
|
73
|
+
reference_text_dict = process_reference_srt_files(show_name)
|
|
74
|
+
srt_text_dict = process_srt_files(show_dir)
|
|
75
|
+
compare_and_rename_files(srt_text_dict, reference_text_dict, dry_run=dry_run)
|
|
76
|
+
cleanup_ocr_files(show_dir)
|
|
77
|
+
|
|
78
|
+
def check_filename(filename):
|
|
79
|
+
"""
|
|
80
|
+
Check if the filename is in the correct format.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
filename (str): The filename to check.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
bool: True if the filename is in the correct format, False otherwise.
|
|
87
|
+
"""
|
|
88
|
+
# Check if the filename matches the expected format
|
|
89
|
+
match = re.match(r".*S\d+E\d+", filename)
|
|
90
|
+
return bool(match)
|
|
91
|
+
def extract_srt_text(filepath):
|
|
92
|
+
"""
|
|
93
|
+
Extracts the text from an SRT file.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
filepath (str): The path to the SRT file.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
list: A list of lists, where each inner list represents a block of text from the SRT file.
|
|
100
|
+
Each inner list contains the lines of text for that block.
|
|
101
|
+
"""
|
|
102
|
+
# extract the text from the file
|
|
103
|
+
with open(filepath) as f:
|
|
104
|
+
filepath = f.read()
|
|
105
|
+
text_lines = [
|
|
106
|
+
filepath.split("\n\n")[i].split("\n")[2:]
|
|
107
|
+
for i in range(len(filepath.split("\n\n")))
|
|
108
|
+
]
|
|
109
|
+
# remove empty lines
|
|
110
|
+
text_lines = [[line for line in lines if line] for lines in text_lines]
|
|
111
|
+
# remove <i> or </i> tags
|
|
112
|
+
text_lines = [
|
|
113
|
+
[re.sub(r"<i>|</i>|", "", line) for line in lines] for lines in text_lines
|
|
114
|
+
]
|
|
115
|
+
# remove empty lists
|
|
116
|
+
text_lines = [lines for lines in text_lines if lines]
|
|
117
|
+
return text_lines
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compare_text(text1, text2):
|
|
121
|
+
"""
|
|
122
|
+
Compare two lists of text lines and return the number of matching lines.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
text1 (list): List of text lines from the first source.
|
|
126
|
+
text2 (list): List of text lines from the second source.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
int: Number of matching lines between the two sources.
|
|
130
|
+
"""
|
|
131
|
+
# Flatten the list of text lines
|
|
132
|
+
flat_text1 = [line for lines in text1 for line in lines]
|
|
133
|
+
flat_text2 = [line for lines in text2 for line in lines]
|
|
134
|
+
|
|
135
|
+
# Compare the two lists of text lines
|
|
136
|
+
matching_lines = set(flat_text1).intersection(flat_text2)
|
|
137
|
+
return len(matching_lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def extract_season_episode(filename):
|
|
141
|
+
"""
|
|
142
|
+
Extract the season and episode number from the filename.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
filename (str): The filename to extract the season and episode from.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
tuple: A tuple containing the season and episode number.
|
|
149
|
+
"""
|
|
150
|
+
# Extract the season and episode number from the filename
|
|
151
|
+
match = re.search(r"S(\d+)E(\d+)", filename)
|
|
152
|
+
if match:
|
|
153
|
+
season = int(match.group(1))
|
|
154
|
+
episode = int(match.group(2))
|
|
155
|
+
return season, episode
|
|
156
|
+
else:
|
|
157
|
+
return None, None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def process_reference_srt_files(series_name):
|
|
161
|
+
"""
|
|
162
|
+
Process reference SRT files for a given series.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
series_name (str): The name of the series.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
dict: A dictionary containing the reference files where the keys are the MKV filenames
|
|
169
|
+
and the values are the corresponding SRT texts.
|
|
170
|
+
"""
|
|
171
|
+
reference_files = {}
|
|
172
|
+
reference_dir = os.path.join(CACHE_DIR, "data", series_name)
|
|
173
|
+
for dirpath, _, filenames in os.walk(reference_dir):
|
|
174
|
+
for filename in filenames:
|
|
175
|
+
if filename.lower().endswith(".srt"):
|
|
176
|
+
srt_file = os.path.join(dirpath, filename)
|
|
177
|
+
logger.info(f"Processing {srt_file}")
|
|
178
|
+
srt_text = extract_srt_text(srt_file)
|
|
179
|
+
season, episode = extract_season_episode(filename)
|
|
180
|
+
mkv_filename = f"{series_name} - S{season:02}E{episode:02}.mkv"
|
|
181
|
+
reference_files[mkv_filename] = srt_text
|
|
182
|
+
return reference_files
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def process_srt_files(show_dir):
|
|
186
|
+
"""
|
|
187
|
+
Process all SRT files in the given directory and its subdirectories.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
show_dir (str): The directory path where the SRT files are located.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
dict: A dictionary containing the SRT file paths as keys and their corresponding text content as values.
|
|
194
|
+
"""
|
|
195
|
+
srt_files = {}
|
|
196
|
+
for dirpath, _, filenames in os.walk(show_dir):
|
|
197
|
+
for filename in filenames:
|
|
198
|
+
if filename.lower().endswith(".srt"):
|
|
199
|
+
srt_file = os.path.join(dirpath, filename)
|
|
200
|
+
logger.info(f"Processing {srt_file}")
|
|
201
|
+
srt_text = extract_srt_text(srt_file)
|
|
202
|
+
srt_files[srt_file] = srt_text
|
|
203
|
+
return srt_files
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def compare_and_rename_files(srt_files, reference_files, dry_run=False):
|
|
207
|
+
"""
|
|
208
|
+
Compare the srt files with the reference files and rename the matching mkv files.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
srt_files (dict): A dictionary containing the srt files as keys and their contents as values.
|
|
212
|
+
reference_files (dict): A dictionary containing the reference files as keys and their contents as values.
|
|
213
|
+
dry_run (bool, optional): If True, the function will only log the renaming actions without actually renaming the files. Defaults to False.
|
|
214
|
+
"""
|
|
215
|
+
logger.info(
|
|
216
|
+
f"Comparing {len(srt_files)} srt files with {len(reference_files)} reference files"
|
|
217
|
+
)
|
|
218
|
+
for srt_text in srt_files.keys():
|
|
219
|
+
parent_dir = os.path.dirname(os.path.dirname(srt_text))
|
|
220
|
+
for reference in reference_files.keys():
|
|
221
|
+
season, episode = extract_season_episode(reference)
|
|
222
|
+
mkv_file = os.path.join(
|
|
223
|
+
parent_dir, os.path.basename(srt_text).replace(".srt", ".mkv")
|
|
224
|
+
)
|
|
225
|
+
matching_lines = compare_text(
|
|
226
|
+
reference_files[reference], srt_files[srt_text]
|
|
227
|
+
)
|
|
228
|
+
if matching_lines >= int(len(reference_files[reference]) * 0.1):
|
|
229
|
+
logger.info(f"Matching lines: {matching_lines}")
|
|
230
|
+
logger.info(f"Found matching file: {mkv_file} ->{reference}")
|
|
231
|
+
new_filename = os.path.join(parent_dir, reference)
|
|
232
|
+
if not os.path.exists(new_filename):
|
|
233
|
+
if os.path.exists(mkv_file) and not dry_run:
|
|
234
|
+
logger.info(f"Renaming {mkv_file} to {new_filename}")
|
|
235
|
+
os.rename(mkv_file, new_filename)
|
|
236
|
+
else:
|
|
237
|
+
logger.info(f"File {new_filename} already exists, skipping")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Get the absolute path of the parent directory of the current script.
|
|
6
|
+
parent_dir = os.path.dirname(os.path.abspath(__file__))
|
|
7
|
+
|
|
8
|
+
# Add the parent directory to the Python path.
|
|
9
|
+
sys.path.append(parent_dir)
|
|
10
|
+
# Add the 'libraries' directory to the Python path.
|
|
11
|
+
sys.path.append(os.path.join(parent_dir, "libraries"))
|
|
12
|
+
# Add the 'libraries' directory to the Python path.
|
|
13
|
+
sys.path.append(os.path.join(parent_dir, "..", "libraries", "pgs2srt"))
|
|
14
|
+
import re
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
|
|
18
|
+
import pytesseract
|
|
19
|
+
from imagemaker import make_image
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from pgsreader import PGSReader
|
|
22
|
+
from PIL import Image, ImageOps
|
|
23
|
+
|
|
24
|
+
from mkv_episode_matcher.__main__ import CONFIG_FILE
|
|
25
|
+
from mkv_episode_matcher.config import get_config
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def convert_mkv_to_sup(mkv_file, output_dir):
|
|
29
|
+
"""
|
|
30
|
+
Convert an .mkv file to a .sup file using FFmpeg and pgs2srt.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
mkv_file (str): Path to the .mkv file.
|
|
34
|
+
output_dir (str): Path to the directory where the .sup file will be saved.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: Path to the converted .sup file.
|
|
38
|
+
"""
|
|
39
|
+
# Get the base name of the .mkv file without the extension
|
|
40
|
+
base_name = os.path.splitext(os.path.basename(mkv_file))[0]
|
|
41
|
+
|
|
42
|
+
# Construct the output .sup file path
|
|
43
|
+
sup_file = os.path.join(output_dir, f"{base_name}.sup")
|
|
44
|
+
if not os.path.exists(sup_file):
|
|
45
|
+
logger.info(f"Processing {mkv_file} to {sup_file}")
|
|
46
|
+
# FFmpeg command to convert .mkv to .sup
|
|
47
|
+
ffmpeg_cmd = ["ffmpeg", "-i", mkv_file, "-map", "0:s:0", "-c", "copy", sup_file]
|
|
48
|
+
try:
|
|
49
|
+
subprocess.run(ffmpeg_cmd, check=True)
|
|
50
|
+
logger.info(f"Converted {mkv_file} to {sup_file}")
|
|
51
|
+
except subprocess.CalledProcessError as e:
|
|
52
|
+
logger.error(f"Error converting {mkv_file}: {e}")
|
|
53
|
+
else:
|
|
54
|
+
logger.info(f"File {sup_file} already exists, skipping")
|
|
55
|
+
return sup_file
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@logger.catch
|
|
59
|
+
def perform_ocr(sup_file_path):
|
|
60
|
+
"""
|
|
61
|
+
Perform OCR on a .sup file and save the extracted text to a .srt file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
sup_file_path (str): Path to the .sup file.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# Get the base name of the .sup file without the extension
|
|
68
|
+
base_name = os.path.splitext(os.path.basename(sup_file_path))[0]
|
|
69
|
+
output_dir = os.path.dirname(sup_file_path)
|
|
70
|
+
logger.info(f"Performing OCR on {sup_file_path}")
|
|
71
|
+
# Construct the output .srt file path
|
|
72
|
+
srt_file = os.path.join(output_dir, f"{base_name}.srt")
|
|
73
|
+
|
|
74
|
+
# Load a PGS/SUP file.
|
|
75
|
+
pgs = PGSReader(sup_file_path)
|
|
76
|
+
|
|
77
|
+
# Set index
|
|
78
|
+
i = 0
|
|
79
|
+
|
|
80
|
+
# Complete subtitle track index
|
|
81
|
+
si = 0
|
|
82
|
+
|
|
83
|
+
tesseract_lang = "eng"
|
|
84
|
+
tesseract_config = f"-c tessedit_char_blacklist=[] --psm 6 --oem {1}"
|
|
85
|
+
|
|
86
|
+
config = get_config(CONFIG_FILE)
|
|
87
|
+
tesseract_path = config.get("tesseract_path")
|
|
88
|
+
logger.debug(f"Setting Teesseract Path to {tesseract_path}")
|
|
89
|
+
pytesseract.pytesseract.tesseract_cmd = str(tesseract_path)
|
|
90
|
+
|
|
91
|
+
# SubRip output
|
|
92
|
+
output = ""
|
|
93
|
+
|
|
94
|
+
if not os.path.exists(srt_file):
|
|
95
|
+
# Iterate the pgs generator
|
|
96
|
+
for ds in pgs.iter_displaysets():
|
|
97
|
+
# If set has image, parse the image
|
|
98
|
+
if ds.has_image:
|
|
99
|
+
# Get Palette Display Segment
|
|
100
|
+
pds = ds.pds[0]
|
|
101
|
+
# Get Object Display Segment
|
|
102
|
+
ods = ds.ods[0]
|
|
103
|
+
|
|
104
|
+
if pds and ods:
|
|
105
|
+
# Create and show the bitmap image and convert it to RGBA
|
|
106
|
+
src = make_image(ods, pds).convert("RGBA")
|
|
107
|
+
|
|
108
|
+
# Create grayscale image with black background
|
|
109
|
+
img = Image.new("L", src.size, "BLACK")
|
|
110
|
+
# Paste the subtitle bitmap
|
|
111
|
+
img.paste(src, (0, 0), src)
|
|
112
|
+
# Invert images so the text is readable by Tesseract
|
|
113
|
+
img = ImageOps.invert(img)
|
|
114
|
+
|
|
115
|
+
# Parse the image with tesesract
|
|
116
|
+
text = pytesseract.image_to_string(
|
|
117
|
+
img, lang=tesseract_lang, config=tesseract_config
|
|
118
|
+
).strip()
|
|
119
|
+
|
|
120
|
+
# Replace "|" with "I"
|
|
121
|
+
# Works better than blacklisting "|" in Tesseract,
|
|
122
|
+
# which results in I becoming "!" "i" and "1"
|
|
123
|
+
text = re.sub(r"[|/\\]", "I", text)
|
|
124
|
+
text = re.sub(r"[_]", "L", text)
|
|
125
|
+
start = datetime.fromtimestamp(ods.presentation_timestamp / 1000)
|
|
126
|
+
start = start + timedelta(hours=-1)
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
# Get Presentation Composition Segment
|
|
130
|
+
pcs = ds.pcs[0]
|
|
131
|
+
|
|
132
|
+
if pcs:
|
|
133
|
+
end = datetime.fromtimestamp(pcs.presentation_timestamp / 1000)
|
|
134
|
+
end = end + timedelta(hours=-1)
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
isinstance(start, datetime)
|
|
138
|
+
and isinstance(end, datetime)
|
|
139
|
+
and len(text)
|
|
140
|
+
):
|
|
141
|
+
si = si + 1
|
|
142
|
+
sub_output = str(si) + "\n"
|
|
143
|
+
sub_output += (
|
|
144
|
+
start.strftime("%H:%M:%S,%f")[0:12]
|
|
145
|
+
+ " --> "
|
|
146
|
+
+ end.strftime("%H:%M:%S,%f")[0:12]
|
|
147
|
+
+ "\n"
|
|
148
|
+
)
|
|
149
|
+
sub_output += text + "\n\n"
|
|
150
|
+
|
|
151
|
+
output += sub_output
|
|
152
|
+
start = end = text = None
|
|
153
|
+
i = i + 1
|
|
154
|
+
with open(srt_file, "w") as f:
|
|
155
|
+
f.write(output)
|
|
156
|
+
logger.info(f"Saved to: {srt_file}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def convert_mkv_to_srt(season_path, mkv_files):
|
|
160
|
+
"""
|
|
161
|
+
Converts MKV files to SRT format.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
season_path (str): The path to the season directory.
|
|
165
|
+
mkv_files (list): List of MKV files to convert.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
None
|
|
169
|
+
"""
|
|
170
|
+
logger.info(f"Converting {len(mkv_files)} files to SRT")
|
|
171
|
+
output_dir = os.path.join(season_path, "ocr")
|
|
172
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
173
|
+
sup_files = []
|
|
174
|
+
for mkv_file in mkv_files:
|
|
175
|
+
sup_file = convert_mkv_to_sup(mkv_file, output_dir)
|
|
176
|
+
sup_files.append(sup_file)
|
|
177
|
+
with ThreadPoolExecutor() as executor:
|
|
178
|
+
for sup_file in sup_files:
|
|
179
|
+
executor.submit(perform_ocr, sup_file)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# tmdb_client.py
|
|
2
|
+
import time
|
|
3
|
+
from threading import Lock
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from mkv_episode_matcher.__main__ import CONFIG_FILE
|
|
9
|
+
from mkv_episode_matcher.config import get_config
|
|
10
|
+
|
|
11
|
+
BASE_IMAGE_URL = "https://image.tmdb.org/t/p/original"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RateLimitedRequest:
|
|
15
|
+
"""
|
|
16
|
+
A class that represents a rate-limited request object.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
rate_limit (int): Maximum number of requests allowed per period.
|
|
20
|
+
period (int): Period in seconds.
|
|
21
|
+
requests_made (int): Counter for requests made.
|
|
22
|
+
start_time (float): Start time of the current period.
|
|
23
|
+
lock (Lock): Lock for synchronization.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, rate_limit=30, period=1):
|
|
27
|
+
self.rate_limit = rate_limit
|
|
28
|
+
self.period = period
|
|
29
|
+
self.requests_made = 0
|
|
30
|
+
self.start_time = time.time()
|
|
31
|
+
self.lock = Lock()
|
|
32
|
+
|
|
33
|
+
def get(self, url):
|
|
34
|
+
"""
|
|
35
|
+
Sends a rate-limited GET request to the specified URL.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
url (str): The URL to send the request to.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Response: The response object returned by the request.
|
|
42
|
+
"""
|
|
43
|
+
with self.lock:
|
|
44
|
+
if self.requests_made >= self.rate_limit:
|
|
45
|
+
sleep_time = self.period - (time.time() - self.start_time)
|
|
46
|
+
if sleep_time > 0:
|
|
47
|
+
time.sleep(sleep_time)
|
|
48
|
+
self.requests_made = 0
|
|
49
|
+
self.start_time = time.time()
|
|
50
|
+
|
|
51
|
+
self.requests_made += 1
|
|
52
|
+
|
|
53
|
+
response = requests.get(url)
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Initialize rate-limited request
|
|
58
|
+
rate_limited_request = RateLimitedRequest(rate_limit=30, period=1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fetch_show_id(show_name):
|
|
62
|
+
"""
|
|
63
|
+
Fetch the TMDb ID for a given show name.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
show_name (str): The name of the show.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: The TMDb ID of the show, or None if not found.
|
|
70
|
+
"""
|
|
71
|
+
config = get_config(CONFIG_FILE)
|
|
72
|
+
tmdb_api_key = config.get("tmdb_api_key")
|
|
73
|
+
url = f"https://api.themoviedb.org/3/search/tv?query={show_name}&api_key={tmdb_api_key}"
|
|
74
|
+
response = requests.get(url)
|
|
75
|
+
if response.status_code == 200:
|
|
76
|
+
results = response.json().get("results", [])
|
|
77
|
+
if results:
|
|
78
|
+
return str(results[0]["id"])
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def fetch_season_details(show_id, season_number):
|
|
83
|
+
"""
|
|
84
|
+
Fetch the total number of episodes for a given show and season from the TMDb API.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
show_id (str): The ID of the show on TMDb.
|
|
88
|
+
season_number (int): The season number to fetch details for.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
int: The total number of episodes in the season, or 0 if the API request failed.
|
|
92
|
+
"""
|
|
93
|
+
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")
|
|
96
|
+
url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season_number}?api_key={tmdb_api_key}"
|
|
97
|
+
try:
|
|
98
|
+
response = requests.get(url)
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
season_data = response.json()
|
|
101
|
+
total_episodes = len(season_data.get("episodes", []))
|
|
102
|
+
return total_episodes
|
|
103
|
+
except requests.exceptions.RequestException as e:
|
|
104
|
+
logger.error(f"Failed to fetch season details for Season {season_number}: {e}")
|
|
105
|
+
return 0
|
|
106
|
+
except KeyError:
|
|
107
|
+
logger.error(
|
|
108
|
+
f"Missing 'episodes' key in response JSON data for Season {season_number}"
|
|
109
|
+
)
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_number_of_seasons(show_id):
|
|
114
|
+
"""
|
|
115
|
+
Retrieves the number of seasons for a given TV show from the TMDB API.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
- show_id (int): The ID of the TV show.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
- num_seasons (int): The number of seasons for the TV show.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
- requests.HTTPError: If there is an error while making the API request.
|
|
125
|
+
"""
|
|
126
|
+
config = get_config(CONFIG_FILE)
|
|
127
|
+
tmdb_api_key = config.get("tmdb_api_key")
|
|
128
|
+
url = f"https://api.themoviedb.org/3/tv/{show_id}?api_key={tmdb_api_key}"
|
|
129
|
+
response = requests.get(url)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
show_data = response.json()
|
|
132
|
+
num_seasons = show_data.get("number_of_seasons", 0)
|
|
133
|
+
logger.info(f"Found {num_seasons} seasons")
|
|
134
|
+
return num_seasons
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# utils.py
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
from typing import Set
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from opensubtitlescom import OpenSubtitles
|
|
10
|
+
|
|
11
|
+
from mkv_episode_matcher.__main__ import CACHE_DIR, CONFIG_FILE
|
|
12
|
+
from mkv_episode_matcher.config import get_config
|
|
13
|
+
from mkv_episode_matcher.tmdb_client import fetch_season_details
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_filename(filename, series_title, season_number, episode_number):
|
|
17
|
+
"""
|
|
18
|
+
Check if a filename matches the expected naming convention for a series episode.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
filename (str): The filename to be checked.
|
|
22
|
+
series_title (str): The title of the series.
|
|
23
|
+
season_number (int): The season number of the episode.
|
|
24
|
+
episode_number (int): The episode number of the episode.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
bool: True if the filename matches the expected naming convention, False otherwise.
|
|
28
|
+
|
|
29
|
+
This function checks if the given filename matches the expected naming convention for a series episode.
|
|
30
|
+
The expected naming convention is '{series_title} - S{season_number:02d}E{episode_number:02d}.mkv'.
|
|
31
|
+
If the filename matches the expected pattern, it returns True; otherwise, it returns False.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
If filename = 'Example - S01E03.mkv', series_title = 'Example', season_number = 1, and episode_number = 3,
|
|
35
|
+
the function will return True because the filename matches the expected pattern.
|
|
36
|
+
"""
|
|
37
|
+
pattern = re.compile(
|
|
38
|
+
f"{re.escape(series_title)} - S{season_number:02d}E{episode_number:02d}.mkv"
|
|
39
|
+
)
|
|
40
|
+
return bool(pattern.match(filename))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def scramble_filename(original_file_path, file_number):
|
|
44
|
+
"""
|
|
45
|
+
Scrambles the filename of the given file path by adding the series title and file number.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
original_file_path (str): The original file path.
|
|
49
|
+
file_number (int): The file number to be added to the filename.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
None
|
|
53
|
+
"""
|
|
54
|
+
logger.info(f"Scrambling {original_file_path}")
|
|
55
|
+
series_title = os.path.basename(
|
|
56
|
+
os.path.dirname(os.path.dirname(original_file_path))
|
|
57
|
+
)
|
|
58
|
+
original_file_name = os.path.basename(original_file_path)
|
|
59
|
+
extension = os.path.splitext(original_file_path)[-1]
|
|
60
|
+
new_file_name = f"{series_title} - {file_number:03d}{extension}"
|
|
61
|
+
new_file_path = os.path.join(os.path.dirname(original_file_path), new_file_name)
|
|
62
|
+
if not os.path.exists(new_file_path):
|
|
63
|
+
logger.info(f"Renaming {original_file_name} -> {new_file_name}")
|
|
64
|
+
os.rename(original_file_path, new_file_path)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def rename_episode_file(original_file_path, season_number, episode_number):
|
|
68
|
+
"""
|
|
69
|
+
Rename an episode file with a standardized naming convention.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
original_file_path (str): The original file path of the episode.
|
|
73
|
+
season_number (int): The season number of the episode.
|
|
74
|
+
episode_number (int): The episode number of the episode.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
None
|
|
78
|
+
|
|
79
|
+
This function renames an episode file with a standardized naming convention based on the series title, season number,
|
|
80
|
+
and episode number. If a file with the intended new name already exists, it appends a numerical suffix to the filename
|
|
81
|
+
until it finds a unique name.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
If original_file_path = '/path/to/episode.mkv', season_number = 1, and episode_number = 3, and the series title is 'Example',
|
|
85
|
+
the function will rename the file to 'Example - S01E03.mkv' if no file with that name already exists. If a file with that
|
|
86
|
+
name already exists, it will be renamed to 'Example - S01E03_2.mkv', and so on.
|
|
87
|
+
"""
|
|
88
|
+
series_title = os.path.basename(
|
|
89
|
+
os.path.dirname(os.path.dirname(original_file_path))
|
|
90
|
+
)
|
|
91
|
+
original_file_name = os.path.basename(original_file_path)
|
|
92
|
+
extension = os.path.splitext(original_file_path)[-1]
|
|
93
|
+
new_file_name = (
|
|
94
|
+
f"{series_title} - S{season_number:02d}E{episode_number:02d}{extension}"
|
|
95
|
+
)
|
|
96
|
+
new_file_path = os.path.join(os.path.dirname(original_file_path), new_file_name)
|
|
97
|
+
|
|
98
|
+
# Check if the new file path already exists
|
|
99
|
+
if os.path.exists(new_file_path):
|
|
100
|
+
logger.warning(f"Filename already exists: {new_file_name}.")
|
|
101
|
+
|
|
102
|
+
# If the file already exists, find a unique name by appending a numerical suffix
|
|
103
|
+
suffix = 2
|
|
104
|
+
while True:
|
|
105
|
+
new_file_name = f"{series_title} - S{season_number:02d}E{episode_number:02d}_{suffix}{extension}"
|
|
106
|
+
new_file_path = os.path.join(
|
|
107
|
+
os.path.dirname(original_file_path), new_file_name
|
|
108
|
+
)
|
|
109
|
+
if not os.path.exists(new_file_path):
|
|
110
|
+
break
|
|
111
|
+
suffix += 1
|
|
112
|
+
|
|
113
|
+
logger.info(f"Renaming {original_file_name} -> {new_file_name}")
|
|
114
|
+
os.rename(original_file_path, new_file_path)
|
|
115
|
+
else:
|
|
116
|
+
logger.info(f"Renaming {original_file_name} -> {new_file_name}")
|
|
117
|
+
os.rename(original_file_path, new_file_path)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_subtitles(show_id, seasons: Set[int]):
|
|
121
|
+
"""
|
|
122
|
+
Retrieves and saves subtitles for a given TV show and seasons.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
show_id (int): The ID of the TV show.
|
|
126
|
+
seasons (Set[int]): A set of season numbers for which subtitles should be retrieved.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
None
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
logger.info(f"Getting subtitles for show ID {show_id}")
|
|
133
|
+
config = get_config(CONFIG_FILE)
|
|
134
|
+
show_dir = config.get("show_dir")
|
|
135
|
+
series_name = os.path.basename(show_dir)
|
|
136
|
+
tmdb_api_key = config.get("tmdb_api_key")
|
|
137
|
+
open_subtitles_api_key = config.get("open_subtitles_api_key")
|
|
138
|
+
open_subtitles_user_agent = config.get("open_subtitles_user_agent")
|
|
139
|
+
open_subtitles_username = config.get("open_subtitles_username")
|
|
140
|
+
open_subtitles_password = config.get("open_subtitles_password")
|
|
141
|
+
if not all(
|
|
142
|
+
[
|
|
143
|
+
show_dir,
|
|
144
|
+
tmdb_api_key,
|
|
145
|
+
open_subtitles_api_key,
|
|
146
|
+
open_subtitles_user_agent,
|
|
147
|
+
open_subtitles_username,
|
|
148
|
+
open_subtitles_password,
|
|
149
|
+
]
|
|
150
|
+
):
|
|
151
|
+
logger.error("Missing configuration settings. Please run the setup script.")
|
|
152
|
+
try:
|
|
153
|
+
# Initialize the OpenSubtitles client
|
|
154
|
+
subtitles = OpenSubtitles(open_subtitles_user_agent, open_subtitles_api_key)
|
|
155
|
+
|
|
156
|
+
# Log in (retrieve auth token)
|
|
157
|
+
subtitles.login(open_subtitles_username, open_subtitles_password)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.error(f"Failed to log in to OpenSubtitles: {e}")
|
|
160
|
+
return
|
|
161
|
+
for season in seasons:
|
|
162
|
+
episodes = fetch_season_details(show_id, season)
|
|
163
|
+
logger.info(f"Found {episodes} episodes in Season {season}")
|
|
164
|
+
|
|
165
|
+
for episode in range(1, episodes + 1):
|
|
166
|
+
logger.info(f"Processing Season {season}, Episode {episode}...")
|
|
167
|
+
srt_filepath = os.path.join(
|
|
168
|
+
CACHE_DIR,
|
|
169
|
+
"data",
|
|
170
|
+
series_name,
|
|
171
|
+
f"{series_name} - S{season:02d}E{episode:02d}.srt",
|
|
172
|
+
)
|
|
173
|
+
if not os.path.exists(srt_filepath):
|
|
174
|
+
# get the episode info from TMDB
|
|
175
|
+
url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season}/episode/{episode}?api_key={tmdb_api_key}"
|
|
176
|
+
response = requests.get(url)
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
episode_data = response.json()
|
|
179
|
+
episode_name = episode_data["name"]
|
|
180
|
+
episode_id = episode_data["id"]
|
|
181
|
+
# search for the subtitle
|
|
182
|
+
response = subtitles.search(tmdb_id=episode_id, languages="en")
|
|
183
|
+
if len(response.data) == 0:
|
|
184
|
+
logger.warning(
|
|
185
|
+
f"No subtitles found for {series_name} - S{season:02d}E{episode:02d}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
for subtitle in response.data:
|
|
189
|
+
subtitle_dict = subtitle.to_dict()
|
|
190
|
+
# Remove special characters and convert to uppercase
|
|
191
|
+
filename_clean = re.sub(
|
|
192
|
+
r"\W+", " ", subtitle_dict["file_name"]
|
|
193
|
+
).upper()
|
|
194
|
+
if f"E{episode:02d}" in filename_clean:
|
|
195
|
+
logger.info(f"Original filename: {subtitle_dict['file_name']}")
|
|
196
|
+
srt_file = subtitles.download_and_save(subtitle)
|
|
197
|
+
series_name = series_name.replace(":", " -")
|
|
198
|
+
shutil.move(srt_file, srt_filepath)
|
|
199
|
+
logger.info(f"Subtitle saved to {srt_filepath}")
|
|
200
|
+
break
|
|
201
|
+
else:
|
|
202
|
+
continue
|
|
203
|
+
else:
|
|
204
|
+
logger.info(
|
|
205
|
+
f"Subtitle already exists for {series_name} - S{season:02d}E{episode:02d}"
|
|
206
|
+
)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cleanup_ocr_files(show_dir):
|
|
211
|
+
"""
|
|
212
|
+
Clean up OCR files generated during the episode matching process.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
show_dir (str): The directory containing the show files.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
None
|
|
219
|
+
|
|
220
|
+
This function cleans up the OCR files generated during the episode matching process.
|
|
221
|
+
It deletes the 'ocr' directory and all its contents in each season directory of the show.
|
|
222
|
+
"""
|
|
223
|
+
for season_dir in os.listdir(show_dir):
|
|
224
|
+
season_dir_path = os.path.join(show_dir, season_dir)
|
|
225
|
+
ocr_dir_path = os.path.join(season_dir_path, "ocr")
|
|
226
|
+
if os.path.exists(ocr_dir_path):
|
|
227
|
+
logger.info(f"Cleaning up OCR files in {ocr_dir_path}")
|
|
228
|
+
shutil.rmtree(ocr_dir_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkv_episode_matcher-0.1.2 → mkv_episode_matcher-0.1.3/mkv_episode_matcher}/libraries/pgs2srt/.git
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
|