Skip to content

Commit

Permalink
Improved subtitles synchronisation settings and added a manual sync m…
Browse files Browse the repository at this point in the history
…odal
  • Loading branch information
morpheus65535 authored Jan 11, 2024
1 parent 0807bd9 commit 0e648b5
Show file tree
Hide file tree
Showing 28 changed files with 931 additions and 225 deletions.
102 changes: 87 additions & 15 deletions bazarr/api/subtitles/subtitles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
import sys
import gc

from flask_restx import Resource, Namespace, reqparse
from flask_restx import Resource, Namespace, reqparse, fields, marshal

from app.database import TableEpisodes, TableMovies, database, select
from languages.get_languages import alpha3_from_alpha2
from utilities.path_mappings import path_mappings
from utilities.video_analyzer import subtitles_sync_references
from subtitles.tools.subsyncer import SubSyncer
from subtitles.tools.translate import translate_subtitles_file
from subtitles.tools.mods import subtitles_apply_mods
from subtitles.indexer.series import store_subtitles
from subtitles.indexer.movies import store_subtitles_movie
from app.config import settings
from app.config import settings, empty_values
from app.event_handler import event_stream

from ..utils import authenticate
Expand All @@ -25,17 +26,77 @@

@api_ns_subtitles.route('subtitles')
class Subtitles(Resource):
get_request_parser = reqparse.RequestParser()
get_request_parser.add_argument('subtitlesPath', type=str, required=True, help='External subtitles file path')
get_request_parser.add_argument('sonarrEpisodeId', type=int, required=False, help='Sonarr Episode ID')
get_request_parser.add_argument('radarrMovieId', type=int, required=False, help='Radarr Movie ID')

audio_tracks_data_model = api_ns_subtitles.model('audio_tracks_data_model', {
'stream': fields.String(),
'name': fields.String(),
'language': fields.String(),
})

embedded_subtitles_data_model = api_ns_subtitles.model('embedded_subtitles_data_model', {
'stream': fields.String(),
'name': fields.String(),
'language': fields.String(),
'forced': fields.Boolean(),
'hearing_impaired': fields.Boolean(),
})

external_subtitles_data_model = api_ns_subtitles.model('external_subtitles_data_model', {
'name': fields.String(),
'path': fields.String(),
'language': fields.String(),
'forced': fields.Boolean(),
'hearing_impaired': fields.Boolean(),
})

get_response_model = api_ns_subtitles.model('SubtitlesGetResponse', {
'audio_tracks': fields.Nested(audio_tracks_data_model),
'embedded_subtitles_tracks': fields.Nested(embedded_subtitles_data_model),
'external_subtitles_tracks': fields.Nested(external_subtitles_data_model),
})

@authenticate
@api_ns_subtitles.response(200, 'Success')
@api_ns_subtitles.response(401, 'Not Authenticated')
@api_ns_subtitles.doc(parser=get_request_parser)
def get(self):
"""Return available audio and embedded subtitles tracks with external subtitles. Used for manual subsync
modal"""
args = self.get_request_parser.parse_args()
subtitlesPath = args.get('subtitlesPath')
episodeId = args.get('sonarrEpisodeId', None)
movieId = args.get('radarrMovieId', None)

result = subtitles_sync_references(subtitles_path=subtitlesPath, sonarr_episode_id=episodeId,
radarr_movie_id=movieId)

return marshal(result, self.get_response_model, envelope='data')

patch_request_parser = reqparse.RequestParser()
patch_request_parser.add_argument('action', type=str, required=True,
help='Action from ["sync", "translate" or mods name]')
patch_request_parser.add_argument('language', type=str, required=True, help='Language code2')
patch_request_parser.add_argument('path', type=str, required=True, help='Subtitles file path')
patch_request_parser.add_argument('type', type=str, required=True, help='Media type from ["episode", "movie"]')
patch_request_parser.add_argument('id', type=int, required=True, help='Media ID (episodeId, radarrId)')
patch_request_parser.add_argument('forced', type=str, required=False, help='Forced subtitles from ["True", "False"]')
patch_request_parser.add_argument('forced', type=str, required=False,
help='Forced subtitles from ["True", "False"]')
patch_request_parser.add_argument('hi', type=str, required=False, help='HI subtitles from ["True", "False"]')
patch_request_parser.add_argument('original_format', type=str, required=False,
help='Use original subtitles format from ["True", "False"]')
patch_request_parser.add_argument('reference', type=str, required=False,
help='Reference to use for sync from video file track number (a:0) or some '
'subtitles file path')
patch_request_parser.add_argument('max_offset_seconds', type=str, required=False,
help='Maximum offset seconds to allow')
patch_request_parser.add_argument('no_fix_framerate', type=str, required=False,
help='Don\'t try to fix framerate from ["True", "False"]')
patch_request_parser.add_argument('gss', type=str, required=False,
help='Use Golden-Section Search from ["True", "False"]')

