Skip to content
Open
Prev Previous commit
Next Next commit
Harden manual and upload subtitle flows
  • Loading branch information
mjc committed Jun 15, 2026
commit 4c3f586a2483bc53b11cc7c7469330469f43060c
2 changes: 1 addition & 1 deletion bazarr/api/providers/providers_episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def get(self):
if not os.path.exists(episodePath):
return 'Episode file not found. Path mapping issue?', 500

sceneName = episodeInfo.sceneName
sceneName = episodeInfo.sceneName or "None"
profileId = episodeInfo.profileId

providers_list = get_providers()
Expand Down
2 changes: 1 addition & 1 deletion bazarr/api/providers/providers_movies.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def get(self):
if not os.path.exists(moviePath):
return 'Movie file not found. Path mapping issue?', 500

sceneName = movieInfo.sceneName
sceneName = movieInfo.sceneName or "None"
profileId = movieInfo.profileId

providers_list = get_providers()
Expand Down
5 changes: 4 additions & 1 deletion bazarr/subtitles/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_

language_set = _get_language_obj(languages=languages)
profile = get_profiles_list(profile_id=profile_id)
original_format = profile['originalFormat']
if not profile or not isinstance(profile, dict):
logging.warning(f"BAZARR unable to get subtitle profile (profile_id={profile_id})")
return None
original_format = profile.get('originalFormat', '')
hi_required = "force HI" if all([x.hi for x in language_set]) else "don't prefer"
also_forced = any([x.forced for x in language_set])
forced_required = all([x.forced for x in language_set])
Expand Down
84 changes: 58 additions & 26 deletions bazarr/subtitles/manual.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@
from .processing import process_subtitle


def _format_episode_part(value):
try:
return f"{int(value):02d}"
except (TypeError, ValueError):
return str(value) if value is not None else "??"


def _get_first_audio_language_name(audio_languages):
if not isinstance(audio_languages, list) or not audio_languages:
return None

first_audio_language = audio_languages[0]
if not isinstance(first_audio_language, dict):
return None

return first_audio_language.get('name')


@update_pools
def manual_search(path, profile_id, providers, sceneName, title, media_type):
logging.debug(f'BAZARR Manually searching subtitles for this file: {path}')
Expand Down Expand Up @@ -249,17 +267,15 @@ def episode_manually_download_specific_subtitle(sonarr_series_id, sonarr_episode
return 'Episode not found', 404

title = episodeInfo.title
season_part = _format_episode_part(episodeInfo.season)
episode_part = _format_episode_part(episodeInfo.episode)
jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Manually downloading Subtitles for {title} - "
f"S{episodeInfo.season:02d}E{episodeInfo.episode:02d} - "
f"S{season_part}E{episode_part} - "
f"{episodeInfo.episodeTitle}")
episodePath = path_mappings.path_replace(episodeInfo.path)
sceneName = episodeInfo.sceneName or "None"
sceneName = episodeInfo.sceneName or None

audio_language_list = get_audio_profile_languages(episodeInfo.audio_language)
if len(audio_language_list) > 0:
audio_language = audio_language_list[0]['name']
else:
audio_language = 'None'
audio_language = _get_first_audio_language_name(get_audio_profile_languages(episodeInfo.audio_language))

