1.0.6 - Multi language support (TMDb), Per folder rule settings
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user