@authenticate
@api_ns_subtitles.doc(parser=patch_request_parser)
Expand Down Expand Up @@ -79,19 +140,30 @@ def patch(self):
video_path = path_mappings.path_replace_movie(metadata.path)

if action == 'sync':
sync_kwargs = {
'video_path': video_path,
'srt_path': subtitles_path,
'srt_lang': language,
'reference': args.get('reference') if args.get('reference') not in empty_values else video_path,
'max_offset_seconds': args.get('max_offset_seconds') if args.get('max_offset_seconds') not in
empty_values else str(settings.subsync.max_offset_seconds),
'no_fix_framerate': args.get('no_fix_framerate') == 'True',
'gss': args.get('gss') == 'True',
}

subsync = SubSyncer()
if media_type == 'episode':
subsync.sync(video_path=video_path, srt_path=subtitles_path,
srt_lang=language, media_type='series', sonarr_series_id=metadata.sonarrSeriesId,
sonarr_episode_id=id)
else:
try:
subsync.sync(video_path=video_path, srt_path=subtitles_path,
srt_lang=language, media_type='movies', radarr_id=id)
except OSError:
return 'Unable to edit subtitles file. Check logs.', 409
del subsync
gc.collect()
try:
if media_type == 'episode':
sync_kwargs['sonarr_series_id'] = metadata.sonarrSeriesId
sync_kwargs['sonarr_episode_id'] = id
else:
sync_kwargs['radarr_id'] = id
subsync.sync(**sync_kwargs)
except OSError:
return 'Unable to edit subtitles file. Check logs.', 409
finally:
del subsync
gc.collect()
elif action == 'translate':
from_language = subtitles_lang_from_filename(subtitles_path)
dest_language = language
Expand Down
4 changes: 4 additions & 0 deletions bazarr/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ class Validator(OriginalValidator):
Validator('subsync.checker', must_exist=True, default={}, is_type_of=dict),
Validator('subsync.checker.blacklisted_providers', must_exist=True, default=[], is_type_of=list),
Validator('subsync.checker.blacklisted_languages', must_exist=True, default=[], is_type_of=list),
Validator('subsync.no_fix_framerate', must_exist=True, default=True, is_type_of=bool),
Validator('subsync.gss', must_exist=True, default=True, is_type_of=bool),
Validator('subsync.max_offset_seconds', must_exist=True, default=60, is_type_of=int,
is_in=[60, 120, 300, 600]),

# series_scores section
Validator('series_scores.hash', must_exist=True, default=359, is_type_of=int),
Expand Down
4 changes: 2 additions & 2 deletions bazarr/subtitles/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
from .sync import sync_subtitles
sync_subtitles(video_path=path, srt_path=downloaded_path,
forced=subtitle.language.forced,
srt_lang=downloaded_language_code2, media_type=media_type,
srt_lang=downloaded_language_code2,
percent_score=percent_score,
sonarr_series_id=episode_metadata.sonarrSeriesId,
sonarr_episode_id=episode_metadata.sonarrEpisodeId)
Expand All @@ -106,7 +106,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
from .sync import sync_subtitles
sync_subtitles(video_path=path, srt_path=downloaded_path,
forced=subtitle.language.forced,
srt_lang=downloaded_language_code2, media_type=media_type,
srt_lang=downloaded_language_code2,
percent_score=percent_score,
radarr_id=movie_metadata.radarrId)

Expand Down
4 changes: 2 additions & 2 deletions bazarr/subtitles/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from subtitles.tools.subsyncer import SubSyncer


def sync_subtitles(video_path, srt_path, srt_lang, forced, media_type, percent_score, sonarr_series_id=None,
def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr_series_id=None,
sonarr_episode_id=None, radarr_id=None):
if forced:
logging.debug('BAZARR cannot sync forced subtitles. Skipping sync routine.')
Expand All @@ -26,7 +26,7 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, media_type, percent_s