try:
result = manual_download_subtitle(episodePath, audio_language, hi, forced, subtitle, selected_provider,
Expand All @@ -275,12 +291,12 @@ def episode_manually_download_specific_subtitle(sonarr_series_id, sonarr_episode
elif result:
store_subtitles(sonarr_episode_id)
history_log(2, sonarr_series_id, sonarr_episode_id, result)
if not settings.general.dont_notify_manual_actions:
if not settings.general.dont_notify_manual_actions and hasattr(result, 'message'):
send_notifications(sonarr_series_id, sonarr_episode_id, result.message)
return '', 204
finally:
jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Manually downloaded Subtitles for {title} - "
f"S{episodeInfo.season:02d}E{episodeInfo.episode:02d} - "
f"S{season_part}E{episode_part} - "
f"{episodeInfo.episodeTitle}")


Expand All @@ -305,13 +321,9 @@ def movie_manually_download_specific_subtitle(radarr_id, hi, forced, use_origina
jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Manually downloading Subtitles for {title} "
f"({movieInfo.year})")
moviePath = path_mappings.path_replace_movie(movieInfo.path)
sceneName = movieInfo.sceneName or "None"
sceneName = movieInfo.sceneName or None

audio_language_list = get_audio_profile_languages(movieInfo.audio_language)
if len(audio_language_list) > 0:
audio_language = audio_language_list[0]['name']
else:
audio_language = 'None'
audio_language = _get_first_audio_language_name(get_audio_profile_languages(movieInfo.audio_language))

try:
result = manual_download_subtitle(moviePath, audio_language, hi, forced, subtitle, selected_provider,
Expand All @@ -327,7 +339,7 @@ def movie_manually_download_specific_subtitle(radarr_id, hi, forced, use_origina
elif result:
store_subtitles_movie(radarr_id)
history_log_movie(2, radarr_id, result)
if not settings.general.dont_notify_manual_actions:
if not settings.general.dont_notify_manual_actions and hasattr(result, 'message'):
send_notifications_movie(radarr_id, result.message)
return '', 204
finally:
Expand All @@ -338,23 +350,43 @@ def movie_manually_download_specific_subtitle(radarr_id, hi, forced, use_origina
def _get_language_obj(profile_id):
language_set = set()

profile = get_profiles_list(profile_id=int(profile_id))
language_items = profile['items']
original_format = profile['originalFormat']
try:
normalized_profile_id = int(profile_id)
except (TypeError, ValueError):
return language_set, False

profile = get_profiles_list(profile_id=normalized_profile_id)
if not isinstance(profile, dict):
return language_set, False

language_items = profile.get('items')
if not isinstance(language_items, list):
language_items = []
original_format = profile.get('originalFormat', False)

for language in language_items:
forced = language['forced']
hi = language['hi']
language = language['language']
if not isinstance(language, dict):
continue

forced = language.get('forced')
hi = language.get('hi')
language_code = language.get('language')
if not isinstance(language_code, str) or not language_code.strip():
continue

lang = alpha3_from_alpha2(language)
lang = alpha3_from_alpha2(language_code)
if not lang:
continue

lang_obj = _get_lang_obj(lang)
try:
lang_obj = _get_lang_obj(lang)
except (AttributeError, TypeError, ValueError):
continue

if forced == "True":
if forced is True:
lang_obj = Language.rebuild(lang_obj, forced=True)

if hi == "True":
if hi is True:
lang_obj = Language.rebuild(lang_obj, hi=True)

language_set.add(lang_obj)
Expand Down
4 changes: 2 additions & 2 deletions bazarr/subtitles/mass_download/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def episode_download_subtitles(no, job_id=None, job_sub_function=False, provider
for result in generate_subtitles(episodePath,
languages,
audio_language,
episode.sceneName,
str(episode.sceneName),
episode.title,
'series',
episode.profileId,
Expand Down Expand Up @@ -210,7 +210,7 @@ def episode_download_specific_subtitles(sonarr_series_id, sonarr_episode_id, lan
if not os.path.exists(episodePath):
return 'Episode file not found. Path mapping issue?', 500

sceneName = episodeInfo.sceneName
sceneName = episodeInfo.sceneName or "None"

title = episodeInfo.title

Expand Down
25 changes: 15 additions & 10 deletions bazarr/subtitles/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
audio_language_code3 = alpha3_from_language(audio_language)
downloaded_path = subtitle.storage_path
subtitle_id = subtitle.id
if subtitle.language.hi:
language_forced = subtitle.language.forced if subtitle.language else False
language_hi = subtitle.language.hi if subtitle.language else False
if language_hi:
modifier_string = " HI"
elif subtitle.language.forced:
elif language_forced:
modifier_string = " forced"
else:
modifier_string = ""
Expand All @@ -71,7 +73,10 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
else:
action = "downloaded"

percent_score = round(subtitle.score * 100 / max_score, 2)
if max_score and max_score > 0:
percent_score = round(subtitle.score * 100 / max_score, 2)
else:
percent_score = 0
message = (f"{downloaded_language}{modifier_string} subtitles {action} from {downloaded_provider} with a score of "
f"{percent_score}%.")

Expand All @@ -93,8 +98,8 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
if sync_checker(subtitle) is True:
from .sync import sync_subtitles
sync_subtitles(video_path=path, srt_path=downloaded_path,
forced=subtitle.language.forced,
hi=subtitle.language.hi,
forced=language_forced,
hi=language_hi,
srt_lang=downloaded_language_code2,
percent_score=percent_score,
sonarr_series_id=episode_metadata.sonarrSeriesId,
Expand All @@ -113,8 +118,8 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
if sync_checker(subtitle) is True:
from .sync import sync_subtitles
sync_subtitles(video_path=path, srt_path=downloaded_path,
forced=subtitle.language.forced,
hi=subtitle.language.hi,
forced=language_forced,
hi=language_hi,
srt_lang=downloaded_language_code2,
percent_score=percent_score,
radarr_id=movie_metadata.radarrId,
Expand Down Expand Up @@ -151,7 +156,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
if settings.general.use_plex is True:
if settings.plex.update_series_library is True:
# Use specific item refresh instead of full library scan
plex_refresh_item(episode_metadata.imdbId, is_movie=False,
plex_refresh_item(episode_metadata.imdbId, is_movie=False,
season=episode_metadata.season, episode=episode_metadata.episode)
if settings.plex.set_episode_added is True:
plex_set_episode_added_date_now(episode_metadata)
Expand Down Expand Up @@ -192,10 +197,10 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
downloaded_language_code2=downloaded_language_code2,
downloaded_provider=downloaded_provider,
score=subtitle.score,
forced=subtitle.language.forced,
forced=language_forced,
subtitle_id=subtitle.id,
reversed_subtitles_path=reversed_subtitles_path,
hearing_impaired=subtitle.language.hi,
hearing_impaired=language_hi,
matched=list(subtitle.matches or []),
not_matched=_get_not_matched(subtitle, media_type)),

Expand Down
5 changes: 4 additions & 1 deletion bazarr/subtitles/refiners/arr_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ def refine_info_url(video):
else:
return

for grab in history['records']:
if not isinstance(history, dict):
return

for grab in history.get('records', []):
# take the latest grab for the episode
if 'nzbInfoUrl' in grab['data'] and grab['data']['nzbInfoUrl']:
video.info_url = grab['data']['nzbInfoUrl']
Expand Down
33 changes: 26 additions & 7 deletions bazarr/subtitles/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@
from jellyfin.operations import jellyfin_refresh_item


def _get_profile_original_format(profile_id):
profile = get_profiles_list(profile_id)
if not isinstance(profile, dict):
return False

return bool(profile.get("originalFormat", 0))


def _get_audio_language_payload(audio_languages):
empty_audio_language = {'name': '', 'code2': '', 'code3': ''}
if not isinstance(audio_languages, list) or not audio_languages:
return empty_audio_language

first_audio_language = audio_languages[0]
if not isinstance(first_audio_language, dict):
return empty_audio_language

return {
key: value if isinstance(value := first_audio_language.get(key), str) else ''
for key in empty_audio_language
}


def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, filename, audio_language, job_id=None,
sonarrSeriesId=None, sonarrEpisodeId=None, radarrId=None):
if not job_id:
Expand Down Expand Up @@ -81,7 +104,7 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, fil
.first()

if episode_metadata:
use_original_format = bool(get_profiles_list(episode_metadata.profileId)["originalFormat"])
use_original_format = _get_profile_original_format(episode_metadata.profileId)
else:
return
else:
Expand All @@ -92,15 +115,11 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, fil
.first()

if movie_metadata:
use_original_format = bool(get_profiles_list(movie_metadata.profileId)["originalFormat"])
use_original_format = _get_profile_original_format(movie_metadata.profileId)
else:
return

audio_language = get_audio_profile_languages(audio_language)
if len(audio_language) and isinstance(audio_language[0], dict):
audio_language = audio_language[0]
else:
audio_language = {'name': '', 'code2': '', 'code3': ''}
audio_language = _get_audio_language_payload(get_audio_profile_languages(audio_language))

sub = Subtitle(
lang_obj,
Expand Down
9 changes: 2 additions & 7 deletions bazarr/subtitles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,9 @@ def get_video(path, title, sceneName, providers=None, media_type="movie"):
logging.debug(f'BAZARR guessing video object using video file path: {path}')
skip_hashing = settings.general.skip_hashing
video = parse_video(path, hints=hints, skip_hashing=skip_hashing, dry_run=False, providers=providers)
if isinstance(sceneName, str):
normalized_scene_name = sceneName.strip()
else:
normalized_scene_name = ""

if normalized_scene_name and normalized_scene_name.lower() != "none":
if sceneName != "None":
# refine the video object using the sceneName and update the video object accordingly
scenename_with_extension = normalized_scene_name + os.path.splitext(path)[1]
scenename_with_extension = sceneName + os.path.splitext(path)[1]
logging.debug(f'BAZARR guessing video object using scene name: {scenename_with_extension}')
scenename_video = parse_video(scenename_with_extension, hints=hints, dry_run=True)
refine_video_with_scenename(initial_video=video, scenename_video=scenename_video)
Expand Down
2 changes: 1 addition & 1 deletion bazarr/subtitles/wanted/movies.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _wanted_movie(movie, providers_list, job_id=None):
for result in generate_subtitles(path_mappings.path_replace_movie(movie.path),
languages,
audio_language,
movie.sceneName,
str(movie.sceneName),
movie.title,
'movie',
movie.profileId,
Expand Down
2 changes: 1 addition & 1 deletion bazarr/subtitles/wanted/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _wanted_episode(episode, providers_list, job_id=None):
for result in generate_subtitles(path_mappings.path_replace(episode.path),
languages,
audio_language,
episode.sceneName,
str(episode.sceneName),
episode.title,
'series',
episode.profileId,
Expand Down
Loading