1.0.6 - Multi language support (TMDb), Per folder rule settings

This commit is contained in:
ponzischeme89
2026-01-18 22:07:00 +13:00
parent 675c3ef959
commit 170694bc28
13 changed files with 892 additions and 45 deletions
+135 -19
View File
@@ -1106,11 +1106,12 @@ def build_intro_blocks(
elif available_time_ms >= int(MIN_DURATION_SECONDS * 1000):
block_end_ms = safe_end_time(first_subtitle_start_ms - min_safe_gap_ms)
# If we can fit at least a brief title, show it
# If we can fit a brief header, include title + ratings/runtime
brief_text = (
f"{SUBLOGUE_SENTINEL}\n"
f"{title} ({year})\n"
f"— Generated by Sublogue"
f"{title_line}\n"
f"{info_line}\n"
f"- Generated by Sublogue"
)
blocks.append(SubtitleBlock(1, 0, block_end_ms, brief_text))
@@ -1123,6 +1124,102 @@ def build_intro_blocks(
return blocks
def build_outro_blocks(
movie: dict,
plot: str,
last_subtitle_end_ms: int,
min_safe_gap_ms: int = 500,
format_options: SubtitleFormatOptions = None,
) -> List[SubtitleBlock]:
"""
Build outro blocks that appear AFTER the last real subtitle.
This avoids any overlap by placing new blocks after the final subtitle end time.
"""
if format_options is None:
format_options = DEFAULT_FORMAT_OPTIONS
title = movie.get("title", "Unknown Title")
year = movie.get("year", "")
imdb_rating = movie.get("imdb_rating") or movie.get("imdbRating") or "N/A"
if not imdb_rating or imdb_rating in ("", "N/A", None):
imdb_rating = "N/A"
rt_rating = movie.get("rotten_tomatoes") or movie.get("rottenTomatoes") or "N/A"
if not rt_rating or rt_rating in ("", "N/A", None):
rt_rating = "N/A"
runtime_raw = movie.get("runtime") or movie.get("Runtime") or "N/A"
if runtime_raw and runtime_raw != "N/A":
runtime_match = re.search(r'(\d+)', str(runtime_raw))
runtime = f"{runtime_match.group(1)} min" if runtime_match else runtime_raw
else:
runtime = "N/A"
director = movie.get("director") or movie.get("Director") or "N/A"
actors = movie.get("actors") or movie.get("Actors") or "N/A"
released = movie.get("released") or movie.get("Released") or "N/A"
genre = movie.get("genre") or movie.get("Genre") or "N/A"
title_display = f"<b>{title}</b>" if format_options.title_bold else title
title_line = f"{title_display} ({year})"
info_line = f"? IMDb: {imdb_rating} ?? RT: {rt_rating} ? {runtime}"
extra_lines = []
if format_options.show_director and director != "N/A":
extra_lines.append(f"?? Director: {director}")
if format_options.show_actors and actors != "N/A":
actor_list = actors.split(", ")
if len(actor_list) > 3:
actors_display = ", ".join(actor_list[:3]) + "..."
else:
actors_display = actors
extra_lines.append(f"?? Cast: {actors_display}")
if format_options.show_released and released != "N/A":
extra_lines.append(f"?? Released: {released}")
if format_options.show_genre and genre != "N/A":
extra_lines.append(f"?? Genre: {genre}")
header_parts = [
SUBLOGUE_SENTINEL,
title_line,
info_line,
]
if extra_lines:
header_parts.extend(extra_lines)
header_parts.append("")
header_parts.append("- Generated by Sublogue")
header_text = "\n".join(header_parts)
plot_chunks = _split_plot_into_display_chunks(plot)
def format_plot_chunk(chunk_text: str, is_first_chunk: bool) -> str:
wrapped = wrap_for_tv(chunk_text)
if format_options.plot_italic:
wrapped = f"<i>{wrapped}</i>"
prefix = "Plot: " if is_first_chunk else ""
return f"{SUBLOGUE_SENTINEL}\n{prefix}{wrapped}"
blocks = []
current_ms = last_subtitle_end_ms + min_safe_gap_ms
header_duration_ms = calculate_reading_duration_ms(header_text)
header_end_ms = current_ms + header_duration_ms
blocks.append(SubtitleBlock(1, current_ms, header_end_ms, header_text))
current_ms = header_end_ms
for i, chunk in enumerate(plot_chunks):
chunk_text = format_plot_chunk(chunk, is_first_chunk=(i == 0))
chunk_duration_ms = calculate_reading_duration_ms(chunk)
chunk_end_ms = current_ms + chunk_duration_ms
blocks.append(SubtitleBlock(len(blocks) + 1, current_ms, chunk_end_ms, chunk_text))
current_ms = chunk_end_ms
return blocks
# ============================================================
# Processor
# ============================================================
@@ -1169,6 +1266,9 @@ class SubtitleProcessor:
format_options: SubtitleFormatOptions = None,
strip_keywords: bool = True,
clean_subtitle_content: bool = True,
insertion_position: str = "start",
preferred_source: str | None = None,
language: str | None = None,
) -> dict:
"""
Process a subtitle file to add plot information.
@@ -1238,6 +1338,8 @@ class SubtitleProcessor:
is_series=is_series,
season=season,
episode=episode,
preferred_source=preferred_source,
language=language,
)
if not movie:
return self._fail("No metadata found")
@@ -1343,13 +1445,22 @@ class SubtitleProcessor:
# These will NEVER overlap with or shift existing subtitles
# Returns EMPTY list if insufficient gap
# ─────────────────────────────────────────────────────────────
intro_blocks = build_intro_blocks(
movie,
plot,
first_subtitle_start_ms=first_subtitle_start_ms,
min_safe_gap_ms=500, # 500ms safety buffer before first subtitle
format_options=format_options,
)
if insertion_position == "end":
intro_blocks = build_outro_blocks(
movie,
plot,
last_subtitle_end_ms=last_original_timing,
min_safe_gap_ms=500, # 500ms safety buffer after last subtitle
format_options=format_options,
)
else:
intro_blocks = build_intro_blocks(
movie,
plot,
first_subtitle_start_ms=first_subtitle_start_ms,
min_safe_gap_ms=500, # 500ms safety buffer before first subtitle
format_options=format_options,
)
# ─────────────────────────────────────────────────────────────
# PHASE 5: Combine intro + original subtitles
@@ -1357,7 +1468,7 @@ class SubtitleProcessor:
# NOTE: We're ONLY renumbering indices (1, 2, 3...), NOT timestamps!
# The start_time and end_time of clean_subs are PRESERVED EXACTLY.
# ─────────────────────────────────────────────────────────────
final = intro_blocks + clean_subs
final = clean_subs + intro_blocks if insertion_position == "end" else intro_blocks + clean_subs
# Renumber all blocks sequentially (index only, timing unchanged)
renumbered = [
@@ -1368,7 +1479,7 @@ class SubtitleProcessor:
# Verify timing preservation (sanity check)
num_intro = len(intro_blocks)
if len(renumbered) > num_intro:
preserved_first = renumbered[num_intro]
preserved_first = renumbered[0] if insertion_position == "end" else renumbered[num_intro]
if preserved_first.start_time != first_subtitle_start_ms:
logger.error(
f"TIMING CORRUPTION DETECTED! Original first subtitle was at "
@@ -1431,6 +1542,8 @@ class SubtitleProcessor:
is_series: bool = False,
season: Optional[int] = None,
episode: Optional[int] = None,
preferred_source: str | None = None,
language: str | None = None,
) -> Optional[dict]:
"""
Fetch metadata from configured sources with fallback.
@@ -1442,14 +1555,15 @@ class SubtitleProcessor:
Year validation ensures we don't match wrong movies (e.g., "Eternity 2025"
shouldn't match "From Here to Eternity 1953").
"""
logger.info("Fetching metadata for '%s' (year=%s)", movie_name, year)
source_preference = preferred_source or self.preferred_source
logger.info("Fetching metadata for '%s' (year=%s, source=%s)", movie_name, year, source_preference)
result = None
omdb_type = "series" if is_series else "movie"
tmdb_type = "tv" if is_series else "movie"
# Try preferred source first
if self.preferred_source == "tvmaze" and self.tvmaze_client and is_series:
if source_preference == "tvmaze" and self.tvmaze_client and is_series:
result = await self.tvmaze_client.fetch_summary(
movie_name,
year=year,
@@ -1459,18 +1573,19 @@ class SubtitleProcessor:
if result:
logger.info("Found metadata via TVmaze: %s (%s)", result.get("title"), result.get("year"))
return result
elif self.preferred_source == "tmdb" and self.tmdb_client:
elif source_preference == "tmdb" and self.tmdb_client:
result = await self.tmdb_client.fetch_summary(
movie_name,
media_type=tmdb_type,
year=year,
season=season,
episode=episode,
language=language,
)
if result:
logger.info("Found metadata via TMDb: %s (%s)", result.get("title"), result.get("year"))
return result
elif self.preferred_source == "omdb" and self.omdb_client:
elif source_preference == "omdb" and self.omdb_client:
result = await self.omdb_client.fetch_summary(
movie_name,
media_type=omdb_type,
@@ -1483,7 +1598,7 @@ class SubtitleProcessor:
return result
# Fallback to other source
if not result and self.omdb_client and self.preferred_source != "omdb":
if not result and self.omdb_client and source_preference != "omdb":
result = await self.omdb_client.fetch_summary(
movie_name,
media_type=omdb_type,
@@ -1495,19 +1610,20 @@ class SubtitleProcessor:
logger.info("Found metadata via OMDb (fallback): %s (%s)", result.get("title"), result.get("year"))
return result
if not result and self.tmdb_client and self.preferred_source != "tmdb":
if not result and self.tmdb_client and source_preference != "tmdb":
result = await self.tmdb_client.fetch_summary(
movie_name,
media_type=tmdb_type,
year=year,
season=season,
episode=episode,
language=language,
)
if result:
logger.info("Found metadata via TMDb (fallback): %s (%s)", result.get("title"), result.get("year"))
return result
if not result and self.tvmaze_client and self.preferred_source != "tvmaze" and is_series:
if not result and self.tvmaze_client and source_preference != "tvmaze" and is_series:
result = await self.tvmaze_client.fetch_summary(
movie_name,
year=year,