if not use_subsync_threshold or (use_subsync_threshold and percent_score < float(subsync_threshold)):
subsync = SubSyncer()
subsync.sync(video_path=video_path, srt_path=srt_path, srt_lang=srt_lang, media_type=media_type,
subsync.sync(video_path=video_path, srt_path=srt_path, srt_lang=srt_lang,
sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, radarr_id=radarr_id)
del subsync
gc.collect()
Expand Down
54 changes: 38 additions & 16 deletions bazarr/subtitles/tools/subsyncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ def __init__(self):
self.vad = 'subs_then_webrtc'
self.log_dir_path = os.path.join(args.config_dir, 'log')

def sync(self, video_path, srt_path, srt_lang, media_type, sonarr_series_id=None, sonarr_episode_id=None,
radarr_id=None):
def sync(self, video_path, srt_path, srt_lang, sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None,
reference=None, max_offset_seconds=str(settings.subsync.max_offset_seconds),
no_fix_framerate=settings.subsync.no_fix_framerate, gss=settings.subsync.gss):
self.reference = video_path
self.srtin = srt_path
self.srtout = f'{os.path.splitext(self.srtin)[0]}.synced.srt'
Expand All @@ -52,20 +53,41 @@ def sync(self, video_path, srt_path, srt_lang, media_type, sonarr_series_id=None
logging.debug('BAZARR FFmpeg used is %s', ffmpeg_exe)

self.ffmpeg_path = os.path.dirname(ffmpeg_exe)
unparsed_args = [self.reference, '-i', self.srtin, '-o', self.srtout, '--ffmpegpath', self.ffmpeg_path, '--vad',
self.vad, '--log-dir-path', self.log_dir_path, '--output-encoding', 'same']
if settings.subsync.force_audio:
unparsed_args.append('--no-fix-framerate')
unparsed_args.append('--reference-stream')
unparsed_args.append('a:0')
if settings.subsync.debug:
unparsed_args.append('--make-test-case')
parser = make_parser()
self.args = parser.parse_args(args=unparsed_args)
if os.path.isfile(self.srtout):
os.remove(self.srtout)
logging.debug('BAZARR deleted the previous subtitles synchronization attempt file.')
try:
unparsed_args = [self.reference, '-i', self.srtin, '-o', self.srtout, '--ffmpegpath', self.ffmpeg_path,
'--vad', self.vad, '--log-dir-path', self.log_dir_path, '--max-offset-seconds',
max_offset_seconds, '--output-encoding', 'same']
if not settings.general.utf8_encode:
unparsed_args.append('--output-encoding')
unparsed_args.append('same')

if no_fix_framerate:
unparsed_args.append('--no-fix-framerate')

if gss:
unparsed_args.append('--gss')

if reference and reference != video_path and os.path.isfile(reference):
# subtitles path provided
self.reference = reference
elif reference and isinstance(reference, str) and len(reference) == 3 and reference[:2] in ['a:', 's:']:
# audio or subtitles track id provided
unparsed_args.append('--reference-stream')
unparsed_args.append(reference)
elif settings.subsync.force_audio:
# nothing else match and force audio settings is enabled
unparsed_args.append('--reference-stream')
unparsed_args.append('a:0')

if settings.subsync.debug:
unparsed_args.append('--make-test-case')

parser = make_parser()
self.args = parser.parse_args(args=unparsed_args)

if os.path.isfile(self.srtout):
os.remove(self.srtout)
logging.debug('BAZARR deleted the previous subtitles synchronization attempt file.')
result = run(self.args)
except Exception:
logging.exception(
Expand Down Expand Up @@ -95,7 +117,7 @@ def sync(self, video_path, srt_path, srt_lang, media_type, sonarr_series_id=None
reversed_subtitles_path=srt_path,
hearing_impaired=None)

if media_type == 'series':
if sonarr_episode_id:
history_log(action=5, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id,
result=result)
else:
Expand Down
8 changes: 4 additions & 4 deletions bazarr/subtitles/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,16 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud
return
series_id = episode_metadata.sonarrSeriesId
episode_id = episode_metadata.sonarrEpisodeId
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, media_type=media_type,
percent_score=100, sonarr_series_id=episode_metadata.sonarrSeriesId, forced=forced,
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100,
sonarr_series_id=episode_metadata.sonarrSeriesId, forced=forced,
sonarr_episode_id=episode_metadata.sonarrEpisodeId)
else:
if not movie_metadata:
return
series_id = ""
episode_id = movie_metadata.radarrId
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, media_type=media_type,
percent_score=100, radarr_id=movie_metadata.radarrId, forced=forced)
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100,
radarr_id=movie_metadata.radarrId, forced=forced)

if use_postprocessing:
command = pp_replace(postprocessing_cmd, path, subtitle_path, uploaded_language, uploaded_language_code2,
Expand Down
Loading

0 comments on commit 0e648b5

Please sign in to comment.