mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-23 22:27:17 -04:00
Merge branch 'development' into morpheus
# Conflicts: # bazarr/get_subtitle.py # bazarr/main.py # views/movie.tpl
This commit is contained in:
commit
5b96df4c40
18 changed files with 880 additions and 131 deletions
|
@ -17,7 +17,6 @@ defaults = {
|
|||
'single_language': 'False',
|
||||
'minimum_score': '90',
|
||||
'use_scenename': 'True',
|
||||
'use_mediainfo': 'True',
|
||||
'use_postprocessing': 'False',
|
||||
'postprocessing_cmd': '',
|
||||
'use_sonarr': 'False',
|
||||
|
|
|
@ -4,8 +4,8 @@ import os
|
|||
import subprocess
|
||||
import locale
|
||||
|
||||
from config import settings
|
||||
from utils import get_binary
|
||||
from pyprobe.pyprobe import VideoFileParser
|
||||
|
||||
class NotMKVAndNoFFprobe(Exception):
|
||||
pass
|
||||
|
@ -20,24 +20,22 @@ class EmbeddedSubsReader:
|
|||
def list_languages(self, file):
|
||||
subtitles_list = []
|
||||
|
||||
if os.path.splitext(file)[1] == '.mkv':
|
||||
with open(file, 'rb') as f:
|
||||
mkv = enzyme.MKV(f)
|
||||
for subtitle_track in mkv.subtitle_tracks:
|
||||
subtitles_list.append([subtitle_track.language, subtitle_track.forced])
|
||||
if self.ffprobe:
|
||||
parser = VideoFileParser(ffprobe=self.ffprobe, includeMissing=True, rawMode=False)
|
||||
data = parser.parseFfprobe(file)
|
||||
|
||||
detected_languages = []
|
||||
|
||||
for detected_language in data['subtitles']:
|
||||
subtitles_list.append([detected_language['language'], detected_language['forced']])
|
||||
else:
|
||||
if self.ffprobe:
|
||||
detected_languages = []
|
||||
try:
|
||||
detected_languages = subprocess.check_output([self.ffprobe, "-loglevel", "error", "-select_streams", "s", "-show_entries", "stream_tags=language", "-of", "csv=p=0", file.encode(locale.getpreferredencoding())], universal_newlines=True, stderr=subprocess.STDOUT).strip().split("\n")
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise FFprobeError(e.output)
|
||||
else:
|
||||
for detected_language in detected_languages:
|
||||
subtitles_list.append([detected_language, False])
|
||||
# I can't get the forced flag from ffprobe so I always assume it isn't forced
|
||||
if os.path.splitext(file)[1] == '.mkv':
|
||||
with open(file, 'rb') as f:
|
||||
mkv = enzyme.MKV(f)
|
||||
for subtitle_track in mkv.subtitle_tracks:
|
||||
subtitles_list.append([subtitle_track.language, subtitle_track.forced])
|
||||
|
||||
return subtitles_list
|
||||
|
||||
|
||||
embedded_subs_reader = EmbeddedSubsReader()
|
||||
embedded_subs_reader = EmbeddedSubsReader()
|
||||
|
|
|
@ -19,7 +19,7 @@ from subzero.video import parse_video
|
|||
from subliminal import region, score as subliminal_scores, \
|
||||
list_subtitles, Episode, Movie
|
||||
from subliminal_patch.core import SZAsyncProviderPool, download_best_subtitles, save_subtitles, download_subtitles, \
|
||||
list_all_subtitles
|
||||
list_all_subtitles, get_subtitle_path
|
||||
from subliminal_patch.score import compute_score
|
||||
from subliminal.refiners.tvdb import series_re
|
||||
from get_languages import language_from_alpha3, alpha2_from_alpha3, alpha3_from_alpha2, language_from_alpha2
|
||||
|
@ -32,19 +32,18 @@ from notifier import send_notifications, send_notifications_movie
|
|||
from get_providers import get_providers, get_providers_auth, provider_throttle, provider_pool
|
||||
from get_args import args
|
||||
from queueconfig import notifications
|
||||
from pymediainfo import MediaInfo
|
||||
from pyprobe.pyprobe import VideoFileParser
|
||||
from database import TableShows, TableEpisodes, TableMovies, TableHistory, TableHistoryMovie
|
||||
from peewee import fn, JOIN
|
||||
|
||||
|
||||
def get_video(path, title, sceneName, use_scenename, use_mediainfo, providers=None, media_type="movie"):
|
||||
def get_video(path, title, sceneName, use_scenename, providers=None, media_type="movie"):
|
||||
"""
|
||||
Construct `Video` instance
|
||||
:param path: path to video
|
||||
:param title: series/movie title
|
||||
:param sceneName: sceneName
|
||||
:param use_scenename: use sceneName
|
||||
:param use_mediainfo: use media info to refine the video
|
||||
:param providers: provider list for selective hashing
|
||||
:param media_type: movie/series
|
||||
:return: `Video` instance
|
||||
|
@ -66,10 +65,9 @@ def get_video(path, title, sceneName, use_scenename, use_mediainfo, providers=No
|
|||
video.used_scene_name = used_scene_name
|
||||
video.original_name = original_name
|
||||
video.original_path = original_path
|
||||
|
||||
refine_from_db(original_path, video)
|
||||
|
||||
if platform.system() != "Linux" and use_mediainfo:
|
||||
refine_from_mediainfo(original_path, video)
|
||||
refine_from_ffprobe(original_path, video)
|
||||
|
||||
logging.debug('BAZARR is using those video object properties: %s', vars(video))
|
||||
return video
|
||||
|
@ -143,7 +141,6 @@ def download_subtitle(path, language, hi, forced, providers, providers_auth, sce
|
|||
language_set.add(lang_obj)
|
||||
|
||||
use_scenename = settings.general.getboolean('use_scenename')
|
||||
use_mediainfo = settings.general.getboolean('use_mediainfo')
|
||||
minimum_score = settings.general.minimum_score
|
||||
minimum_score_movie = settings.general.minimum_score_movie
|
||||
use_postprocessing = settings.general.getboolean('use_postprocessing')
|
||||
|
@ -159,7 +156,7 @@ def download_subtitle(path, language, hi, forced, providers, providers_auth, sce
|
|||
post_download_hook=None,
|
||||
language_hook=None
|
||||
"""
|
||||
video = get_video(force_unicode(path), title, sceneName, use_scenename, use_mediainfo, providers=providers,
|
||||
video = get_video(force_unicode(path), title, sceneName, use_scenename, providers=providers,
|
||||
media_type=media_type)
|
||||
if video:
|
||||
min_score, max_score, scores = get_scores(video, media_type, min_score_movie_perc=int(minimum_score_movie),
|
||||
|
@ -282,7 +279,8 @@ def manual_search(path, language, hi, forced, providers, providers_auth, sceneNa
|
|||
logging.debug('BAZARR Manually searching subtitles for this file: ' + path)
|
||||
|
||||
final_subtitles = []
|
||||
|
||||
|
||||
initial_hi = True if hi == "True" else False
|
||||
if hi == "True":
|
||||
hi = "force HI"
|
||||
else:
|
||||
|
@ -311,13 +309,12 @@ def manual_search(path, language, hi, forced, providers, providers_auth, sceneNa
|
|||
language_set.add(lang_obj)
|
||||
|
||||
use_scenename = settings.general.getboolean('use_scenename')
|
||||
use_mediainfo = settings.general.getboolean('use_mediainfo')
|
||||
minimum_score = settings.general.minimum_score
|
||||
minimum_score_movie = settings.general.minimum_score_movie
|
||||
use_postprocessing = settings.general.getboolean('use_postprocessing')
|
||||
postprocessing_cmd = settings.general.postprocessing_cmd
|
||||
if providers:
|
||||
video = get_video(force_unicode(path), title, sceneName, use_scenename, use_mediainfo, providers=providers,
|
||||
video = get_video(force_unicode(path), title, sceneName, use_scenename, providers=providers,
|
||||
media_type=media_type)
|
||||
else:
|
||||
logging.info("BAZARR All providers are throttled")
|
||||
|
@ -357,8 +354,11 @@ def manual_search(path, language, hi, forced, providers, providers_auth, sceneNa
|
|||
if can_verify_series and not {"series", "season", "episode"}.issubset(matches):
|
||||
logging.debug(u"BAZARR Skipping %s, because it doesn't match our series/episode", s)
|
||||
continue
|
||||
|
||||
score = compute_score(matches, s, video, hearing_impaired=hi)
|
||||
|
||||
if s.hearing_impaired == initial_hi:
|
||||
matches.add('hearing_impaired')
|
||||
|
||||
score = compute_score(matches, s, video, hearing_impaired=initial_hi)
|
||||
not_matched = scores - matches
|
||||
s.score = score
|
||||
|
||||
|
@ -389,11 +389,10 @@ def manual_download_subtitle(path, language, hi, forced, subtitle, provider, pro
|
|||
|
||||
subtitle = pickle.loads(codecs.decode(subtitle.encode(), "base64"))
|
||||
use_scenename = settings.general.getboolean('use_scenename')
|
||||
use_mediainfo = settings.general.getboolean('use_mediainfo')
|
||||
use_postprocessing = settings.general.getboolean('use_postprocessing')
|
||||
postprocessing_cmd = settings.general.postprocessing_cmd
|
||||
single = settings.general.getboolean('single_language')
|
||||
video = get_video(force_unicode(path), title, sceneName, use_scenename, use_mediainfo, providers={provider},
|
||||
video = get_video(force_unicode(path), title, sceneName, use_scenename, providers={provider},
|
||||
media_type=media_type)
|
||||
if video:
|
||||
min_score, max_score, scores = get_scores(video, media_type)
|
||||
|
@ -494,6 +493,51 @@ def manual_download_subtitle(path, language, hi, forced, subtitle, provider, pro
|
|||
logging.debug('BAZARR Ended manually downloading subtitles for file: ' + path)
|
||||
|
||||
|
||||
def manual_upload_subtitle(path, language, forced, title, scene_name, media_type, subtitle):
|
||||
logging.debug('BAZARR Manually uploading subtitles for this file: ' + path)
|
||||
|
||||
single = settings.general.getboolean('single_language')
|
||||
|
||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
||||
|
||||
_, ext = os.path.splitext(subtitle.filename)
|
||||
|
||||
language = alpha3_from_alpha2(language)
|
||||
|
||||
if language == 'pob':
|
||||
lang_obj = Language('por', 'BR')
|
||||
else:
|
||||
lang_obj = Language(language)
|
||||
|
||||
if forced:
|
||||
lang_obj = Language.rebuild(lang_obj, forced=True)
|
||||
|
||||
subtitle_path = get_subtitle_path(video_path=force_unicode(path),
|
||||
language=None if single else lang_obj,
|
||||
extension=ext,
|
||||
forced_tag=forced)
|
||||
|
||||
subtitle_path = force_unicode(subtitle_path)
|
||||
|
||||
if os.path.exists(subtitle_path):
|
||||
os.remove(subtitle_path)
|
||||
|
||||
subtitle.save(subtitle_path)
|
||||
|
||||
if chmod:
|
||||
os.chmod(subtitle_path, chmod)
|
||||
|
||||
message = language_from_alpha3(language) + (" forced" if forced else "") + " subtitles manually uploaded."
|
||||
|
||||
if media_type == 'series':
|
||||
reversed_path = path_replace_reverse(path)
|
||||
else:
|
||||
reversed_path = path_replace_reverse_movie(path)
|
||||
|
||||
return message, reversed_path
|
||||
|
||||
|
||||
def series_download_subtitles(no):
|
||||
episodes_details_clause = [
|
||||
(TableEpisodes.sonarr_series_id == no),
|
||||
|
@ -962,31 +1006,42 @@ def refine_from_db(path, video):
|
|||
return video
|
||||
|
||||
|
||||
def refine_from_mediainfo(path, video):
|
||||
if video.fps:
|
||||
return
|
||||
|
||||
exe = get_binary('mediainfo')
|
||||
def refine_from_ffprobe(path, video):
|
||||
exe = get_binary('ffprobe')
|
||||
if not exe:
|
||||
logging.debug('BAZARR MediaInfo library not found!')
|
||||
logging.debug('BAZARR FFprobe not found!')
|
||||
return
|
||||
else:
|
||||
logging.debug('BAZARR MediaInfo library used is %s', exe)
|
||||
logging.debug('BAZARR FFprobe used is %s', exe)
|
||||
|
||||
media_info = MediaInfo.parse(path, library_file=exe)
|
||||
|
||||
video_track = next((t for t in media_info.tracks if t.track_type == 'Video'), None)
|
||||
if not video_track:
|
||||
logging.debug('BAZARR MediaInfo was unable to find video tracks in the file!')
|
||||
return
|
||||
|
||||
logging.debug('MediaInfo found: %s', video_track.to_data())
|
||||
|
||||
if not video.fps:
|
||||
if video_track.frame_rate:
|
||||
video.fps = float(video_track.frame_rate)
|
||||
elif video_track.framerate_num and video_track.framerate_den:
|
||||
video.fps = round(float(video_track.framerate_num) / float(video_track.framerate_den), 3)
|
||||
parser = VideoFileParser(ffprobe=exe, includeMissing=True, rawMode=False)
|
||||
data = parser.parseFfprobe(path)
|
||||
|
||||
logging.debug('FFprobe found: %s', data)
|
||||
|
||||
if 'videos' not in data:
|
||||
logging.debug('BAZARR FFprobe was unable to find video tracks in the file!')
|
||||
else:
|
||||
if 'resolution' in data['videos'][0]:
|
||||
if not video.resolution:
|
||||
if data['videos'][0]['resolution'][0] >= 3200:
|
||||
video.resolution = "2160p"
|
||||
elif data['videos'][0]['resolution'][0] >= 1800:
|
||||
video.resolution = "1080p"
|
||||
elif data['videos'][0]['resolution'][0] >= 1200:
|
||||
video.resolution = "720p"
|
||||
elif data['videos'][0]['resolution'][0] >= 0:
|
||||
video.resolution = "480p"
|
||||
if 'codec' in data['videos'][0]:
|
||||
if not video.video_codec:
|
||||
video.video_codec = data['videos'][0]['codec']
|
||||
|
||||
if 'audios' not in data:
|
||||
logging.debug('BAZARR FFprobe was unable to find audio tracks in the file!')
|
||||
else:
|
||||
if 'codec' in data['audios'][0]:
|
||||
if not video.audio_codec:
|
||||
video.audio_codec = data['audios'][0]['codec'].upper()
|
||||
|
||||
|
||||
def upgrade_subtitles():
|
||||
|
@ -998,7 +1053,7 @@ def upgrade_subtitles():
|
|||
query_actions = [1, 2, 3]
|
||||
else:
|
||||
query_actions = [1, 3]
|
||||
|
||||
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
upgradable_episodes = TableHistory.select(
|
||||
TableHistory.video_path,
|
||||
|
@ -1039,7 +1094,7 @@ def upgrade_subtitles():
|
|||
for episode in upgradable_episodes_not_perfect:
|
||||
if os.path.exists(path_replace(episode['video_path'])) and int(episode['score']) < 357:
|
||||
episodes_to_upgrade.append(episode)
|
||||
|
||||
|
||||
if settings.general.getboolean('use_radarr'):
|
||||
upgradable_movies = TableHistoryMovie.select(
|
||||
TableHistoryMovie.video_path,
|
||||
|
@ -1055,7 +1110,7 @@ def upgrade_subtitles():
|
|||
).join_from(
|
||||
TableHistoryMovie, TableMovies, JOIN.LEFT_OUTER
|
||||
).where(
|
||||
(TableHistoryMovie.action.in_(query_actions)) &
|
||||
(TableHistoryMovie.action.in_(query_actions)) &
|
||||
(TableHistoryMovie.score.is_null(False))
|
||||
).group_by(
|
||||
TableHistoryMovie.video_path,
|
||||
|
|
116
bazarr/main.py
116
bazarr/main.py
|
@ -47,12 +47,13 @@ from get_episodes import *
|
|||
from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_subtitles, movies_scan_subtitles, \
|
||||
list_missing_subtitles, list_missing_subtitles_movies
|
||||
from get_subtitle import download_subtitle, series_download_subtitles, movies_download_subtitles, \
|
||||
manual_search, manual_download_subtitle
|
||||
manual_search, manual_download_subtitle, manual_upload_subtitle
|
||||
from utils import history_log, history_log_movie
|
||||
from scheduler import *
|
||||
from notifier import send_notifications, send_notifications_movie
|
||||
from config import settings, url_sonarr, url_radarr, url_radarr_short, url_sonarr_short, base_url
|
||||
from subliminal_patch.extensions import provider_registry as provider_manager
|
||||
from subliminal_patch.core import SUBTITLE_EXTENSIONS
|
||||
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
@ -232,7 +233,7 @@ def wizard():
|
|||
@custom_auth_basic(check_credentials)
|
||||
def save_wizard():
|
||||
authorize()
|
||||
|
||||
|
||||
settings_general_ip = request.forms.get('settings_general_ip')
|
||||
settings_general_port = request.forms.get('settings_general_port')
|
||||
settings_general_baseurl = request.forms.get('settings_general_baseurl')
|
||||
|
@ -441,7 +442,7 @@ def save_wizard():
|
|||
|
||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
||||
settings.write(handle)
|
||||
|
||||
|
||||
configured()
|
||||
redirect(base_url)
|
||||
|
||||
|
@ -747,7 +748,7 @@ def edit_serieseditor():
|
|||
lang = request.forms.getall('languages')
|
||||
hi = request.forms.get('hearing_impaired')
|
||||
forced = request.forms.get('forced')
|
||||
|
||||
|
||||
for serie in series:
|
||||
if str(lang) != "[]" and str(lang) != "['']":
|
||||
if str(lang) == "['None']":
|
||||
|
@ -936,7 +937,7 @@ def edit_movieseditor():
|
|||
lang = request.forms.getall('languages')
|
||||
hi = request.forms.get('hearing_impaired')
|
||||
forced = request.forms.get('forced')
|
||||
|
||||
|
||||
for movie in movies:
|
||||
if str(lang) != "[]" and str(lang) != "['']":
|
||||
if str(lang) == "['None']":
|
||||
|
@ -1012,7 +1013,7 @@ def edit_movie(no):
|
|||
).where(
|
||||
TableMovies.radarr_id % no
|
||||
).execute()
|
||||
|
||||
|
||||
list_missing_subtitles_movies(no)
|
||||
|
||||
redirect(ref)
|
||||
|
@ -1185,7 +1186,7 @@ def historyseries():
|
|||
fn.MAX(TableHistory.timestamp).alias('timestamp'),
|
||||
TableHistory.score
|
||||
).where(
|
||||
(TableHistory.action.in_(query_actions)) &
|
||||
(TableHistory.action.in_(query_actions)) &
|
||||
(TableHistory.score.is_null(False))
|
||||
).group_by(
|
||||
TableHistory.video_path,
|
||||
|
@ -1276,7 +1277,7 @@ def historymovies():
|
|||
fn.MAX(TableHistoryMovie.timestamp).alias('timestamp'),
|
||||
TableHistoryMovie.score
|
||||
).where(
|
||||
(TableHistoryMovie.action.in_(query_actions)) &
|
||||
(TableHistoryMovie.action.in_(query_actions)) &
|
||||
(TableHistoryMovie.score.is_null(False))
|
||||
).group_by(
|
||||
TableHistoryMovie.video_path,
|
||||
|
@ -1292,7 +1293,7 @@ def historymovies():
|
|||
else:
|
||||
if int(upgradable_movie['score']) < 120:
|
||||
upgradable_movies_not_perfect.append(tuple(upgradable_movie.values()))
|
||||
|
||||
|
||||
return template('historymovies', bazarr_version=bazarr_version, rows=data, row_count=row_count,
|
||||
page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size,
|
||||
current_port=settings.general.port, upgradable_movies=upgradable_movies_not_perfect)
|
||||
|
@ -1429,7 +1430,7 @@ def _settings():
|
|||
def save_settings():
|
||||
authorize()
|
||||
ref = request.environ['HTTP_REFERER']
|
||||
|
||||
|
||||
settings_general_ip = request.forms.get('settings_general_ip')
|
||||
settings_general_port = request.forms.get('settings_general_port')
|
||||
settings_general_baseurl = request.forms.get('settings_general_baseurl')
|
||||
|
@ -1476,11 +1477,6 @@ def save_settings():
|
|||
settings_general_scenename = 'False'
|
||||
else:
|
||||
settings_general_scenename = 'True'
|
||||
settings_general_mediainfo = request.forms.get('settings_general_mediainfo')
|
||||
if settings_general_mediainfo is None:
|
||||
settings_general_mediainfo = 'False'
|
||||
else:
|
||||
settings_general_mediainfo = 'True'
|
||||
settings_general_embedded = request.forms.get('settings_general_embedded')
|
||||
if settings_general_embedded is None:
|
||||
settings_general_embedded = 'False'
|
||||
|
@ -1563,7 +1559,6 @@ def save_settings():
|
|||
settings.general.single_language = text_type(settings_general_single_language)
|
||||
settings.general.minimum_score = text_type(settings_general_minimum_score)
|
||||
settings.general.use_scenename = text_type(settings_general_scenename)
|
||||
settings.general.use_mediainfo = text_type(settings_general_mediainfo)
|
||||
settings.general.use_postprocessing = text_type(settings_general_use_postprocessing)
|
||||
settings.general.postprocessing_cmd = text_type(settings_general_postprocessing_cmd)
|
||||
settings.general.use_sonarr = text_type(settings_general_use_sonarr)
|
||||
|
@ -2147,6 +2142,51 @@ def manual_get_subtitle():
|
|||
pass
|
||||
|
||||
|
||||
@route(base_url + 'manual_upload_subtitle', method='POST')
|
||||
@custom_auth_basic(check_credentials)
|
||||
def perform_manual_upload_subtitle():
|
||||
authorize()
|
||||
ref = request.environ['HTTP_REFERER']
|
||||
|
||||
episodePath = request.forms.get('episodePath')
|
||||
sceneName = request.forms.get('sceneName')
|
||||
language = request.forms.get('language')
|
||||
forced = True if request.forms.get('forced') == '1' else False
|
||||
upload = request.files.get('upload')
|
||||
sonarrSeriesId = request.forms.get('sonarrSeriesId')
|
||||
sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
|
||||
title = request.forms.get('title')
|
||||
|
||||
_, ext = os.path.splitext(upload.filename)
|
||||
|
||||
if ext not in SUBTITLE_EXTENSIONS:
|
||||
raise ValueError('A subtitle of an invalid format was uploaded.')
|
||||
|
||||
try:
|
||||
result = manual_upload_subtitle(path=episodePath,
|
||||
language=language,
|
||||
forced=forced,
|
||||
title=title,
|
||||
scene_name=sceneName,
|
||||
media_type='series',
|
||||
subtitle=upload)
|
||||
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
language_code = language + ":forced" if forced else language
|
||||
provider = "manual"
|
||||
score = 360
|
||||
history_log(4, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score)
|
||||
send_notifications(sonarrSeriesId, sonarrEpisodeId, message)
|
||||
store_subtitles(unicode(episodePath))
|
||||
list_missing_subtitles(sonarrSeriesId)
|
||||
|
||||
redirect(ref)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@route(base_url + 'get_subtitle_movie', method='POST')
|
||||
@custom_auth_basic(check_credentials)
|
||||
def get_subtitle_movie():
|
||||
|
@ -2239,6 +2279,50 @@ def manual_get_subtitle_movie():
|
|||
pass
|
||||
|
||||
|
||||
@route(base_url + 'manual_upload_subtitle_movie', method='POST')
|
||||
@custom_auth_basic(check_credentials)
|
||||
def perform_manual_upload_subtitle_movie():
|
||||
authorize()
|
||||
ref = request.environ['HTTP_REFERER']
|
||||
|
||||
moviePath = request.forms.get('moviePath')
|
||||
sceneName = request.forms.get('sceneName')
|
||||
language = request.forms.get('language')
|
||||
forced = True if request.forms.get('forced') == '1' else False
|
||||
upload = request.files.get('upload')
|
||||
radarrId = request.forms.get('radarrId')
|
||||
title = request.forms.get('title')
|
||||
|
||||
_, ext = os.path.splitext(upload.filename)
|
||||
|
||||
if ext not in SUBTITLE_EXTENSIONS:
|
||||
raise ValueError('A subtitle of an invalid format was uploaded.')
|
||||
|
||||
try:
|
||||
result = manual_upload_subtitle(path=moviePath,
|
||||
language=language,
|
||||
forced=forced,
|
||||
title=title,
|
||||
scene_name=sceneName,
|
||||
media_type='series',
|
||||
subtitle=upload)
|
||||
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
language_code = language + ":forced" if forced else language
|
||||
provider = "manual"
|
||||
score = 120
|
||||
history_log_movie(4, radarrId, message, path, language_code, provider, score)
|
||||
send_notifications_movie(radarrId, message)
|
||||
store_subtitles_movie(unicode(moviePath))
|
||||
list_missing_subtitles_movies(radarrId)
|
||||
|
||||
redirect(ref)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def configured():
|
||||
System.update({System.configured: 1}).execute()
|
||||
|
||||
|
|
|
@ -52,19 +52,13 @@ def get_binary(name):
|
|||
binaries_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'bin'))
|
||||
|
||||
exe = None
|
||||
if name != 'mediainfo':
|
||||
installed_exe = which(name)
|
||||
installed_exe = which(name)
|
||||
|
||||
if name != 'mediainfo' and installed_exe and os.path.isfile(installed_exe):
|
||||
if installed_exe and os.path.isfile(installed_exe):
|
||||
return installed_exe
|
||||
else:
|
||||
if platform.system() == "Windows": # Windows
|
||||
exe = os.path.abspath(os.path.join(binaries_dir, "Windows", "i386", name, "%s.exe" % name))
|
||||
if exe and not os.path.isfile(exe):
|
||||
if sys.maxsize > 2**32: # is 64bits Python
|
||||
exe = os.path.abspath(os.path.join(binaries_dir, "Windows", "x86_64", name, "%s.dll" % name))
|
||||
else: # is 32bits Python
|
||||
exe = os.path.abspath(os.path.join(binaries_dir, "Windows", "i386", name, "%s.dll" % name))
|
||||
|
||||
elif platform.system() == "Darwin": # MacOSX
|
||||
exe = os.path.abspath(os.path.join(binaries_dir, "MacOSX", "i386", name, name))
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
2
libs/pyprobe/__init__.py
Normal file
2
libs/pyprobe/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
from pyprobe import VideoFileParser
|
41
libs/pyprobe/baseparser.py
Normal file
41
libs/pyprobe/baseparser.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
|
||||
class BaseParser:
|
||||
@classmethod
|
||||
def parse(cls, data, rawMode, includeMissing):
|
||||
"""Core of the parser classes
|
||||
|
||||
Collects all methods prefixed with "value_" and builds a dict of
|
||||
their return values. Parser classes will inherit from this class.
|
||||
All methods that begin with "value_" in a parser class will be given
|
||||
the same `data` argument and are expected to pull their corresponding
|
||||
value from the collection.
|
||||
|
||||
These methods return a tuple - their raw value and formatted value.
|
||||
The raw value is a string or tuple of string and the formatted value
|
||||
be of type string, int, float, or tuple.
|
||||
|
||||
If no data is found in a method, the raw value is expected to be None,
|
||||
and for the formatted value, strings will be "null", ints will be 0,
|
||||
floats will be 0.0.
|
||||
|
||||
Args:
|
||||
data (dict): Raw video data
|
||||
rawMode (bool): Returns raw values instead of formatted values
|
||||
includeMissing (bool): If value is missing, return "empty" value
|
||||
|
||||
Returns:
|
||||
dict<str, dict<str, var>>: Parsed data from class methods, may not have every value.
|
||||
|
||||
"""
|
||||
parsers = [getattr(cls, p) for p in dir(cls) if p.startswith("value_")]
|
||||
info = {}
|
||||
for parser in parsers:
|
||||
parsed_raw, parsed_formatted = parser(data)
|
||||
if parsed_raw == None and not includeMissing:
|
||||
continue
|
||||
name = parser.__name__[6:]
|
||||
if rawMode:
|
||||
info[name] = parsed_raw
|
||||
else:
|
||||
info[name] = parsed_formatted
|
||||
return info
|
215
libs/pyprobe/ffprobeparsers.py
Normal file
215
libs/pyprobe/ffprobeparsers.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
from os import path
|
||||
|
||||
from baseparser import BaseParser
|
||||
|
||||
|
||||
class StreamParser(BaseParser):
|
||||
@staticmethod
|
||||
def value_codec(data):
|
||||
"""Returns a string"""
|
||||
info = data.get("codec_name", None)
|
||||
return info, (info or "null")
|
||||
|
||||
@staticmethod
|
||||
def value_format(data):
|
||||
"""Returns a string"""
|
||||
info = data.get("format_name", None)
|
||||
return info, (info or "null")
|
||||
|
||||
@staticmethod
|
||||
def value_bit_rate(data):
|
||||
"""Returns an int"""
|
||||
info = data.get("bit_rate", None)
|
||||
try:
|
||||
return info, int(float(info))
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
||||
|
||||
|
||||
class VideoStreamParser(BaseParser):
|
||||
@staticmethod
|
||||
def value_codec(data):
|
||||
return StreamParser.value_codec(data)
|
||||
|
||||
@staticmethod
|
||||
def value_format(data):
|
||||
return StreamParser.value_format(data)
|
||||
|
||||
@staticmethod
|
||||
def value_bit_rate(data):
|
||||
return StreamParser.value_bit_rate(data)
|
||||
|
||||
@staticmethod
|
||||
def value_resolution(data):
|
||||
"""Returns a tuple (width, height)"""
|
||||
width = data.get("width", None)
|
||||
height = data.get("height", None)
|
||||
if width is None and height is None:
|
||||
return None, (0, 0)
|
||||
try:
|
||||
return (width, height), (int(float(width)), int(float(height)))
|
||||
except (ValueError, TypeError):
|
||||
return (width, height), (0, 0)
|
||||
|
||||
@staticmethod
|
||||
def average_framerate(data):
|
||||
"""Returns an int"""
|
||||
frames = data.get("nb_frames", None)
|
||||
duration = data.get("duration", None)
|
||||
try:
|
||||
return float(frames) / float(duration)
|
||||
except (ValueError, TypeError, ZeroDivisionError):
|
||||
return 0.0
|
||||
|
||||
@classmethod
|
||||
def value_framerate(cls, data):
|
||||
"""Returns a float"""
|
||||
input_str = data.get("avg_frame_rate", None)
|
||||
try:
|
||||
num, den = input_str.split("/")
|
||||
return input_str, round(float(num) / float(den), 3)
|
||||
except (ValueError, ZeroDivisionError, AttributeError):
|
||||
info = cls.average_framerate(data)
|
||||
return input_str, info
|
||||
|
||||
@staticmethod
|
||||
def value_aspect_ratio(data):
|
||||
"""Returns a string"""
|
||||
info = data.get("display_aspect_ratio", None)
|
||||
return info, (info or "null")
|
||||
|
||||
@staticmethod
|
||||
def value_pixel_format(data):
|
||||
"""Returns a string"""
|
||||
info = data.get("pix_fmt", None)
|
||||
return info, (info or "null")
|
||||
|
||||
|
||||
class AudioStreamParser(StreamParser):
|
||||
@staticmethod
|
||||
def value_sample_rate(data):
|
||||
"""Returns an int - audio sample rate in Hz"""
|
||||
info = data.get("sample_rate", None)
|
||||
try:
|
||||
return info, int(float(info))
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
||||
|
||||
@staticmethod
|
||||
def value_channel_count(data):
|
||||
"""Returns an int"""
|
||||
info = data.get("channels", None)
|
||||
try:
|
||||
return info, int(float(info))
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
||||
|
||||
@staticmethod
|
||||
def value_channel_layout(data):
|
||||
"""Returns a string"""
|
||||
info = data.get("channel_layout", None)
|
||||
return info, (info or "null")
|
||||
|
||||
|
||||
class SubtitleStreamParser(BaseParser):
|
||||
@staticmethod
|
||||
def value_codec(data):
|
||||
return StreamParser.value_codec(data)
|
||||
|
||||
@staticmethod
|
||||
def value_language(data):
|
||||
"""Returns a string """
|
||||
tags = data.get("tags", None)
|
||||
if tags:
|
||||
info = tags.get("language", None)
|
||||
return info, (info or "null")
|
||||
return None, "null"
|
||||
|
||||
@staticmethod
|
||||
def value_forced(data):
|
||||
"""Returns a bool """
|
||||
disposition = data.get("disposition", None)
|
||||
if disposition:
|
||||
info = disposition.get("forced", None)
|
||||
return bool(info), (bool(info) or False)
|
||||
return None, "null"
|
||||
|
||||
|
||||
class ChapterParser(BaseParser):
|
||||
@staticmethod
|
||||
def value_start(data):
|
||||
"""Returns an int"""
|
||||
info = data.get("start_time", None)
|
||||
try:
|
||||
return info, float(data.get("start_time"))
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
||||
|
||||
@classmethod
|
||||
def value_end(cls, data):
|
||||
"""Returns a float"""
|
||||
info = data.get("end_time", None)
|
||||
try:
|
||||
return info, float(info)
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
||||
|
||||
@staticmethod
|
||||
def value_title(data):
|
||||
"""Returns a string"""
|
||||
info = data.get("tags", {}).get("title", None)
|
||||
return info, (info or "null")
|
||||
|
||||
@staticmethod
|
||||
def fillEmptyTitles(chapters):
|
||||
"""Add text in place of empty titles
|
||||
If a chapter doesn't have a title, this will add a basic
|
||||
string in the form "Chapter `index+1`"
|
||||
|
||||
Args:
|
||||
chapters(list<dict>): The list of parsed chapters
|
||||
|
||||
"""
|
||||
index = 0
|
||||
for chapter in chapters:
|
||||
if not chapter["title"]:
|
||||
chapter["title"] = "Chapter " + str(index)
|
||||
index += 1
|
||||
|
||||
|
||||
class RootParser(BaseParser):
|
||||
@staticmethod
|
||||
def value_duration(data):
|
||||
"""Returns an int"""
|
||||
info = data.get("duration", None)
|
||||
try:
|
||||
return info, float(info)
|
||||
except (ValueError, TypeError):
|
||||
return info, 0.0
|
||||
|
||||
@staticmethod
|
||||
def value_size(data):
|
||||
"""Returns an int"""
|
||||
info = data.get("size", None)
|
||||
if info is None:
|
||||
file_path = data.get("filename", "")
|
||||
if path.isfile(file_path):
|
||||
info = str(path.getsize(file_path))
|
||||
try:
|
||||
return info, int(float(info))
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
||||
|
||||
@classmethod
|
||||
def value_bit_rate(cls, data):
|
||||
"""Returns an int"""
|
||||
info = data.get("bit_rate", None)
|
||||
if info is None:
|
||||
_, size = cls.value_size(data)
|
||||
_, duration = cls.value_duration(data)
|
||||
if size and duration:
|
||||
info = size / (duration / 60 * 0.0075) / 1000
|
||||
try:
|
||||
return info, int(float(info))
|
||||
except (ValueError, TypeError):
|
||||
return info, 0
|
213
libs/pyprobe/pyprobe.py
Normal file
213
libs/pyprobe/pyprobe.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
import json
|
||||
import subprocess
|
||||
from os import path
|
||||
from sys import getfilesystemencoding
|
||||
|
||||
import ffprobeparsers
|
||||
|
||||
|
||||
class VideoFileParser:
|
||||
def __init__(
|
||||
self,
|
||||
ffprobe="ffprobe",
|
||||
includeMissing=True,
|
||||
rawMode=False,
|
||||
):
|
||||
self._ffprobe = ffprobe
|
||||
self._includeMissing = includeMissing
|
||||
self._rawMode = rawMode
|
||||
|
||||
########################################
|
||||
# Main Method
|
||||
|
||||
def parseFfprobe(self, inputFile):
|
||||
"""Takes an input file and returns the parsed data using ffprobe.
|
||||
|
||||
Args:
|
||||
inputFile (str): Video file path
|
||||
|
||||
Returns:
|
||||
dict<str, dict<str, var>>: Parsed video info
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: The input video file or input executable was not found
|
||||
IOError: Execution failed
|
||||
|
||||
"""
|
||||
if not path.isfile(inputFile):
|
||||
raise FileNotFoundError(inputFile + " not found")
|
||||
self._checkExecutable(self._ffprobe)
|
||||
fdict = self._executeFfprobe(inputFile)
|
||||
return self._parseFfprobe(fdict, inputFile)
|
||||
|
||||
########################################
|
||||
# ffprobe Parsing
|
||||
|
||||
def _executeFfprobe(self, inputFile):
|
||||
"""Executes ffprobe program on input file to get raw info
|
||||
|
||||
fdict = dict<str, fdict> or dict<str, str>
|
||||
|
||||
Args:
|
||||
inputFile (str): Video file path
|
||||
|
||||
Returns:
|
||||
fdict: Parsed data
|
||||
|
||||
"""
|
||||
commandArgs = [
|
||||
"-v",
|
||||
"quiet",
|
||||
"-hide_banner",
|
||||
"-show_error",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
"-show_programs",
|
||||
"-show_chapters",
|
||||
"-show_private_data",
|
||||
"-print_format",
|
||||
"json",
|
||||
]
|
||||
outputJson = self._executeParser(self._ffprobe, commandArgs, inputFile)
|
||||
|
||||
try:
|
||||
data = json.loads(outputJson)
|
||||
except json.JSONDecodeError:
|
||||
raise IOError("Could not decode ffprobe output for file " + inputFile)
|
||||
return data
|
||||
|
||||
def _parseFfprobe(self, fOutput, inputFile):
|
||||
"""Parse all data from fOutput to organized format
|
||||
|
||||
fdict = dict<str, fdict> or dict<str, str>
|
||||
|
||||
Args:
|
||||
fOutput (fdict): Stream data from ffprobe
|
||||
inputFile (str): Video file path
|
||||
|
||||
Returns:
|
||||
dict<str, dict<str, str>>: Parsed video data
|
||||
|
||||
"""
|
||||
videoInfo = {}
|
||||
videoInfo["path"] = path.abspath(inputFile)
|
||||
videoInfo.update(
|
||||
ffprobeparsers.RootParser.parse(
|
||||
fOutput["format"], self._rawMode, self._includeMissing
|
||||
)
|
||||
)
|
||||
videoInfo.update(self._parseFfprobeStreams(fOutput))
|
||||
videoInfo.update(self._parseFfprobeChapters(fOutput))
|
||||
if not self._rawMode:
|
||||
ffprobeparsers.ChapterParser.fillEmptyTitles(videoInfo["chapters"])
|
||||
return videoInfo
|
||||
|
||||
def _parseFfprobeStreams(self, fOutput):
|
||||
"""Parses video, audio, and subtitle streams
|
||||
|
||||
fdict = dict<str, fdict> or dict<str, str>
|
||||
|
||||
Args:
|
||||
streams_data (fdict): Stream data from ffprobe
|
||||
|
||||
Returns:
|
||||
dict<str, dict<str, var>>: Parsed streams - video, audio, and subtitle
|
||||
|
||||
"""
|
||||
parsedInfo = {"videos": [], "audios": [], "subtitles": []}
|
||||
for stream in fOutput["streams"]:
|
||||
streamType = stream["codec_type"]
|
||||
data = None
|
||||
if streamType == "video":
|
||||
data = ffprobeparsers.VideoStreamParser.parse(
|
||||
stream, self._rawMode, self._includeMissing
|
||||
)
|
||||
parsedInfo["videos"].append(data)
|
||||
elif streamType == "audio":
|
||||
data = ffprobeparsers.AudioStreamParser.parse(
|
||||
stream, self._rawMode, self._includeMissing
|
||||
)
|
||||
parsedInfo["audios"].append(data)
|
||||
elif streamType == "subtitle":
|
||||
data = ffprobeparsers.SubtitleStreamParser.parse(
|
||||
stream, self._rawMode, self._includeMissing
|
||||
)
|
||||
parsedInfo["subtitles"].append(data)
|
||||
return parsedInfo
|
||||
|
||||
def _parseFfprobeChapters(self, fOutput):
|
||||
"""Parses chapters
|
||||
|
||||
fdict = dict<str, fdict> or dict<str, str>
|
||||
|
||||
Args:
|
||||
chapters_data (fdict): Stream data from ffprobe
|
||||
|
||||
Returns:
|
||||
dict<str, dict<str, var>>: Parsed chapters
|
||||
|
||||
"""
|
||||
parsedInfo = {"chapters": []}
|
||||
if fOutput["chapters"] is None:
|
||||
return parsedInfo
|
||||
for chapter in fOutput["chapters"]:
|
||||
parsedInfo["chapters"].append(
|
||||
ffprobeparsers.ChapterParser.parse(
|
||||
chapter, self._rawMode, self._includeMissing
|
||||
)
|
||||
)
|
||||
return parsedInfo
|
||||
|
||||
########################################
|
||||
# Misc Methods
|
||||
|
||||
@staticmethod
|
||||
def _executeParser(parser, commandArgs, inputFile):
|
||||
"""Executes parser on the input file
|
||||
|
||||
Args:
|
||||
parser (str): Executable location or command
|
||||
commandArgs (list of strings): Extra command arguments
|
||||
inputFile (str): the input file location
|
||||
|
||||
Raises:
|
||||
IOError: ffprobe execution failed
|
||||
|
||||
"""
|
||||
command = [parser] + commandArgs + [inputFile.encode(getfilesystemencoding())]
|
||||
try:
|
||||
completedProcess = subprocess.check_output(
|
||||
command, stderr=subprocess.STDOUT
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise IOError(
|
||||
"Error occurred during execution - " + e.output
|
||||
)
|
||||
return completedProcess
|
||||
|
||||
@staticmethod
|
||||
def _checkExecutable(executable):
|
||||
"""Checks if target is executable
|
||||
|
||||
Args:
|
||||
executable (str): Executable location, can be file or command
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: Executable was not found
|
||||
|
||||
"""
|
||||
try:
|
||||
subprocess.check_output(
|
||||
[executable, "--help"],
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
except OSError:
|
||||
raise FileNotFoundError(executable + " not found")
|
||||
|
||||
|
||||
class FileNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IOError(Exception):
|
||||
pass
|
|
@ -16,6 +16,7 @@ langdetect=1.0.7
|
|||
peewee=3.9.6
|
||||
py-pretty=1
|
||||
pycountry=18.2.23
|
||||
pyprobe=0.1.2 <-- modified version: do not update!!!
|
||||
pysrt=1.1.1
|
||||
pytz=2018.4
|
||||
rarfile=3.0
|
||||
|
|
|
@ -199,6 +199,7 @@
|
|||
<th class="collapsing">Existing<br>subtitles</th>
|
||||
<th class="collapsing">Missing<br>subtitles</th>
|
||||
<th class="collapsing">Manual<br>search</th>
|
||||
<th class="collapsing">Manual<br>upload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -291,6 +292,11 @@
|
|||
<a data-episodePath="{{episode['path']}}" data-scenename="{{episode['scene_name']}}" data-language="{{subs_languages_list}}" data-hi="{{details.hearing_impaired}}" data-forced="{{details.forced}}" data-series_title="{{details.title}}" data-season="{{episode['season']}}" data-episode="{{episode['episode']}}" data-episode_title="{{episode['title']}}" data-sonarrSeriesId="{{episode['sonarr_series_id']}}" data-sonarrEpisodeId="{{episode['sonarr_episode_id']}}" class="manual_search ui tiny label"><i class="ui user icon" style="margin-right:0px" ></i></a>
|
||||
%end
|
||||
</td>
|
||||
<td>
|
||||
%if subs_languages is not None:
|
||||
<a data-episodePath="{{episode[1]}}" data-scenename="{{episode[8]}}" data-language="{{subs_languages_list}}" data-hi="{{details[4]}}" data-series_title="{{details[0]}}" data-season="{{episode[2]}}" data-episode="{{episode[3]}}" data-episode_title="{{episode[0]}}" data-sonarrSeriesId="{{episode[5]}}" data-sonarrEpisodeId="{{episode[7]}}" class="manual_upload ui tiny label"><i class="ui cloud upload icon" style="margin-right:0px" ></i></a>
|
||||
%end
|
||||
</td>
|
||||
</tr>
|
||||
%end
|
||||
</tbody>
|
||||
|
@ -397,6 +403,59 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload_dialog ui small modal">
|
||||
<i class="close icon"></i>
|
||||
<div class="header">
|
||||
<span id="series_title_span_u"></span> - <span id="season_u"></span>x<span id="episode_u"></span> - <span id="episode_title_u"></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<form class="ui form" name="upload_form" id="upload_form" action="{{base_url}}manual_upload_subtitle" method="post" enctype="multipart/form-data">
|
||||
<div class="ui grid">
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned three wide column">
|
||||
<label>Language</label>
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<select class="ui search dropdown" id="language" name="language">
|
||||
%for language in subs_languages_list:
|
||||
<option value="{{language}}">{{language_from_alpha2(language)}}</option>
|
||||
%end
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned three wide column">
|
||||
<label>Forced</label>
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<div class="ui toggle checkbox">
|
||||
<input name="forced" type="checkbox" value="1">
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned three wide column">
|
||||
<label>File</label>
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<input type="file" name="upload">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="upload_episodePath" name="episodePath" value="" />
|
||||
<input type="hidden" id="upload_sceneName" name="sceneName" value="" />
|
||||
<input type="hidden" id="upload_sonarrSeriesId" name="sonarrSeriesId" value="" />
|
||||
<input type="hidden" id="upload_sonarrEpisodeId" name="sonarrEpisodeId" value="" />
|
||||
<input type="hidden" id="upload_title" name="title" value="" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel button" >Cancel</button>
|
||||
<button type="submit" name="save" value="save" form="upload_form" class="ui blue approve button">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
% include('footer.tpl')
|
||||
</body>
|
||||
</html>
|
||||
|
@ -469,15 +528,10 @@
|
|||
});
|
||||
});
|
||||
|
||||
$('a:not(.manual_search), .menu .item, button:not(#config, .cancel, #search_missing_subtitles)').on('click', function(){
|
||||
$('a:not(.manual_search, .manual_upload), .menu .item, button:not(#config, .cancel, #search_missing_subtitles)').on('click', function(){
|
||||
$('#loader').addClass('active');
|
||||
});
|
||||
|
||||
$('.modal')
|
||||
.modal({
|
||||
autofocus: false
|
||||
});
|
||||
|
||||
$('#config').on('click', function(){
|
||||
$('#series_form').attr('action', '{{base_url}}edit_series/{{no}}');
|
||||
|
||||
|
@ -499,7 +553,12 @@
|
|||
$("#series_hearing-impaired_div").checkbox('uncheck');
|
||||
}
|
||||
|
||||
$('.config_dialog').modal('show');
|
||||
$('.config_dialog')
|
||||
.modal({
|
||||
centered: false,
|
||||
autofocus: false
|
||||
})
|
||||
.modal('show');
|
||||
});
|
||||
|
||||
$('.manual_search').on('click', function(){
|
||||
|
@ -604,7 +663,41 @@
|
|||
|
||||
$('.search_dialog')
|
||||
.modal({
|
||||
centered: false
|
||||
centered: false,
|
||||
autofocus: false
|
||||
})
|
||||
.modal('show');
|
||||
});
|
||||
|
||||
$('.manual_upload').on('click', function(){
|
||||
$("#series_title_span_u").html($(this).data("series_title"));
|
||||
$("#season_u").html($(this).data("season"));
|
||||
$("#episode_u").html($(this).data("episode"));
|
||||
$("#episode_title_u").html($(this).data("episode_title"));
|
||||
|
||||
episodePath = $(this).attr("data-episodePath");
|
||||
sceneName = $(this).attr("data-sceneName");
|
||||
language = $(this).attr("data-language");
|
||||
hi = $(this).attr("data-hi");
|
||||
sonarrSeriesId = $(this).attr("data-sonarrSeriesId");
|
||||
sonarrEpisodeId = $(this).attr("data-sonarrEpisodeId");
|
||||
var languages = Array.from({{!subs_languages_list}});
|
||||
var is_pb = languages.includes('pb');
|
||||
var is_pt = languages.includes('pt');
|
||||
var title = "{{!details[0].replace("'", "\'")}}";
|
||||
|
||||
$('#language').dropdown();
|
||||
|
||||
$('#upload_episodePath').val(episodePath);
|
||||
$('#upload_sceneName').val(sceneName);
|
||||
$('#upload_sonarrSeriesId').val(sonarrSeriesId);
|
||||
$('#upload_sonarrEpisodeId').val(sonarrEpisodeId);
|
||||
$('#upload_title').val(title);
|
||||
|
||||
$('.upload_dialog')
|
||||
.modal({
|
||||
centered: false,
|
||||
autofocus: false
|
||||
})
|
||||
.modal('show');
|
||||
});
|
||||
|
|
|
@ -74,6 +74,10 @@
|
|||
<div class="ui inverted basic compact icon" data-tooltip="Subtitles file has been upgraded." data-inverted="" data-position="top left">
|
||||
<i class="ui recycle icon"></i>
|
||||
</div>
|
||||
%elif row[0] == 4:
|
||||
<div class="ui inverted basic compact icon" data-tooltip="Subtitles file has been manually uploaded." data-inverted="" data-position="top left">
|
||||
<i class="ui cloud upload icon"></i>
|
||||
</div>
|
||||
%end
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -76,6 +76,10 @@
|
|||
<div class="ui inverted basic compact icon" data-tooltip="Subtitles file has been upgraded." data-inverted="" data-position="top left">
|
||||
<i class="ui recycle icon"></i>
|
||||
</div>
|
||||
%elif row[0] == 4:
|
||||
<div class="ui inverted basic compact icon" data-tooltip="Subtitles file has been manually uploaded." data-inverted="" data-position="top left">
|
||||
<i class="ui cloud upload icon"></i>
|
||||
</div>
|
||||
%end
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
%>
|
||||
%if subs_languages is not None:
|
||||
<button class="manual_search ui button" data-tooltip="Manually search for subtitles" data-inverted="" data-moviePath="{{details.path}}" data-scenename="{{details.scene_name}}" data-language="{{subs_languages_list}}" data-hi="{{details.hearing_impaired}}" data-forced="{{details.forced}}" data-movie_title="{{details.title}}" data-radarrId="{{details.radarr_id}}"><i class="ui inverted large compact user icon"></i></button>
|
||||
<button class="manual_upload ui button" data-tooltip="Manually upload subtitles" data-inverted="" data-moviePath="{{details.path}}" data-scenename="{{details.scene_name}}" data-language="{{subs_languages_list}}" data-hi="{{details.hearing_impaired}}" data-movie_title="{{details.forced}}" data-radarrId="{{details.title}}"><i class="ui inverted large compact cloud upload icon"></i></button>
|
||||
%end
|
||||
<button id="config" class="ui button" data-tooltip="Edit movie" data-inverted="" data-tmdbid="{{details.tmdb_id}}" data-title="{{details.title}}" data-poster="{{details.poster}}" data-audio="{{details.audio_language}}" data-languages="{{!subs_languages_list}}" data-hearing-impaired="{{details.hearing_impaired}}" data-forced="{{details.forced}}"><i class="ui inverted large compact configure icon"></i></button>
|
||||
</div>
|
||||
|
@ -354,6 +355,58 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload_dialog ui small modal">
|
||||
<i class="close icon"></i>
|
||||
<div class="header">
|
||||
<span id="movie_title_upload_span"></span>
|
||||
</div>
|
||||
<div class="scrolling content">
|
||||
<form class="ui form" name="upload_form" id="upload_form" action="{{base_url}}manual_upload_subtitle_movie" method="post" enctype="multipart/form-data">
|
||||
<div class="ui grid">
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned three wide column">
|
||||
<label>Language</label>
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<select class="ui search dropdown" id="language" name="language">
|
||||
%for language in subs_languages_list:
|
||||
<option value="{{language}}">{{language_from_alpha2(language)}}</option>
|
||||
%end
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned three wide column">
|
||||
<label>Forced</label>
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<div class="ui toggle checkbox">
|
||||
<input name="forced" type="checkbox" value="1">
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned three wide column">
|
||||
<label>File</label>
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<input type="file" name="upload">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="upload_moviePath" name="moviePath" value="" />
|
||||
<input type="hidden" id="upload_sceneName" name="sceneName" value="" />
|
||||
<input type="hidden" id="upload_radarrId" name="radarrId" value="" />
|
||||
<input type="hidden" id="upload_title" name="title" value="" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel button" >Cancel</button>
|
||||
<button type="submit" name="save" value="save" form="upload_form" class="ui blue approve button">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
% include('footer.tpl')
|
||||
</body>
|
||||
</html>
|
||||
|
@ -425,15 +478,10 @@
|
|||
});
|
||||
});
|
||||
|
||||
$('a, .menu .item, button:not(#config, .cancel, .manual_search, #search_missing_subtitles_movie)').on('click', function(){
|
||||
$('a, .menu .item, button:not(#config, .cancel, .manual_search, .manual_upload, #search_missing_subtitles_movie)').on('click', function(){
|
||||
$('#loader').addClass('active');
|
||||
});
|
||||
|
||||
$('.modal')
|
||||
.modal({
|
||||
autofocus: false
|
||||
});
|
||||
|
||||
$('#config').on('click', function(){
|
||||
$('#movie_form').attr('action', '{{base_url}}edit_movie/{{no}}');
|
||||
|
||||
|
@ -455,7 +503,12 @@
|
|||
$("#movie_hearing-impaired_div").checkbox('uncheck');
|
||||
}
|
||||
|
||||
$('.config_dialog').modal('show');
|
||||
$('.config_dialog')
|
||||
.modal({
|
||||
centered: false,
|
||||
autofocus: false
|
||||
})
|
||||
.modal('show');
|
||||
});
|
||||
|
||||
$('.manual_search').on('click', function(){
|
||||
|
@ -557,7 +610,33 @@
|
|||
|
||||
$('.search_dialog')
|
||||
.modal({
|
||||
centered: false
|
||||
centered: false,
|
||||
autofocus: false
|
||||
})
|
||||
.modal('show')
|
||||
;
|
||||
});
|
||||
|
||||
$('.manual_upload').on('click', function() {
|
||||
$("#movie_title_upload_span").html($(this).data("movie_title"));
|
||||
|
||||
moviePath = $(this).attr("data-moviePath");
|
||||
sceneName = $(this).attr("data-sceneName");
|
||||
language = $(this).attr("data-language");
|
||||
radarrId = $(this).attr("data-radarrId");
|
||||
var title = "{{!details[0].replace("'", "\'")}}";
|
||||
|
||||
$('#language').dropdown();
|
||||
|
||||
$('#upload_moviePath').val(moviePath);
|
||||
$('#upload_sceneName').val(sceneName);
|
||||
$('#upload_radarrId').val(radarrId);
|
||||
$('#upload_title').val(title);
|
||||
|
||||
$('.upload_dialog')
|
||||
.modal({
|
||||
centered: false,
|
||||
autofocus: false
|
||||
})
|
||||
.modal('show')
|
||||
;
|
||||
|
|
|
@ -20,33 +20,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned four wide column">
|
||||
<label>Use MediaInfo</label>
|
||||
</div>
|
||||
<div class="one wide column">
|
||||
% import platform
|
||||
<div id="settings_mediainfo" class="ui toggle checkbox{{' disabled' if platform.system() == 'Linux' else ''}}" data-mediainfo={{settings.general.getboolean('use_mediainfo')}}>
|
||||
<input name="settings_general_mediainfo" type="checkbox">
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapsed column">
|
||||
<div class="collapsed center aligned column">
|
||||
<div class="ui basic icon" data-tooltip="Use MediaInfo to extract video and audio stream properties." data-inverted="">
|
||||
<i class="help circle large icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapsed column">
|
||||
<div class="collapsed center aligned column">
|
||||
<div class="ui basic icon" data-tooltip="This settings is only available on Windows and MacOS." data-inverted="">
|
||||
<i class="yellow warning sign icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned four wide column">
|
||||
<label>Minimum score for episodes</label>
|
||||
|
@ -579,12 +552,6 @@
|
|||
$("#settings_scenename").checkbox('uncheck');
|
||||
}
|
||||
|
||||
if ($('#settings_mediainfo').data("mediainfo") === "True") {
|
||||
$("#settings_mediainfo").checkbox('check');
|
||||
} else {
|
||||
$("#settings_mediainfo").checkbox('uncheck');
|
||||
}
|
||||
|
||||
if ($('#settings_upgrade_subs').data("upgrade") === "True") {
|
||||
$("#settings_upgrade_subs").checkbox('check');
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue