fix subtitle title matching stripping hyphens along with the language suffix, breaking genre-tag detection

This commit is contained in:
Justin Oro
2026-08-30 14:54:52 -07:00
parent 123b7bc0e8
commit b1d713903b
+184
View File
@@ -996,6 +996,20 @@ def strip_language_code_clusters(words):
VERSION_TAG_PATTERN = re.compile(r'\bv\d+(?:\.\d+)*\b', re.IGNORECASE)
LEADING_TRACK_NUMBER_PATTERN = re.compile(r'^0\d\s+')
GENRE_TAG_WORDS = {
"action", "adventure", "animation", "animated", "biography", "comedy",
"crime", "documentary", "drama", "family", "fantasy", "horror",
"musical", "mystery", "romance", "scifi", "sci-fi", "thriller",
"war", "western",
}
GENRE_TAG_PATTERN = re.compile(
r'-\s*(?:' + '|'.join(re.escape(w) for w in sorted(GENRE_TAG_WORDS, key=len, reverse=True)) + r')\s+(?=(?:19|20)\d{2}\b)',
re.IGNORECASE,
)
def strip_junk_tokens(text):
text = re.sub(r'\b\d{3,4}p\b', ' ', text, flags=re.IGNORECASE)
@@ -1007,6 +1021,8 @@ def strip_junk_tokens(text):
words = text.split()
kept = [w for w in words if w.lower() not in JUNK_TOKENS]
kept = strip_language_code_clusters(kept)
if len(kept) > 1 and kept[-1].lower() in ENGLISH_LANGUAGE_TOKENS:
kept = kept[:-1]
return squeeze_spaces(' '.join(kept))
@@ -1082,9 +1098,11 @@ def strip_junk_trailing_paren(raw_name):
def build_query(raw_name, year, keep_hyphens=False):
raw_name = strip_leading_watermark(raw_name)
raw_name = LEADING_TRACK_NUMBER_PATTERN.sub('', raw_name)
raw_name = URL_PATTERN.sub(' ', raw_name)
raw_name = WWW_DOMAIN_PATTERN.sub(' ', raw_name)
raw_name = VERSION_TAG_PATTERN.sub(' ', raw_name)
raw_name = GENRE_TAG_PATTERN.sub(' ', raw_name)
cut_points = []
match = SEASON_TRUNCATE_PATTERN.search(raw_name)
@@ -1620,6 +1638,23 @@ def resolve_subtitle_language(path):
return None
TRAILING_WORD_PATTERN = re.compile(r'[._\-\s]+([A-Za-z]+)\s*$')
def strip_subtitle_language_suffix(stem):
result = stem
while True:
m_trail = TRAILING_WORD_PATTERN.search(result)
if not m_trail:
break
word = m_trail.group(1).lower()
if word in SUBTITLE_DESCRIPTOR_WORDS or word in ENGLISH_LANGUAGE_TOKENS or word in OTHER_LANGUAGE_TOKENS:
result = result[:m_trail.start()]
else:
break
return result if result.strip() else stem
def gather_video_subtitles(video_path, multi):
folder = video_path.parent
pattern = stem_match_pattern(video_path.stem) if multi else None
@@ -2113,6 +2148,63 @@ def find_matching_episode_video(target_season_dir, episode):
return None
def build_movie_folder_index(share):
index = []
for folder in sorted(p for p in Path(share).iterdir() if p.is_dir() and not p.name.startswith(".")):
videos = list_video_files(folder)
if len(videos) != 1:
continue
folder_year = extract_year(folder.name)
folder_query = build_query(folder.name, folder_year)
index.append((folder, videos[0], folder_year, folder_query))
return index
def match_movie_from_index(index, query, year):
best = None
best_video = None
best_ratio = 0.0
for folder, video, folder_year, folder_query in index:
if year and folder_year and abs(int(folder_year) - int(year)) > NEAR_YEAR_MAX_DIFF:
continue
ratio = difflib.SequenceMatcher(None, query.lower(), folder_query.lower()).ratio()
if ratio > best_ratio:
best_ratio = ratio
best = folder
best_video = video
if best is not None and best_ratio >= NEAR_YEAR_MATCH_MIN_SIMILARITY:
return best, best_video
return None, None
def build_show_folder_index(share):
index = []
for folder in sorted(p for p in Path(share).iterdir() if p.is_dir() and not p.name.startswith(".")):
folder_year = extract_year(folder.name)
folder_query = build_query(folder.name, folder_year)
index.append((folder, folder_year, folder_query))
return index
def match_show_from_index(index, query, year):
best = None
best_ratio = 0.0
for folder, folder_year, folder_query in index:
if year and folder_year and abs(int(folder_year) - int(year)) > NEAR_YEAR_MAX_DIFF:
continue
ratio = difflib.SequenceMatcher(None, query.lower(), folder_query.lower()).ratio()
if ratio > best_ratio:
best_ratio = ratio
best = folder
if best is not None and best_ratio >= NEAR_YEAR_MATCH_MIN_SIMILARITY:
return best
return None
def find_subtitle_pack_folder(container_folder):
canonical_name = subtitle_folder_name()
try:
@@ -3075,6 +3167,13 @@ def list_loose_video_files(share):
]
def list_loose_subtitle_files(share):
return [
item for item in sorted(Path(share).iterdir())
if item.is_file() and (item.suffix.lower().lstrip(".") in SUBTITLE_EXTENSIONS or item.suffix.lower().lstrip(".") == "idx")
]
def process_loose_movie_files(share, api_key, log, test_mode, test_limit, args):
files = list_loose_video_files(share)
if not files:
@@ -3238,6 +3337,86 @@ def process_loose_tv_files(share, api_key, log, test_mode, test_limit, args):
return folders_renamed, folders_skipped, files_renamed, files_skipped
def process_loose_subtitle_files(share, media_type, log, test_mode, args):
files = list_loose_subtitle_files(share)
if not files:
return 0, 0
print(f"Found {len(files)} loose subtitle file(s) directly in {Path(share).name} with no video of their own:")
print()
movie_index = build_movie_folder_index(share) if media_type == "movie" else None
show_index = build_show_folder_index(share) if media_type != "movie" else None
renamed = 0
skipped = 0
total = len(files)
for index, item in enumerate(files, start=1):
print(f"[{index}/{total}] {item.name}")
if not is_file_stable(item):
print(f"Skipping (still being copied): {item.name}")
skipped += 1
continue
base = strip_subtitle_language_suffix(item.stem)
year = extract_year(base)
query = build_query(base, year)
if not query:
print(f"Skipping (could not determine title): {item.name}")
skipped += 1
continue
video = None
folder = None
if media_type == "movie":
folder, video = match_movie_from_index(movie_index, query, year)
if not folder:
print(f"No matching movie found, skipping: {item.name}")
skipped += 1
continue
else:
se = parse_season_episode(item.name)
if not se:
print(f"No season/episode found, skipping: {item.name}")
skipped += 1
continue
season, episode, _ = se
folder = match_show_from_index(show_index, query, year)
if not folder:
print(f"No matching show found, skipping: {item.name}")
skipped += 1
continue
season_dir = folder / season_folder_name(season)
if not season_dir.is_dir():
print(f"No matching season in '{folder.name}', skipping: {item.name}")
skipped += 1
continue
video = find_matching_episode_video(season_dir, episode)
if not video:
print(f"No matching episode in '{folder.name}', skipping: {item.name}")
skipped += 1
continue
if test_mode:
print(f"Would move: {item.name} -> matches {folder.name}/{video.name}")
preview_consolidated_subtitles([item], video)
renamed += 1
continue
if not args.yes and not confirm(f"Move '{item.name}' to match '{folder.name}/{video.name}'?"):
print(f"Skipped: {item.name}")
skipped += 1
continue
renamed += rename_consolidated_subtitles([item], video, log)
print()
return renamed, skipped
LOOKUP_WORKERS = 8
@@ -3469,6 +3648,11 @@ def run_scan(args, log):
files_renamed += loose_files_renamed
files_skipped += loose_files_skipped
if not (is_single_show or is_single_movie):
sub_renamed, sub_skipped = process_loose_subtitle_files(share, media_type, log, test_mode, args)
files_renamed += sub_renamed
files_skipped += sub_skipped
print()
if test_mode:
print("Test mode: no changes were made.")