Merge branch 'development'

# Conflicts:
#	bazarr/get_providers.py
#	libs/subliminal_patch/providers/titrari.py
This commit is contained in:
Louis Vézina 2020-01-31 20:30:39 -05:00
commit f81f7ed27c
26 changed files with 1298 additions and 351 deletions

View File

@ -45,8 +45,10 @@ If you need something that is not already part of Bazarr, feel free to create a
* Argenteam * Argenteam
* Assrt * Assrt
* BetaSeries * BetaSeries
* BSPlayer
* GreekSubtitles * GreekSubtitles
* Hosszupuska * Hosszupuska
* LegendasDivx
* LegendasTV * LegendasTV
* Napiprojekt * Napiprojekt
* Napisy24 * Napisy24

115
bazarr.py
View File

@ -11,6 +11,7 @@ import os
import sys import sys
import platform import platform
import re import re
import signal
from bazarr.get_args import args from bazarr.get_args import args
@ -39,15 +40,97 @@ check_python_version()
dir_name = os.path.dirname(__file__) dir_name = os.path.dirname(__file__)
def start_bazarr(): class ProcessRegistry:
def register(self, process):
pass
def unregister(self, process):
pass
class DaemonStatus(ProcessRegistry):
def __init__(self):
self.__should_stop = False
self.__processes = set()
def register(self, process):
self.__processes.add(process)
def unregister(self, process):
self.__processes.remove(process)
'''
Waits all the provided processes for the specified amount of time in seconds.
'''
@staticmethod
def __wait_for_processes(processes, timeout):
reference_ts = time.time()
elapsed = 0
remaining_processes = list(processes)
while elapsed < timeout and len(remaining_processes) > 0:
remaining_time = timeout - elapsed
for ep in list(remaining_processes):
if ep.poll() is not None:
remaining_processes.remove(ep)
else:
if remaining_time > 0:
if PY3:
try:
ep.wait(remaining_time)
remaining_processes.remove(ep)
except sp.TimeoutExpired:
pass
else:
'''
In python 2 there is no such thing as some mechanism to wait with a timeout.
'''
time.sleep(1)
elapsed = time.time() - reference_ts
remaining_time = timeout - elapsed
return remaining_processes
'''
Sends to every single of the specified processes the given signal and (if live_processes is not None) append to it processes which are still alive.
'''
@staticmethod
def __send_signal(processes, signal_no, live_processes=None):
for ep in processes:
if ep.poll() is None:
if live_processes is not None:
live_processes.append(ep)
try:
ep.send_signal(signal_no)
except Exception as e:
print('Failed sending signal %s to process %s because of an unexpected error: %s' % (signal_no, ep.pid, e))
return live_processes
'''
Flags this instance as should stop and terminates as smoothly as possible children processes.
'''
def stop(self):
self.__should_stop = True
live_processes = DaemonStatus.__send_signal(self.__processes, signal.SIGINT, list())
live_processes = DaemonStatus.__wait_for_processes(live_processes, 120)
DaemonStatus.__send_signal(live_processes, signal.SIGTERM)
def should_stop(self):
return self.__should_stop
def start_bazarr(process_registry=ProcessRegistry()):
script = [sys.executable, "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:] script = [sys.executable, "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:]
ep = sp.Popen(script, stdout=sp.PIPE, stderr=sp.STDOUT, stdin=sp.PIPE) ep = sp.Popen(script, stdout=sp.PIPE, stderr=sp.STDOUT, stdin=sp.PIPE)
process_registry.register(ep)
print("Bazarr starting...") print("Bazarr starting...")
try: try:
while True: while True:
line = ep.stdout.readline() line = ep.stdout.readline()
if line == '' or not line: if line == '' or not line:
# Process ended so let's unregister it
process_registry.unregister(ep)
break break
if PY3: if PY3:
sys.stdout.buffer.write(line) sys.stdout.buffer.write(line)
@ -73,7 +156,7 @@ if __name__ == '__main__':
pass pass
def daemon(): def daemon(bazarr_runner = lambda: start_bazarr()):
if os.path.exists(stopfile): if os.path.exists(stopfile):
try: try:
os.remove(stopfile) os.remove(stopfile)
@ -89,12 +172,30 @@ if __name__ == '__main__':
except: except:
print('Unable to delete restart file.') print('Unable to delete restart file.')
else: else:
start_bazarr() bazarr_runner()
start_bazarr() bazarr_runner = lambda: start_bazarr()
# Keep the script running forever. should_stop = lambda: False
while True:
daemon() if PY3:
daemonStatus = DaemonStatus()
def shutdown():
# indicates that everything should stop
daemonStatus.stop()
# emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break")
os.kill(os.getpid(), signal.SIGINT)
signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown())
should_stop = lambda: daemonStatus.should_stop()
bazarr_runner = lambda: start_bazarr(daemonStatus)
bazarr_runner()
# Keep the script running forever until stop is requested through term or keyboard interrupt
while not should_stop():
daemon(bazarr_runner)
time.sleep(1) time.sleep(1)

View File

@ -102,6 +102,10 @@ defaults = {
'password': '', 'password': '',
'random_agents': 'True' 'random_agents': 'True'
}, },
'legendasdivx': {
'username': '',
'password': ''
},
'legendastv': { 'legendastv': {
'username': '', 'username': '',
'password': '' 'password': ''

View File

@ -38,7 +38,7 @@ PROVIDER_THROTTLE_MAP = {
} }
} }
PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendastv", "napiprojekt", "shooter", "hosszupuska", PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendasdivx", "legendastv", "napiprojekt", "shooter", "hosszupuska",
"supersubtitles", "titlovi", "argenteam", "assrt", "subscene"] "supersubtitles", "titlovi", "argenteam", "assrt", "subscene"]
throttle_count = {} throttle_count = {}
@ -114,6 +114,9 @@ def get_providers_auth():
'password': settings.subscene.password, 'password': settings.subscene.password,
'only_foreign': False, # fixme 'only_foreign': False, # fixme
}, },
'legendasdivx': {'username': settings.legendasdivx.username,
'password': settings.legendasdivx.password,
},
'legendastv': {'username': settings.legendastv.username, 'legendastv': {'username': settings.legendastv.username,
'password': settings.legendastv.password, 'password': settings.legendastv.password,
}, },

View File

@ -6,195 +6,193 @@ import os
import requests import requests
import logging import logging
from queueconfig import notifications from queueconfig import notifications
from collections import OrderedDict
import datetime
from get_args import args
from config import settings, url_sonarr from config import settings, url_sonarr
from list_subtitles import list_missing_subtitles from list_subtitles import list_missing_subtitles
from database import database, dict_converter from database import database, dict_converter
from utils import get_sonarr_version from utils import get_sonarr_version
import six
from helper import path_replace from helper import path_replace
def update_series(): def update_series():
notifications.write(msg="Update series list from Sonarr is running...", queue='get_series') notifications.write(msg="Update series list from Sonarr is running...", queue='get_series')
apikey_sonarr = settings.sonarr.apikey apikey_sonarr = settings.sonarr.apikey
if apikey_sonarr is None:
return
sonarr_version = get_sonarr_version() sonarr_version = get_sonarr_version()
serie_default_enabled = settings.general.getboolean('serie_default_enabled') serie_default_enabled = settings.general.getboolean('serie_default_enabled')
serie_default_language = settings.general.serie_default_language serie_default_language = settings.general.serie_default_language
serie_default_hi = settings.general.serie_default_hi serie_default_hi = settings.general.serie_default_hi
serie_default_forced = settings.general.serie_default_forced serie_default_forced = settings.general.serie_default_forced
audio_profiles = get_profile_list()
if apikey_sonarr is None:
pass # Get shows data from Sonarr
else: url_sonarr_api_series = url_sonarr() + "/api/series?apikey=" + apikey_sonarr
audio_profiles = get_profile_list() try:
r = requests.get(url_sonarr_api_series, timeout=60, verify=False)
# Get shows data from Sonarr r.raise_for_status()
url_sonarr_api_series = url_sonarr() + "/api/series?apikey=" + apikey_sonarr except requests.exceptions.HTTPError:
try: logging.exception("BAZARR Error trying to get series from Sonarr. Http error.")
r = requests.get(url_sonarr_api_series, timeout=60, verify=False) return
r.raise_for_status() except requests.exceptions.ConnectionError:
except requests.exceptions.HTTPError as errh: logging.exception("BAZARR Error trying to get series from Sonarr. Connection Error.")
logging.exception("BAZARR Error trying to get series from Sonarr. Http error.") return
return except requests.exceptions.Timeout:
except requests.exceptions.ConnectionError as errc: logging.exception("BAZARR Error trying to get series from Sonarr. Timeout Error.")
logging.exception("BAZARR Error trying to get series from Sonarr. Connection Error.") return
return except requests.exceptions.RequestException:
except requests.exceptions.Timeout as errt: logging.exception("BAZARR Error trying to get series from Sonarr.")
logging.exception("BAZARR Error trying to get series from Sonarr. Timeout Error.") return
return
except requests.exceptions.RequestException as err: # Get current shows in DB
logging.exception("BAZARR Error trying to get series from Sonarr.") current_shows_db = database.execute("SELECT sonarrSeriesId FROM table_shows")
return
current_shows_db_list = [x['sonarrSeriesId'] for x in current_shows_db]
current_shows_sonarr = []
series_to_update = []
series_to_add = []
series_list_length = len(r.json())
for i, show in enumerate(r.json(), 1):
notifications.write(msg="Getting series data from Sonarr...", queue='get_series', item=i,
length=series_list_length)
overview = show['overview'] if 'overview' in show else ''
poster = ''
fanart = ''
for image in show['images']:
if image['coverType'] == 'poster':
poster_big = image['url'].split('?')[0]
poster = os.path.splitext(poster_big)[0] + '-250' + os.path.splitext(poster_big)[1]
if image['coverType'] == 'fanart':
fanart = image['url'].split('?')[0]
alternate_titles = None
if show['alternateTitles'] is not None:
alternate_titles = str([item['title'] for item in show['alternateTitles']])
if sonarr_version.startswith('2'):
audio_language = profile_id_to_language(show['qualityProfileId'], audio_profiles)
else: else:
# Get current shows in DB audio_language = profile_id_to_language(show['languageProfileId'], audio_profiles)
current_shows_db = database.execute("SELECT sonarrSeriesId FROM table_shows")
current_shows_db_list = [x['sonarrSeriesId'] for x in current_shows_db]
current_shows_sonarr = []
series_to_update = []
series_to_add = []
altered_series = []
seriesListLength = len(r.json()) # Add shows in Sonarr to current shows list
for i, show in enumerate(r.json(), 1): current_shows_sonarr.append(show['id'])
notifications.write(msg="Getting series data from Sonarr...", queue='get_series', item=i, length=seriesListLength)
try:
overview = six.text_type(show['overview'])
except:
overview = ""
try:
poster_big = show['images'][2]['url'].split('?')[0]
poster = os.path.splitext(poster_big)[0] + '-250' + os.path.splitext(poster_big)[1]
except:
poster = ""
try:
fanart = show['images'][0]['url'].split('?')[0]
except:
fanart = ""
if show['alternateTitles'] != None: if show['id'] in current_shows_db_list:
alternateTitles = str([item['title'] for item in show['alternateTitles']]) series_to_update.append({'title': show["title"],
else: 'path': show["path"],
alternateTitles = None 'tvdbId': int(show["tvdbId"]),
'sonarrSeriesId': int(show["id"]),
'overview': overview,
'poster': poster,
'fanart': fanart,
'audio_language': audio_language,
'sortTitle': show['sortTitle'],
'year': show['year'],
'alternateTitles': alternate_titles})
else:
if serie_default_enabled is True:
series_to_add.append({'title': show["title"],
'path': show["path"],
'tvdbId': show["tvdbId"],
'languages': serie_default_language,
'hearing_impaired': serie_default_hi,
'sonarrSeriesId': show["id"],
'overview': overview,
'poster': poster,
'fanart': fanart,
'audio_language': audio_language,
'sortTitle': show['sortTitle'],
'year': show['year'],
'alternateTitles': alternate_titles,
'forced': serie_default_forced})
else:
series_to_add.append({'title': show["title"],
'path': show["path"],
'tvdbId': show["tvdbId"],
'sonarrSeriesId': show["id"],
'overview': overview,
'poster': poster,
'fanart': fanart,
'audio_language': audio_language,
'sortTitle': show['sortTitle'],
'year': show['year'],
'alternateTitles': alternate_titles})
# Add shows in Sonarr to current shows list # Remove old series from DB
current_shows_sonarr.append(show['id']) removed_series = list(set(current_shows_db_list) - set(current_shows_sonarr))
if show['tvdbId'] in current_shows_db_list:
series_to_update.append({'title': six.text_type(show["title"]),
'path': six.text_type(show["path"]),
'tvdbId': int(show["tvdbId"]),
'sonarrSeriesId': int(show["id"]),
'overview': six.text_type(overview),
'poster': six.text_type(poster),
'fanart': six.text_type(fanart),
'audio_language': six.text_type(profile_id_to_language((show['qualityProfileId'] if get_sonarr_version().startswith('2') else show['languageProfileId']), audio_profiles)),
'sortTitle': six.text_type(show['sortTitle']),
'year': six.text_type(show['year']),
'alternateTitles': six.text_type(alternateTitles)})
else:
if serie_default_enabled is True:
series_to_add.append({'title': show["title"],
'path': show["path"],
'tvdbId': show["tvdbId"],
'languages': serie_default_language,
'hearing_impaired': serie_default_hi,
'sonarrSeriesId': show["id"],
'overview': overview,
'poster': poster,
'fanart': fanart,
'audio_language': profile_id_to_language((show['qualityProfileId'] if sonarr_version.startswith('2') else show['languageProfileId']), audio_profiles),
'sortTitle': show['sortTitle'],
'year': show['year'],
'alternateTitles': alternateTitles,
'forced': serie_default_forced})
else:
series_to_add.append({'title': show["title"],
'path': show["path"],
'tvdbId': show["tvdbId"],
'sonarrSeriesId': show["id"],
'overview': overview,
'poster': poster,
'fanart': fanart,
'audio_language': profile_id_to_language((show['qualityProfileId'] if sonarr_version.startswith('2') else show['languageProfileId']), audio_profiles),
'sortTitle': show['sortTitle'],
'year': show['year'],
'alternateTitles': alternateTitles})
# Remove old series from DB for series in removed_series:
removed_series = list(set(current_shows_db_list) - set(current_shows_sonarr)) database.execute("DELETE FROM table_shows WHERE sonarrSEriesId=?", (series,))
for series in removed_series: # Update existing series in DB
database.execute("DELETE FROM table_shows WHERE sonarrSEriesId=?",(series,)) series_in_db_list = []
series_in_db = database.execute("SELECT title, path, tvdbId, sonarrSeriesId, overview, poster, fanart, "
"audio_language, sortTitle, year, alternateTitles FROM table_shows")
# Update existing series in DB for item in series_in_db:
series_in_db_list = [] series_in_db_list.append(item)
series_in_db = database.execute("SELECT title, path, tvdbId, sonarrSeriesId, overview, poster, fanart, "
"audio_language, sortTitle, year, alternateTitles FROM table_shows")
for item in series_in_db: series_to_update_list = [i for i in series_to_update if i not in series_in_db_list]
series_in_db_list.append(item)
series_to_update_list = [i for i in series_to_update if i not in series_in_db_list] for updated_series in series_to_update_list:
query = dict_converter.convert(updated_series)
database.execute('''UPDATE table_shows SET ''' + query.keys_update + ''' WHERE sonarrSeriesId = ?''',
query.values + (updated_series['sonarrSeriesId'],))
for updated_series in series_to_update_list: # Insert new series in DB
query = dict_converter.convert(updated_series) for added_series in series_to_add:
database.execute('''UPDATE table_shows SET ''' + query.keys_update + ''' WHERE sonarrSeriesId = ?''', query = dict_converter.convert(added_series)
query.values + (updated_series['sonarrSeriesId'],)) result = database.execute(
'''INSERT OR IGNORE INTO table_shows(''' + query.keys_insert + ''') VALUES(''' +
query.question_marks + ''')''', query.values)
if result:
list_missing_subtitles(no=added_series['sonarrSeriesId'])
else:
logging.debug('BAZARR unable to insert this series into the database:',
path_replace(added_series['path']))
# Insert new series in DB logging.debug('BAZARR All series synced from Sonarr into database.')
for added_series in series_to_add:
query = dict_converter.convert(added_series)
result = database.execute(
'''INSERT OR IGNORE INTO table_shows(''' + query.keys_insert + ''') VALUES(''' +
query.question_marks + ''')''', query.values)
if result:
list_missing_subtitles(no=added_series['sonarrSeriesId'])
else:
logging.debug('BAZARR unable to insert this series into the database:',
path_replace(added_series['path']))
logging.debug('BAZARR All series synced from Sonarr into database.')
def get_profile_list(): def get_profile_list():
apikey_sonarr = settings.sonarr.apikey apikey_sonarr = settings.sonarr.apikey
sonarr_version = get_sonarr_version() sonarr_version = get_sonarr_version()
profiles_list = [] profiles_list = []
# Get profiles data from Sonarr
# Get profiles data from Sonarr
if sonarr_version.startswith('2'): if sonarr_version.startswith('2'):
url_sonarr_api_series = url_sonarr() + "/api/profile?apikey=" + apikey_sonarr url_sonarr_api_series = url_sonarr() + "/api/profile?apikey=" + apikey_sonarr
elif sonarr_version.startswith('3'): else:
url_sonarr_api_series = url_sonarr() + "/api/v3/languageprofile?apikey=" + apikey_sonarr url_sonarr_api_series = url_sonarr() + "/api/v3/languageprofile?apikey=" + apikey_sonarr
try: try:
profiles_json = requests.get(url_sonarr_api_series, timeout=60, verify=False) profiles_json = requests.get(url_sonarr_api_series, timeout=60, verify=False)
except requests.exceptions.ConnectionError as errc: except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.") logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.")
except requests.exceptions.Timeout as errt: return None
except requests.exceptions.Timeout:
logging.exception("BAZARR Error trying to get profiles from Sonarr. Timeout Error.") logging.exception("BAZARR Error trying to get profiles from Sonarr. Timeout Error.")
except requests.exceptions.RequestException as err: return None
except requests.exceptions.RequestException:
logging.exception("BAZARR Error trying to get profiles from Sonarr.") logging.exception("BAZARR Error trying to get profiles from Sonarr.")
return None
# Parsing data returned from Sonarr
if sonarr_version.startswith('2'):
for profile in profiles_json.json():
profiles_list.append([profile['id'], profile['language'].capitalize()])
else: else:
# Parsing data returned from Sonarr for profile in profiles_json.json():
if sonarr_version.startswith('2'): profiles_list.append([profile['id'], profile['name'].capitalize()])
for profile in profiles_json.json():
profiles_list.append([profile['id'], profile['language'].capitalize()])
elif sonarr_version.startswith('3'):
for profile in profiles_json.json():
profiles_list.append([profile['id'], profile['name'].capitalize()])
return profiles_list return profiles_list
return None
def profile_id_to_language(id, profiles): def profile_id_to_language(id_, profiles):
for profile in profiles: for profile in profiles:
if id == profile[0]: if id_ == profile[0]:
return profile[1] return profile[1]

View File

@ -40,6 +40,7 @@ from analytics import track_event
import six import six
from six.moves import range from six.moves import range
from functools import reduce from functools import reduce
from locale import getpreferredencoding
def get_video(path, title, sceneName, use_scenename, providers=None, media_type="movie"): def get_video(path, title, sceneName, use_scenename, providers=None, media_type="movie"):
@ -234,34 +235,7 @@ def download_subtitle(path, language, hi, forced, providers, providers_auth, sce
command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language,
downloaded_language_code2, downloaded_language_code3, downloaded_language_code2, downloaded_language_code3,
subtitle.language.forced) subtitle.language.forced)
try: postprocessing(command, path)
if os.name == 'nt':
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait for the process to terminate
out, err = process.communicate()
if os.name == 'nt':
out = out.decode(encoding)
except:
if out == "":
logging.error(
'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.error('BAZARR Post-processing result for file ' + path + ' : ' + out)
else:
if out == "":
logging.info(
'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out)
# fixme: support multiple languages at once # fixme: support multiple languages at once
if media_type == 'series': if media_type == 'series':
@ -459,34 +433,7 @@ def manual_download_subtitle(path, language, hi, forced, subtitle, provider, pro
command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language,
downloaded_language_code2, downloaded_language_code3, downloaded_language_code2, downloaded_language_code3,
subtitle.language.forced) subtitle.language.forced)
try: postprocessing(command, path)
if os.name == 'nt':
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait for the process to terminate
out, err = process.communicate()
if os.name == 'nt':
out = out.decode(encoding)
except:
if out == "":
logging.error(
'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.error('BAZARR Post-processing result for file ' + path + ' : ' + out)
else:
if out == "":
logging.info(
'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out)
if media_type == 'series': if media_type == 'series':
reversed_path = path_replace_reverse(path) reversed_path = path_replace_reverse(path)
@ -1063,7 +1010,7 @@ def upgrade_subtitles():
except ValueError: except ValueError:
pass pass
else: else:
if int(upgradable_movie['score']) < 360: if int(upgradable_movie['score']) < 120:
upgradable_movies_not_perfect.append(upgradable_movie) upgradable_movies_not_perfect.append(upgradable_movie)
movies_to_upgrade = [] movies_to_upgrade = []
@ -1078,6 +1025,8 @@ def upgrade_subtitles():
if settings.general.getboolean('use_sonarr'): if settings.general.getboolean('use_sonarr'):
for i, episode in enumerate(episodes_to_upgrade, 1): for i, episode in enumerate(episodes_to_upgrade, 1):
if episode['languages'] in [None, 'None', '[]']:
continue
providers = get_providers() providers = get_providers()
if not providers: if not providers:
notifications.write(msg='BAZARR All providers are throttled', queue='get_subtitle', duration='long') notifications.write(msg='BAZARR All providers are throttled', queue='get_subtitle', duration='long')
@ -1127,6 +1076,8 @@ def upgrade_subtitles():
if settings.general.getboolean('use_radarr'): if settings.general.getboolean('use_radarr'):
for i, movie in enumerate(movies_to_upgrade, 1): for i, movie in enumerate(movies_to_upgrade, 1):
if movie['languages'] in [None, 'None', '[]']:
continue
providers = get_providers() providers = get_providers()
if not providers: if not providers:
notifications.write(msg='BAZARR All providers are throttled', queue='get_subtitle', duration='long') notifications.write(msg='BAZARR All providers are throttled', queue='get_subtitle', duration='long')
@ -1173,3 +1124,41 @@ def upgrade_subtitles():
store_subtitles_movie(movie['video_path'], path_replace_movie(movie['video_path'])) store_subtitles_movie(movie['video_path'], path_replace_movie(movie['video_path']))
history_log_movie(3, movie['radarrId'], message, path, language_code, provider, score) history_log_movie(3, movie['radarrId'], message, path, language_code, provider, score)
send_notifications_movie(movie['radarrId'], message) send_notifications_movie(movie['radarrId'], message)
def postprocessing(command, path):
try:
encoding = getpreferredencoding()
if os.name == 'nt':
if six.PY3:
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, encoding=getpreferredencoding())
else:
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
if six.PY3:
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, encoding=encoding)
else:
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait for the process to terminate
out, err = process.communicate()
if six.PY2:
out = out.decode(encoding)
out = out.replace('\n', ' ').replace('\r', ' ')
except Exception as e:
logging.error('BAZARR Post-processing failed for file ' + path + ' : ' + repr(e))
else:
if out == "":
logging.info(
'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out)

View File

@ -1,6 +1,6 @@
# coding=utf-8 # coding=utf-8
bazarr_version = '0.8.4' bazarr_version = '0.8.4.1'
import os import os
os.environ["SZ_USER_AGENT"] = "Bazarr/1" os.environ["SZ_USER_AGENT"] = "Bazarr/1"
@ -9,6 +9,7 @@ os.environ["BAZARR_VERSION"] = bazarr_version
import gc import gc
import sys import sys
import libs import libs
import io
import six import six
from six.moves import zip from six.moves import zip
@ -209,11 +210,11 @@ def shutdown():
else: else:
database.close() database.close()
try: try:
stop_file = open(os.path.join(args.config_dir, "bazarr.stop"), "w") stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
except Exception as e: except Exception as e:
logging.error('BAZARR Cannot create bazarr.stop file.') logging.error('BAZARR Cannot create bazarr.stop file.')
else: else:
stop_file.write('') stop_file.write(six.text_type(''))
stop_file.close() stop_file.close()
sys.exit(0) sys.exit(0)
@ -229,12 +230,12 @@ def restart():
else: else:
database.close() database.close()
try: try:
restart_file = open(os.path.join(args.config_dir, "bazarr.restart"), "w") restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
except Exception as e: except Exception as e:
logging.error('BAZARR Cannot create bazarr.restart file.') logging.error('BAZARR Cannot create bazarr.restart file.')
else: else:
logging.info('Bazarr is being restarted...') logging.info('Bazarr is being restarted...')
restart_file.write('') restart_file.write(six.text_type(''))
restart_file.close() restart_file.close()
sys.exit(0) sys.exit(0)
@ -398,6 +399,8 @@ def save_wizard():
settings.addic7ed.password = request.forms.get('settings_addic7ed_password') settings.addic7ed.password = request.forms.get('settings_addic7ed_password')
settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents) settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents)
settings.assrt.token = request.forms.get('settings_assrt_token') settings.assrt.token = request.forms.get('settings_assrt_token')
settings.legendasdivx.username = request.forms.get('settings_legendasdivx_username')
settings.legendasdivx.password = request.forms.get('settings_legendasdivx_password')
settings.legendastv.username = request.forms.get('settings_legendastv_username') settings.legendastv.username = request.forms.get('settings_legendastv_username')
settings.legendastv.password = request.forms.get('settings_legendastv_password') settings.legendastv.password = request.forms.get('settings_legendastv_password')
settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username') settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username')
@ -996,15 +999,18 @@ def historyseries():
else: else:
series_monitored_only_query_string = '' series_monitored_only_query_string = ''
upgradable_episodes = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score FROM table_history " upgradable_episodes = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score, table_shows.languages FROM table_history "
"INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = " "INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = "
"table_history.sonarrEpisodeId WHERE action IN (" + "table_history.sonarrEpisodeId JOIN table_shows on table_shows.sonarrSeriesId = "
"table_history.sonarrSeriesId WHERE action IN (" +
','.join(map(str, query_actions)) + ") AND timestamp > ? AND " ','.join(map(str, query_actions)) + ") AND timestamp > ? AND "
"score is not null" + series_monitored_only_query_string + " GROUP BY " "score is not null" + series_monitored_only_query_string + " GROUP BY "
"table_history.video_path, table_history.language", "table_history.video_path, table_history.language",
(minimum_timestamp,)) (minimum_timestamp,))
for upgradable_episode in upgradable_episodes: for upgradable_episode in upgradable_episodes:
if upgradable_episode['languages'] in [None, 'None', '[]']:
continue
if upgradable_episode['timestamp'] > minimum_timestamp: if upgradable_episode['timestamp'] > minimum_timestamp:
try: try:
int(upgradable_episode['score']) int(upgradable_episode['score'])
@ -1073,15 +1079,17 @@ def historymovies():
else: else:
query_actions = [1, 3] query_actions = [1, 3]
upgradable_movies = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score FROM table_history_movie " upgradable_movies = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score, "
"INNER JOIN table_movies on table_movies.radarrId=" "table_movies.languages FROM table_history_movie INNER JOIN table_movies "
"table_history_movie.radarrId WHERE action IN (" + "on table_movies.radarrId=table_history_movie.radarrId WHERE action IN (" +
','.join(map(str, query_actions)) + ','.join(map(str, query_actions)) +
") AND timestamp > ? AND score is not NULL" + ") AND timestamp > ? AND score is not NULL" +
movies_monitored_only_query_string + " GROUP BY video_path, language", movies_monitored_only_query_string + " GROUP BY video_path, language",
(minimum_timestamp,)) (minimum_timestamp,))
for upgradable_movie in upgradable_movies: for upgradable_movie in upgradable_movies:
if upgradable_movie['languages'] in [None, 'None', '[]']:
continue
if upgradable_movie['timestamp'] > minimum_timestamp: if upgradable_movie['timestamp'] > minimum_timestamp:
try: try:
int(upgradable_movie['score']) int(upgradable_movie['score'])
@ -1527,6 +1535,8 @@ def save_settings():
settings.addic7ed.password = request.forms.get('settings_addic7ed_password') settings.addic7ed.password = request.forms.get('settings_addic7ed_password')
settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents) settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents)
settings.assrt.token = request.forms.get('settings_assrt_token') settings.assrt.token = request.forms.get('settings_assrt_token')
settings.legendasdivx.username = request.forms.get('settings_legendasdivx_username')
settings.legendasdivx.password = request.forms.get('settings_legendasdivx_password')
settings.legendastv.username = request.forms.get('settings_legendastv_username') settings.legendastv.username = request.forms.get('settings_legendastv_username')
settings.legendastv.password = request.forms.get('settings_legendastv_password') settings.legendastv.password = request.forms.get('settings_legendastv_password')
settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username') settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username')
@ -1698,7 +1708,7 @@ def system():
throttled_providers = list_throttled_providers() throttled_providers = list_throttled_providers()
try: try:
with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r') as f: with io.open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r', encoding='UTF-8') as f:
releases = ast.literal_eval(f.read()) releases = ast.literal_eval(f.read())
except Exception as e: except Exception as e:
releases = [] releases = []
@ -1724,7 +1734,7 @@ def system():
def get_logs(): def get_logs():
authorize() authorize()
logs = [] logs = []
with open(os.path.join(args.config_dir, 'log', 'bazarr.log')) as file: with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file:
for line in file.readlines(): for line in file.readlines():
lin = [] lin = []
lin = line.split('|') lin = line.split('|')
@ -1749,9 +1759,9 @@ def execute_task(taskid):
@custom_auth_basic(check_credentials) @custom_auth_basic(check_credentials)
def remove_subtitles(): def remove_subtitles():
authorize() authorize()
episodePath = request.forms.get('episodePath') episodePath = request.forms.episodePath
language = request.forms.get('language') language = request.forms.get('language')
subtitlesPath = request.forms.get('subtitlesPath') subtitlesPath = request.forms.subtitlesPath
sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
@ -1768,9 +1778,9 @@ def remove_subtitles():
@custom_auth_basic(check_credentials) @custom_auth_basic(check_credentials)
def remove_subtitles_movie(): def remove_subtitles_movie():
authorize() authorize()
moviePath = request.forms.get('moviePath') moviePath = request.forms.moviePath
language = request.forms.get('language') language = request.forms.get('language')
subtitlesPath = request.forms.get('subtitlesPath') subtitlesPath = request.forms.subtitlesPath
radarrId = request.forms.get('radarrId') radarrId = request.forms.get('radarrId')
try: try:
@ -1788,14 +1798,14 @@ def get_subtitle():
authorize() authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
episodePath = request.forms.get('episodePath') episodePath = request.forms.episodePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
hi = request.forms.get('hi') hi = request.forms.get('hi')
forced = request.forms.get('forced') forced = request.forms.get('forced')
sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
title = request.forms.get('title') title = request.forms.title
providers_list = get_providers() providers_list = get_providers()
providers_auth = get_providers_auth() providers_auth = get_providers_auth()
@ -1823,12 +1833,12 @@ def get_subtitle():
def manual_search_json(): def manual_search_json():
authorize() authorize()
episodePath = request.forms.get('episodePath') episodePath = request.forms.episodePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
hi = request.forms.get('hi') hi = request.forms.get('hi')
forced = request.forms.get('forced') forced = request.forms.get('forced')
title = request.forms.get('title') title = request.forms.title
providers_list = get_providers() providers_list = get_providers()
providers_auth = get_providers_auth() providers_auth = get_providers_auth()
@ -1843,16 +1853,16 @@ def manual_get_subtitle():
authorize() authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
episodePath = request.forms.get('episodePath') episodePath = request.forms.episodePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
hi = request.forms.get('hi') hi = request.forms.get('hi')
forced = request.forms.get('forced') forced = request.forms.get('forced')
selected_provider = request.forms.get('provider') selected_provider = request.forms.get('provider')
subtitle = request.forms.get('subtitle') subtitle = request.forms.subtitle
sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
title = request.forms.get('title') title = request.forms.title
providers_auth = get_providers_auth() providers_auth = get_providers_auth()
@ -1881,14 +1891,14 @@ def perform_manual_upload_subtitle():
authorize() authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
episodePath = request.forms.get('episodePath') episodePath = request.forms.episodePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
forced = True if request.forms.get('forced') == '1' else False forced = True if request.forms.get('forced') == '1' else False
upload = request.files.get('upload') upload = request.files.get('upload')
sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
title = request.forms.get('title') title = request.forms.title
_, ext = os.path.splitext(upload.filename) _, ext = os.path.splitext(upload.filename)
@ -1925,13 +1935,13 @@ def get_subtitle_movie():
authorize() authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
moviePath = request.forms.get('moviePath') moviePath = request.forms.moviePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
hi = request.forms.get('hi') hi = request.forms.get('hi')
forced = request.forms.get('forced') forced = request.forms.get('forced')
radarrId = request.forms.get('radarrId') radarrId = request.forms.get('radarrId')
title = request.forms.get('title') title = request.forms.title
providers_list = get_providers() providers_list = get_providers()
providers_auth = get_providers_auth() providers_auth = get_providers_auth()
@ -1959,12 +1969,12 @@ def get_subtitle_movie():
def manual_search_movie_json(): def manual_search_movie_json():
authorize() authorize()
moviePath = request.forms.get('moviePath') moviePath = request.forms.moviePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
hi = request.forms.get('hi') hi = request.forms.get('hi')
forced = request.forms.get('forced') forced = request.forms.get('forced')
title = request.forms.get('title') title = request.forms.title
providers_list = get_providers() providers_list = get_providers()
providers_auth = get_providers_auth() providers_auth = get_providers_auth()
@ -1979,15 +1989,15 @@ def manual_get_subtitle_movie():
authorize() authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
moviePath = request.forms.get('moviePath') moviePath = request.forms.moviePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
hi = request.forms.get('hi') hi = request.forms.get('hi')
forced = request.forms.get('forced') forced = request.forms.get('forced')
selected_provider = request.forms.get('provider') selected_provider = request.forms.provider
subtitle = request.forms.get('subtitle') subtitle = request.forms.subtitle
radarrId = request.forms.get('radarrId') radarrId = request.forms.get('radarrId')
title = request.forms.get('title') title = request.forms.title
providers_auth = get_providers_auth() providers_auth = get_providers_auth()
@ -2015,13 +2025,13 @@ def perform_manual_upload_subtitle_movie():
authorize() authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
moviePath = request.forms.get('moviePath') moviePath = request.forms.moviePath
sceneName = request.forms.get('sceneName') sceneName = request.forms.sceneName
language = request.forms.get('language') language = request.forms.get('language')
forced = True if request.forms.get('forced') == '1' else False forced = True if request.forms.get('forced') == '1' else False
upload = request.files.get('upload') upload = request.files.get('upload')
radarrId = request.forms.get('radarrId') radarrId = request.forms.get('radarrId')
title = request.forms.get('title') title = request.forms.title
_, ext = os.path.splitext(upload.filename) _, ext = os.path.splitext(upload.filename)

View File

@ -2,23 +2,21 @@
from __future__ import absolute_import from __future__ import absolute_import
import apprise import apprise
import os
import logging import logging
from get_args import args
from database import database from database import database
def update_notifier(): def update_notifier():
# define apprise object # define apprise object
a = apprise.Apprise() a = apprise.Apprise()
# Retrieve all of the details # Retrieve all of the details
results = a.details() results = a.details()
notifiers_new = [] notifiers_new = []
notifiers_old = [] notifiers_old = []
notifiers_current_db = database.execute("SELECT name FROM table_settings_notifier") notifiers_current_db = database.execute("SELECT name FROM table_settings_notifier")
notifiers_current = [] notifiers_current = []
@ -31,66 +29,68 @@ def update_notifier():
logging.debug('Adding new notifier agent: ' + x['service_name']) logging.debug('Adding new notifier agent: ' + x['service_name'])
else: else:
notifiers_old.append([x['service_name']]) notifiers_old.append([x['service_name']])
notifiers_to_delete = [item for item in notifiers_current if item not in notifiers_old] notifiers_to_delete = [item for item in notifiers_current if item not in notifiers_old]
database.execute("INSERT INTO table_settings_notifier (name, enabled) VALUES (?, ?)", notifiers_new, execute_many=True) database.execute("INSERT INTO table_settings_notifier (name, enabled) VALUES (?, ?)", notifiers_new,
execute_many=True)
database.execute("DELETE FROM table_settings_notifier WHERE name=?", notifiers_to_delete, execute_many=True) database.execute("DELETE FROM table_settings_notifier WHERE name=?", notifiers_to_delete, execute_many=True)
def get_notifier_providers(): def get_notifier_providers():
providers = database.execute("SELECT name, url FROM table_settings_notifier WHERE enabled=1") providers = database.execute("SELECT name, url FROM table_settings_notifier WHERE enabled=1")
return providers return providers
def get_series_name(sonarrSeriesId): def get_series_name(sonarr_series_id):
data = database.execute("SELECT title FROM table_shows WHERE sonarrSeriesId=?", (sonarrSeriesId,), only_one=True) data = database.execute("SELECT title FROM table_shows WHERE sonarrSeriesId=?", (sonarr_series_id,), only_one=True)
return data['title'] or None return data['title'] or None
def get_episode_name(sonarrEpisodeId): def get_episode_name(sonarr_episode_id):
data = database.execute("SELECT title, season, episode FROM table_episodes WHERE sonarrEpisodeId=?", data = database.execute("SELECT title, season, episode FROM table_episodes WHERE sonarrEpisodeId=?",
(sonarrEpisodeId,), only_one=True) (sonarr_episode_id,), only_one=True)
return data['title'], data['season'], data['episode'] return data['title'], data['season'], data['episode']
def get_movies_name(radarrId): def get_movies_name(radarr_id):
data = database.execute("SELECT title FROM table_movies WHERE radarrId=?", (radarrId,), only_one=True) data = database.execute("SELECT title FROM table_movies WHERE radarrId=?", (radarr_id,), only_one=True)
return data['title'] return data['title']
def send_notifications(sonarrSeriesId, sonarrEpisodeId, message): def send_notifications(sonarr_series_id, sonarr_episode_id, message):
providers = get_notifier_providers() providers = get_notifier_providers()
series = get_series_name(sonarrSeriesId) series = get_series_name(sonarr_series_id)
episode = get_episode_name(sonarrEpisodeId) episode = get_episode_name(sonarr_episode_id)
apobj = apprise.Apprise() apobj = apprise.Apprise()
for provider in providers: for provider in providers:
if provider['url'] is not None: if provider['url'] is not None:
apobj.add(provider['url']) apobj.add(provider['url'])
apobj.notify( apobj.notify(
title='Bazarr notification', title='Bazarr notification',
body=(series + ' - S' + str(episode[1]).zfill(2) + 'E' + str(episode[2]).zfill(2) + ' - ' + episode[0] + ' : ' + message), body="{} - S{:02d}E{:02d} - {} : {}".format(series, episode[1], episode[2], episode[0], message),
) )
def send_notifications_movie(radarrId, message): def send_notifications_movie(radarr_id, message):
providers = get_notifier_providers() providers = get_notifier_providers()
movie = get_movies_name(radarrId) movie = get_movies_name(radarr_id)
apobj = apprise.Apprise() apobj = apprise.Apprise()
for provider in providers: for provider in providers:
if provider['url'] is not None: if provider['url'] is not None:
apobj.add(provider['url']) apobj.add(provider['url'])
apobj.notify( apobj.notify(
title='Bazarr notification', title='Bazarr notification',
body=movie + ' : ' + message, body="{} : {}".format(movie, message),
) )

View File

@ -4,7 +4,6 @@ from __future__ import absolute_import
import os import os
import time import time
import platform import platform
import sys
import logging import logging
import requests import requests
@ -17,22 +16,20 @@ import datetime
import glob import glob
def history_log(action, sonarrSeriesId, sonarrEpisodeId, description, video_path=None, language=None, provider=None, def history_log(action, sonarr_series_id, sonarr_episode_id, description, video_path=None, language=None, provider=None,
score=None, forced=False): score=None):
from database import database from database import database
database.execute("INSERT INTO table_history (action, sonarrSeriesId, sonarrEpisodeId, timestamp, description," database.execute("INSERT INTO table_history (action, sonarrSeriesId, sonarrEpisodeId, timestamp, description,"
"video_path, language, provider, score) VALUES (?,?,?,?,?,?,?,?,?)", (action, sonarrSeriesId, "video_path, language, provider, score) VALUES (?,?,?,?,?,?,?,?,?)",
sonarrEpisodeId, time.time(), (action, sonarr_series_id, sonarr_episode_id, time.time(), description, video_path, language,
description, video_path, provider, score))
language, provider, score))
def history_log_movie(action, radarrId, description, video_path=None, language=None, provider=None, score=None, def history_log_movie(action, radarr_id, description, video_path=None, language=None, provider=None, score=None):
forced=False):
from database import database from database import database
database.execute("INSERT INTO table_history_movie (action, radarrId, timestamp, description, video_path, language, " database.execute("INSERT INTO table_history_movie (action, radarrId, timestamp, description, video_path, language, "
"provider, score) VALUES (?,?,?,?,?,?,?,?)", (action, radarrId, time.time(), description, "provider, score) VALUES (?,?,?,?,?,?,?,?)",
video_path, language, provider, score)) (action, radarr_id, time.time(), description, video_path, language, provider, score))
def get_binary(name): def get_binary(name):
@ -46,10 +43,8 @@ def get_binary(name):
else: else:
if platform.system() == "Windows": # Windows if platform.system() == "Windows": # Windows
exe = os.path.abspath(os.path.join(binaries_dir, "Windows", "i386", name, "%s.exe" % name)) exe = os.path.abspath(os.path.join(binaries_dir, "Windows", "i386", name, "%s.exe" % name))
elif platform.system() == "Darwin": # MacOSX elif platform.system() == "Darwin": # MacOSX
exe = os.path.abspath(os.path.join(binaries_dir, "MacOSX", "i386", name, name)) exe = os.path.abspath(os.path.join(binaries_dir, "MacOSX", "i386", name, name))
elif platform.system() == "Linux": # Linux elif platform.system() == "Linux": # Linux
exe = os.path.abspath(os.path.join(binaries_dir, "Linux", platform.machine(), name, name)) exe = os.path.abspath(os.path.join(binaries_dir, "Linux", platform.machine(), name, name))
@ -82,62 +77,52 @@ def cache_maintenance():
def get_sonarr_version(): def get_sonarr_version():
use_sonarr = settings.general.getboolean('use_sonarr')
apikey_sonarr = settings.sonarr.apikey
sv = url_sonarr() + "/api/system/status?apikey=" + apikey_sonarr
sonarr_version = '' sonarr_version = ''
if use_sonarr: if settings.general.getboolean('use_sonarr'):
try: try:
sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey
sonarr_version = requests.get(sv, timeout=60, verify=False).json()['version'] sonarr_version = requests.get(sv, timeout=60, verify=False).json()['version']
except Exception as e: except Exception:
logging.debug('BAZARR cannot get Sonarr version') logging.debug('BAZARR cannot get Sonarr version')
return sonarr_version return sonarr_version
def get_sonarr_platform(): def get_sonarr_platform():
use_sonarr = settings.general.getboolean('use_sonarr')
apikey_sonarr = settings.sonarr.apikey
sv = url_sonarr() + "/api/system/status?apikey=" + apikey_sonarr
sonarr_platform = '' sonarr_platform = ''
if use_sonarr: if settings.general.getboolean('use_sonarr'):
try: try:
if requests.get(sv, timeout=60, verify=False).json()['isLinux'] or requests.get(sv, timeout=60, verify=False).json()['isOsx']: sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey
response = requests.get(sv, timeout=60, verify=False).json()
if response['isLinux'] or response['isOsx']:
sonarr_platform = 'posix' sonarr_platform = 'posix'
elif requests.get(sv, timeout=60, verify=False).json()['isWindows']: elif response['isWindows']:
sonarr_platform = 'nt' sonarr_platform = 'nt'
except Exception as e: except Exception:
logging.DEBUG('BAZARR cannot get Sonarr platform') logging.debug('BAZARR cannot get Sonarr platform')
return sonarr_platform return sonarr_platform
def get_radarr_version(): def get_radarr_version():
use_radarr = settings.general.getboolean('use_radarr')
apikey_radarr = settings.radarr.apikey
rv = url_radarr() + "/api/system/status?apikey=" + apikey_radarr
radarr_version = '' radarr_version = ''
if use_radarr: if settings.general.getboolean('use_radarr'):
try: try:
rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey
radarr_version = requests.get(rv, timeout=60, verify=False).json()['version'] radarr_version = requests.get(rv, timeout=60, verify=False).json()['version']
except Exception as e: except Exception:
logging.debug('BAZARR cannot get Radarr version') logging.debug('BAZARR cannot get Radarr version')
return radarr_version return radarr_version
def get_radarr_platform(): def get_radarr_platform():
use_radarr = settings.general.getboolean('use_radarr')
apikey_radarr = settings.radarr.apikey
rv = url_radarr() + "/api/system/status?apikey=" + apikey_radarr
radarr_platform = '' radarr_platform = ''
if use_radarr: if settings.general.getboolean('use_radarr'):
try: try:
if requests.get(rv, timeout=60, verify=False).json()['isLinux'] or requests.get(rv, timeout=60, verify=False).json()['isOsx']: rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey
response = requests.get(rv, timeout=60, verify=False).json()
if response['isLinux'] or response['isOsx']:
radarr_platform = 'posix' radarr_platform = 'posix'
elif requests.get(rv, timeout=60, verify=False).json()['isWindows']: elif response['isWindows']:
radarr_platform = 'nt' radarr_platform = 'nt'
except Exception as e: except Exception:
logging.DEBUG('BAZARR cannot get Radarr platform') logging.debug('BAZARR cannot get Radarr platform')
return radarr_platform return radarr_platform

View File

@ -543,6 +543,10 @@ def scan_video(path, dont_use_actual_file=False, hints=None, providers=None, ski
if video.size > 10485760: if video.size > 10485760:
logger.debug('Size is %d', video.size) logger.debug('Size is %d', video.size)
osub_hash = None osub_hash = None
if "bsplayer" in providers:
video.hashes['bsplayer'] = osub_hash = hash_opensubtitles(hash_path)
if "opensubtitles" in providers: if "opensubtitles" in providers:
video.hashes['opensubtitles'] = osub_hash = hash_opensubtitles(hash_path) video.hashes['opensubtitles'] = osub_hash = hash_opensubtitles(hash_path)

View File

@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import logging
import io
import os
from requests import Session
from guessit import guessit
from subliminal_patch.providers import Provider
from subliminal_patch.subtitle import Subtitle
from subliminal.utils import sanitize_release_group
from subliminal.subtitle import guess_matches
from subzero.language import Language
import gzip
import random
from time import sleep
from xml.etree import ElementTree
logger = logging.getLogger(__name__)
class BSPlayerSubtitle(Subtitle):
"""BSPlayer Subtitle."""
provider_name = 'bsplayer'
def __init__(self, language, filename, subtype, video, link):
super(BSPlayerSubtitle, self).__init__(language)
self.language = language
self.filename = filename
self.page_link = link
self.subtype = subtype
self.video = video
@property
def id(self):
return self.page_link
@property
def release_info(self):
return self.filename
def get_matches(self, video):
matches = set()
video_filename = video.name
video_filename = os.path.basename(video_filename)
video_filename, _ = os.path.splitext(video_filename)
video_filename = sanitize_release_group(video_filename)
subtitle_filename = self.filename
subtitle_filename = os.path.basename(subtitle_filename)
subtitle_filename, _ = os.path.splitext(subtitle_filename)
subtitle_filename = sanitize_release_group(subtitle_filename)
matches |= guess_matches(video, guessit(self.filename))
matches.add(id(self))
matches.add('hash')
return matches
class BSPlayerProvider(Provider):
"""BSPlayer Provider."""
languages = {Language('por', 'BR')} | {Language(l) for l in [
'ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor', 'nld', 'pol', 'por',
'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho'
]}
SEARCH_THROTTLE = 8
# batantly based on kodi's bsplayer plugin
# also took from BSPlayer-Subtitles-Downloader
def __init__(self):
self.initialize()
def initialize(self):
self.session = Session()
self.search_url = self.get_sub_domain()
self.token = None
self.login()
def terminate(self):
self.session.close()
self.logout()
def api_request(self, func_name='logIn', params='', tries=5):
headers = {
'User-Agent': 'BSPlayer/2.x (1022.12360)',
'Content-Type': 'text/xml; charset=utf-8',
'Connection': 'close',
'SOAPAction': '"http://api.bsplayer-subtitles.com/v1.php#{func_name}"'.format(func_name=func_name)
}
data = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" '
'xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="{search_url}">'
'<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
'<ns1:{func_name}>{params}</ns1:{func_name}></SOAP-ENV:Body></SOAP-ENV:Envelope>'
).format(search_url=self.search_url, func_name=func_name, params=params)
logger.info('Sending request: %s.' % func_name)
for i in iter(range(tries)):
try:
self.session.headers.update(headers.items())
res = self.session.post(self.search_url, data)
return ElementTree.fromstring(res.text)
### with requests
# res = requests.post(
# url=self.search_url,
# data=data,
# headers=headers
# )
# return ElementTree.fromstring(res.text)
except Exception as ex:
logger.info("ERROR: %s." % ex)
if func_name == 'logIn':
self.search_url = self.get_sub_domain()
sleep(1)
logger.info('ERROR: Too many tries (%d)...' % tries)
raise Exception('Too many tries...')
def login(self):
# If already logged in
if self.token:
return True
root = self.api_request(
func_name='logIn',
params=('<username></username>'
'<password></password>'
'<AppID>BSPlayer v2.67</AppID>')
)
res = root.find('.//return')
if res.find('status').text == 'OK':
self.token = res.find('data').text
logger.info("Logged In Successfully.")
return True
return False
def logout(self):
# If already logged out / not logged in
if not self.token:
return True
root = self.api_request(
func_name='logOut',
params='<handle>{token}</handle>'.format(token=self.token)
)
res = root.find('.//return')
self.token = None
if res.find('status').text == 'OK':
logger.info("Logged Out Successfully.")
return True
return False
def query(self, video, video_hash, language):
if not self.login():
return []
if isinstance(language, (tuple, list, set)):
# language_ids = ",".join(language)
# language_ids = 'spa'
language_ids = ','.join(sorted(l.opensubtitles for l in language))
if video.imdb_id is None:
imdbId = '*'
else:
imdbId = video.imdb_id
sleep(self.SEARCH_THROTTLE)
root = self.api_request(
func_name='searchSubtitles',
params=(
'<handle>{token}</handle>'
'<movieHash>{movie_hash}</movieHash>'
'<movieSize>{movie_size}</movieSize>'
'<languageId>{language_ids}</languageId>'
'<imdbId>{imdbId}</imdbId>'
).format(token=self.token, movie_hash=video_hash,
movie_size=video.size, language_ids=language_ids, imdbId=imdbId)
)
res = root.find('.//return/result')
if res.find('status').text != 'OK':
return []
items = root.findall('.//return/data/item')
subtitles = []
if items:
logger.info("Subtitles Found.")
for item in items:
subID=item.find('subID').text
subDownloadLink=item.find('subDownloadLink').text
subLang= Language.fromopensubtitles(item.find('subLang').text)
subName=item.find('subName').text
subFormat=item.find('subFormat').text
subtitles.append(
BSPlayerSubtitle(subLang,subName, subFormat, video, subDownloadLink)
)
return subtitles
def list_subtitles(self, video, languages):
return self.query(video, video.hashes['bsplayer'], languages)
def get_sub_domain(self):
# s1-9, s101-109
SUB_DOMAINS = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9',
's101', 's102', 's103', 's104', 's105', 's106', 's107', 's108', 's109']
API_URL_TEMPLATE = "http://{sub_domain}.api.bsplayer-subtitles.com/v1.php"
sub_domains_end = len(SUB_DOMAINS) - 1
return API_URL_TEMPLATE.format(sub_domain=SUB_DOMAINS[random.randint(0, sub_domains_end)])
def download_subtitle(self, subtitle):
session = Session()
_addheaders = {
'User-Agent': 'Mozilla/4.0 (compatible; Synapse)'
}
session.headers.update(_addheaders)
res = session.get(subtitle.page_link)
if res:
if res.text == '500':
raise ValueError('Error 500 on server')
with gzip.GzipFile(fileobj=io.BytesIO(res.content)) as gf:
subtitle.content = gf.read()
subtitle.normalize()
return subtitle
raise ValueError('Problems conecting to the server')

View File

@ -0,0 +1,307 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import logging
import io
import os
import rarfile
import zipfile
from requests import Session
from guessit import guessit
from subliminal_patch.exceptions import ParseResponseError
from subliminal_patch.providers import Provider
from subliminal.providers import ParserBeautifulSoup
from subliminal_patch.subtitle import Subtitle
from subliminal.video import Episode
from subliminal.subtitle import SUBTITLE_EXTENSIONS, fix_line_ending,guess_matches
from subzero.language import Language
logger = logging.getLogger(__name__)
class LegendasdivxSubtitle(Subtitle):
"""Legendasdivx Subtitle."""
provider_name = 'legendasdivx'
def __init__(self, language, video, data):
super(LegendasdivxSubtitle, self).__init__(language)
self.language = language
self.page_link = data['link']
self.hits=data['hits']
self.exact_match=data['exact_match']
self.description=data['description'].lower()
self.video = video
self.videoname =data['videoname']
@property
def id(self):
return self.page_link
@property
def release_info(self):
return self.description
def get_matches(self, video):
matches = set()
if self.videoname.lower() in self.description:
matches.update(['title'])
matches.update(['season'])
matches.update(['episode'])
# episode
if video.title and video.title.lower() in self.description:
matches.update(['title'])
if video.year and '{:04d}'.format(video.year) in self.description:
matches.update(['year'])
if isinstance(video, Episode):
# already matched in search query
if video.season and 's{:02d}'.format(video.season) in self.description:
matches.update(['season'])
if video.episode and 'e{:02d}'.format(video.episode) in self.description:
matches.update(['episode'])
if video.episode and video.season and video.series:
if '{}.s{:02d}e{:02d}'.format(video.series.lower(),video.season,video.episode) in self.description:
matches.update(['series'])
matches.update(['season'])
matches.update(['episode'])
if '{} s{:02d}e{:02d}'.format(video.series.lower(),video.season,video.episode) in self.description:
matches.update(['series'])
matches.update(['season'])
matches.update(['episode'])
# release_group
if video.release_group and video.release_group.lower() in self.description:
matches.update(['release_group'])
# resolution
if video.resolution and video.resolution.lower() in self.description:
matches.update(['resolution'])
# format
formats = []
if video.format:
formats = [video.format.lower()]
if formats[0] == "web-dl":
formats.append("webdl")
formats.append("webrip")
formats.append("web ")
for frmt in formats:
if frmt.lower() in self.description:
matches.update(['format'])
break
# video_codec
if video.video_codec:
video_codecs = [video.video_codec.lower()]
if video_codecs[0] == "h264":
formats.append("x264")
elif video_codecs[0] == "h265":
formats.append("x265")
for vc in formats:
if vc.lower() in self.description:
matches.update(['video_codec'])
break
matches |= guess_matches(video, guessit(self.description))
return matches
class LegendasdivxProvider(Provider):
"""Legendasdivx Provider."""
languages = {Language('por', 'BR')} | {Language('por')}
SEARCH_THROTTLE = 8
site = 'https://www.legendasdivx.pt'
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Origin': 'https://www.legendasdivx.pt',
'Referer': 'https://www.legendasdivx.pt',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache'
}
loginpage = site + '/forum/ucp.php?mode=login'
searchurl = site + '/modules.php?name=Downloads&file=jz&d_op=search&op=_jz00&query={query}'
language_list = list(languages)
def __init__(self, username, password):
self.username = username
self.password = password
def initialize(self):
self.session = Session()
self.login()
def terminate(self):
self.logout()
self.session.close()
def login(self):
logger.info('Logging in')
self.headers['Referer'] = self.site + '/index.php'
self.session.headers.update(self.headers.items())
res = self.session.get(self.loginpage)
bsoup = ParserBeautifulSoup(res.content, ['lxml'])
_allinputs = bsoup.findAll('input')
fields = {}
for field in _allinputs:
fields[field.get('name')] = field.get('value')
fields['username'] = self.username
fields['password'] = self.password
fields['autologin'] = 'on'
fields['viewonline'] = 'on'
self.headers['Referer'] = self.loginpage
self.session.headers.update(self.headers.items())
res = self.session.post(self.loginpage, fields)
try:
logger.debug('Got session id %s' %
self.session.cookies.get_dict()['PHPSESSID'])
except KeyError as e:
logger.error(repr(e))
logger.error("Didn't get session id, check your credentials")
return False
except Exception as e:
logger.error(repr(e))
logger.error('uncached error #legendasdivx #AA')
return False
return True
def logout(self):
# need to figure this out
return True
def query(self, video, language):
try:
logger.debug('Got session id %s' %
self.session.cookies.get_dict()['PHPSESSID'])
except Exception as e:
self.login()
return []
language_ids = '0'
if isinstance(language, (tuple, list, set)):
if len(language) == 1:
language_ids = ','.join(sorted(l.opensubtitles for l in language))
if language_ids == 'por':
language_ids = '&form_cat=28'
else:
language_ids = '&form_cat=29'
querytext = video.name
querytext = os.path.basename(querytext)
querytext, _ = os.path.splitext(querytext)
videoname = querytext
querytext = querytext.lower()
querytext = querytext.replace(
".", "+").replace("[", "").replace("]", "")
if language_ids != '0':
querytext = querytext + language_ids
self.headers['Referer'] = self.site + '/index.php'
self.session.headers.update(self.headers.items())
res = self.session.get(self.searchurl.format(query=querytext))
# form_cat=28 = br
# form_cat=29 = pt
if "A legenda não foi encontrada" in res.text:
logger.warning('%s not found', querytext)
return []
bsoup = ParserBeautifulSoup(res.content, ['html.parser'])
_allsubs = bsoup.findAll("div", {"class": "sub_box"})
subtitles = []
lang = Language.fromopensubtitles("pob")
for _subbox in _allsubs:
hits=0
for th in _subbox.findAll("th", {"class": "color2"}):
if th.string == 'Hits:':
hits = int(th.parent.find("td").string)
if th.string == 'Idioma:':
lang = th.parent.find("td").find ("img").get ('src')
if 'brazil' in lang:
lang = Language.fromopensubtitles('pob')
else:
lang = Language.fromopensubtitles('por')
description = _subbox.find("td", {"class": "td_desc brd_up"})
download = _subbox.find("a", {"class": "sub_download"})
try:
# sometimes BSoup just doesn't get the link
logger.debug(download.get('href'))
except Exception as e:
logger.warning('skipping subbox on %s' % self.searchurl.format(query=querytext))
continue
exact_match = False
if video.name.lower() in description.get_text().lower():
exact_match = True
data = {'link': self.site + '/modules.php' + download.get('href'),
'exact_match': exact_match,
'hits': hits,
'videoname': videoname,
'description': description.get_text() }
subtitles.append(
LegendasdivxSubtitle(lang, video, data)
)
return subtitles
def list_subtitles(self, video, languages):
return self.query(video, languages)
def download_subtitle(self, subtitle):
res = self.session.get(subtitle.page_link)
if res:
if res.text == '500':
raise ValueError('Error 500 on server')
archive = self._get_archive(res.content)
# extract the subtitle
subtitle_content = self._get_subtitle_from_archive(archive)
subtitle.content = fix_line_ending(subtitle_content)
subtitle.normalize()
return subtitle
raise ValueError('Problems conecting to the server')
def _get_archive(self, content):
# open the archive
# stole^H^H^H^H^H inspired from subvix provider
archive_stream = io.BytesIO(content)
if rarfile.is_rarfile(archive_stream):
logger.debug('Identified rar archive')
archive = rarfile.RarFile(archive_stream)
elif zipfile.is_zipfile(archive_stream):
logger.debug('Identified zip archive')
archive = zipfile.ZipFile(archive_stream)
else:
# raise ParseResponseError('Unsupported compressed format')
raise Exception('Unsupported compressed format')
return archive
def _get_subtitle_from_archive(self, archive):
# some files have a non subtitle with .txt extension
_tmp = list(SUBTITLE_EXTENSIONS)
_tmp.remove('.txt')
_subtitle_extensions = tuple(_tmp)
for name in archive.namelist():
# discard hidden files
if os.path.split(name)[-1].startswith('.'):
continue
# discard non-subtitle files
if not name.lower().endswith(_subtitle_extensions):
continue
logger.debug("returning from archive: %s" % name)
return archive.read(name)
raise ParseResponseError('Can not find the subtitle in the compressed file')

View File

@ -35,6 +35,10 @@ class SubdivxSubtitle(Subtitle):
def id(self): def id(self):
return self.page_link return self.page_link
@property
def release_info(self):
return self.description
def get_matches(self, video): def get_matches(self, video):
matches = set() matches = set()

View File

@ -84,7 +84,7 @@ class Subs4FreeProvider(Provider):
def initialize(self): def initialize(self):
self.session = Session() self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__) self.session.headers['User-Agent'] = os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")
def terminate(self): def terminate(self):
self.session.close() self.session.close()

View File

@ -0,0 +1,229 @@
# coding=utf-8
from __future__ import absolute_import
import io
import logging
import re
from subliminal import __short_version__
import rarfile
from zipfile import ZipFile, is_zipfile
from rarfile import RarFile, is_rarfile
from subliminal_patch.providers import Provider
from subliminal_patch.providers.mixins import ProviderSubtitleArchiveMixin
from subliminal_patch.subtitle import Subtitle
from subliminal_patch.utils import sanitize, fix_inconsistent_naming as _fix_inconsistent_naming
from subliminal.exceptions import ProviderError
from subliminal.providers import ParserBeautifulSoup
from subliminal.video import Episode, Movie
from subzero.language import Language
# parsing regex definitions
title_re = re.compile(r'(?P<title>(?:.+(?= [Aa][Kk][Aa] ))|.+)(?:(?:.+)(?P<altitle>(?<= [Aa][Kk][Aa] ).+))?')
def fix_inconsistent_naming(title):
"""Fix titles with inconsistent naming using dictionary and sanitize them.
:param str title: original title.
:return: new title.
:rtype: str
"""
return _fix_inconsistent_naming(title, {"DC's Legends of Tomorrow": "Legends of Tomorrow",
"Marvel's Jessica Jones": "Jessica Jones"})
logger = logging.getLogger(__name__)
# Configure :mod:`rarfile` to use the same path separator as :mod:`zipfile`
rarfile.PATH_SEP = '/'
class TitrariSubtitle(Subtitle):
provider_name = 'titrari'
def __init__(self, language, download_link, sid, releases, title, imdb_id, year=None, download_count=None, comments=None):
super(TitrariSubtitle, self).__init__(language)
self.sid = sid
self.title = title
self.imdb_id = imdb_id
self.download_link = download_link
self.year = year
self.download_count = download_count
self.releases = self.release_info = releases
self.comments = comments
@property
def id(self):
return self.sid
def __str__(self):
return self.title + "(" + str(self.year) + ")" + " -> " + self.download_link
def __repr__(self):
return self.title + "(" + str(self.year) + ")"
def get_matches(self, video):
matches = set()
if isinstance(video, Movie):
# title
if video.title and sanitize(self.title) == fix_inconsistent_naming(video.title):
matches.add('title')
if video.year and self.year == video.year:
matches.add('year')
if video.imdb_id and self.imdb_id == video.imdb_id:
matches.add('imdb_id')
if video.release_group and video.release_group in self.comments:
matches.add('release_group')
if video.resolution and video.resolution.lower() in self.comments:
matches.add('resolution')
self.matches = matches
return matches
class TitrariProvider(Provider, ProviderSubtitleArchiveMixin):
subtitle_class = TitrariSubtitle
languages = {Language(l) for l in ['ron', 'eng']}
languages.update(set(Language.rebuild(l, forced=True) for l in languages))
api_url = 'https://www.titrari.ro/'
query_advanced_search = 'cautareavansata'
def __init__(self):
self.session = None
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
def terminate(self):
self.session.close()
def query(self, languages=None, title=None, imdb_id=None, video=None):
subtitles = []
params = self.getQueryParams(imdb_id, title)
search_response = self.session.get(self.api_url, params=params, timeout=15)
search_response.raise_for_status()
if not search_response.content:
logger.debug('[#### Provider: titrari.ro] No data returned from provider')
return []
soup = ParserBeautifulSoup(search_response.content.decode('utf-8', 'ignore'), ['lxml', 'html.parser'])
# loop over subtitle cells
rows = soup.select('td[rowspan=\'5\']')
for index, row in enumerate(rows):
result_anchor_el = row.select_one('a')
# Download link
href = result_anchor_el.get('href')
download_link = self.api_url + href
fullTitle = row.parent.find("h1").find("a").text
#Get title
try:
title = fullTitle.split("(")[0]
except:
logger.error("[#### Provider: titrari.ro] Error parsing title.")
# Get downloads count
try:
downloads = int(row.parent.parent.select("span")[index].text[12:])
except:
logger.error("[#### Provider: titrari.ro] Error parsing downloads.")
# Get year
try:
year = int(fullTitle.split("(")[1].split(")")[0])
except:
year = None
logger.error("[#### Provider: titrari.ro] Error parsing year.")
# Get imdbId
sub_imdb_id = self.getImdbIdFromSubtitle(row)
try:
comments = row.parent.parent.find_all("td", class_=re.compile("comment"))[index*2+1].text
except:
logger.error("Error parsing comments.")
subtitle = self.subtitle_class(next(iter(languages)), download_link, index, None, title, sub_imdb_id, year, downloads, comments)
logger.debug('[#### Provider: titrari.ro] Found subtitle %r', str(subtitle))
subtitles.append(subtitle)
ordered_subs = self.order(subtitles, video)
return ordered_subs
def order(self, subtitles, video):
logger.debug("[#### Provider: titrari.ro] Sorting by download count...")
sorted_subs = sorted(subtitles, key=lambda s: s.download_count, reverse=True)
return sorted_subs
def getImdbIdFromSubtitle(self, row):
try:
imdbId = row.parent.parent.find_all(src=re.compile("imdb"))[0].parent.get('href').split("tt")[-1]
except:
logger.error("[#### Provider: titrari.ro] Error parsing imdbId.")
if imdbId is not None:
return "tt" + imdbId
else:
return None
def getQueryParams(self, imdb_id, title):
queryParams = {
'page': self.query_advanced_search,
'z8': '1'
}
if imdb_id is not None:
queryParams["z5"] = imdb_id
elif title is not None:
queryParams["z7"] = title
return queryParams
def list_subtitles(self, video, languages):
title = fix_inconsistent_naming(video.title)
imdb_id = None
try:
imdb_id = video.imdb_id[2:]
except:
logger.error("[#### Provider: titrari.ro] Error parsing video.imdb_id.")
return [s for s in
self.query(languages, title, imdb_id, video)]
def download_subtitle(self, subtitle):
r = self.session.get(subtitle.download_link, headers={'Referer': self.api_url}, timeout=10)
r.raise_for_status()
# open the archive
archive_stream = io.BytesIO(r.content)
if is_rarfile(archive_stream):
logger.debug('[#### Provider: titrari.ro] Archive identified as rar')
archive = RarFile(archive_stream)
elif is_zipfile(archive_stream):
logger.debug('[#### Provider: titrari.ro] Archive identified as zip')
archive = ZipFile(archive_stream)
else:
subtitle.content = r.content
if subtitle.is_valid():
return
subtitle.content = None
raise ProviderError('[#### Provider: titrari.ro] Unidentified archive type')
subtitle.content = self.get_subtitle_from_archive(subtitle, archive)

View File

@ -244,7 +244,7 @@ class TitulkyProvider(Provider):
for sub in subs: for sub in subs:
page_link = '%s%s' % (self.server_url, sub.a.get('href').encode('utf-8')) page_link = '%s%s' % (self.server_url, sub.a.get('href').encode('utf-8'))
title = sub.find_all('td')[0:1] title = sub.find_all('td')[0:1]
title = [x.text.encode('utf-8') for x in title] title = [x.text for x in title]
version = sub.find(class_="fixedTip") version = sub.find(class_="fixedTip")
if version is None: if version is None:
version = "" version = ""
@ -316,13 +316,12 @@ class TitulkyProvider(Provider):
elif 'Limit vyčerpán' in r.text: elif 'Limit vyčerpán' in r.text:
raise DownloadLimitExceeded raise DownloadLimitExceeded
soup = ParserBeautifulSoup(r.text.decode('utf-8', 'ignore'), ['lxml', 'html.parser']) soup = ParserBeautifulSoup(r.text, ['lxml', 'html.parser'])
# links = soup.find("a", {"id": "downlink"}).find_all('a') # links = soup.find("a", {"id": "downlink"}).find_all('a')
link = soup.find(id="downlink") link = soup.find(id="downlink")
# TODO: add settings for choice # TODO: add settings for choice
url = link.get('href') url = self.dn_url + link.get('href')
url = self.dn_url + url
time.sleep(0.5) time.sleep(0.5)
r = self.session.get(url, headers={'Referer': subtitle.download_link}, r = self.session.get(url, headers={'Referer': subtitle.download_link},
timeout=30) timeout=30)

View File

@ -358,6 +358,15 @@ class ModifiedSubtitle(Subtitle):
id = None id = None
MERGED_FORMATS = {
"TV": ("HDTV", "SDTV", "AHDTV", "UHDTV"),
"Air": ("SATRip", "DVB", "PPV"),
"Disk": ("DVD", "HD-DVD", "BluRay")
}
MERGED_FORMATS_REV = dict((v.lower(), k.lower()) for k in MERGED_FORMATS for v in MERGED_FORMATS[k])
def guess_matches(video, guess, partial=False): def guess_matches(video, guess, partial=False):
"""Get matches between a `video` and a `guess`. """Get matches between a `video` and a `guess`.
@ -386,12 +395,15 @@ def guess_matches(video, guess, partial=False):
for title in titles: for title in titles:
if sanitize(title) in (sanitize(name) for name in [video.series] + video.alternative_series): if sanitize(title) in (sanitize(name) for name in [video.series] + video.alternative_series):
matches.add('series') matches.add('series')
# title # title
if video.title and 'episode_title' in guess and sanitize(guess['episode_title']) == sanitize(video.title): if video.title and 'episode_title' in guess and sanitize(guess['episode_title']) == sanitize(video.title):
matches.add('title') matches.add('title')
# season # season
if video.season and 'season' in guess and guess['season'] == video.season: if video.season and 'season' in guess and guess['season'] == video.season:
matches.add('season') matches.add('season')
# episode # episode
# Currently we only have single-ep support (guessit returns a multi-ep as a list with int values) # Currently we only have single-ep support (guessit returns a multi-ep as a list with int values)
# Most providers only support single-ep, so make sure it contains only 1 episode # Most providers only support single-ep, so make sure it contains only 1 episode
@ -401,12 +413,15 @@ def guess_matches(video, guess, partial=False):
episode = min(episode_guess) if episode_guess and isinstance(episode_guess, list) else episode_guess episode = min(episode_guess) if episode_guess and isinstance(episode_guess, list) else episode_guess
if episode == video.episode: if episode == video.episode:
matches.add('episode') matches.add('episode')
# year # year
if video.year and 'year' in guess and guess['year'] == video.year: if video.year and 'year' in guess and guess['year'] == video.year:
matches.add('year') matches.add('year')
# count "no year" as an information # count "no year" as an information
if not partial and video.original_series and 'year' not in guess: if not partial and video.original_series and 'year' not in guess:
matches.add('year') matches.add('year')
elif isinstance(video, Movie): elif isinstance(video, Movie):
# year # year
if video.year and 'year' in guess and guess['year'] == video.year: if video.year and 'year' in guess and guess['year'] == video.year:
@ -440,21 +455,25 @@ def guess_matches(video, guess, partial=False):
formats = [formats] formats = [formats]
if video.format: if video.format:
video_format = video.format video_format = video.format.lower()
if video_format in ("HDTV", "SDTV", "TV"): _video_gen_format = MERGED_FORMATS_REV.get(video_format)
video_format = "TV" if _video_gen_format:
logger.debug("Treating HDTV/SDTV the same") logger.debug("Treating %s as %s the same", video_format, _video_gen_format)
for frmt in formats: for frmt in formats:
if frmt in ("HDTV", "SDTV"): _guess_gen_frmt = MERGED_FORMATS_REV.get(frmt.lower())
frmt = "TV"
if frmt.lower() == video_format.lower(): if _guess_gen_frmt == _video_gen_format:
matches.add('format') matches.add('format')
break break
if "release_group" in matches and "format" not in matches:
logger.info("Release group matched but format didn't. Remnoving release group match.")
matches.remove("release_group")
# video_codec # video_codec
if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec: if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec:
matches.add('video_codec') matches.add('video_codec')
# audio_codec # audio_codec
if video.audio_codec and 'audio_codec' in guess and guess['audio_codec'] == video.audio_codec: if video.audio_codec and 'audio_codec' in guess and guess['audio_codec'] == video.audio_codec:
matches.add('audio_codec') matches.add('audio_codec')

View File

@ -84,10 +84,11 @@ def _get_localzone(_root='/'):
if not etctz: if not etctz:
continue continue
tz = pytz.timezone(etctz.replace(' ', '_')) tz = pytz.timezone(etctz.replace(' ', '_'))
if _root == '/': # Disabling this offset valdation due to issue with some timezone: https://github.com/regebro/tzlocal/issues/80
# if _root == '/':
# We are using a file in etc to name the timezone. # We are using a file in etc to name the timezone.
# Verify that the timezone specified there is actually used: # Verify that the timezone specified there is actually used:
utils.assert_tz_offset(tz) # utils.assert_tz_offset(tz)
return tz return tz
except IOError: except IOError:
@ -138,7 +139,7 @@ def _get_localzone(_root='/'):
if os.path.exists(tzpath) and os.path.islink(tzpath): if os.path.exists(tzpath) and os.path.islink(tzpath):
tzpath = os.path.realpath(tzpath) tzpath = os.path.realpath(tzpath)
start = tzpath.find("/")+1 start = tzpath.find("/")+1
while start is not 0: while start != 0:
tzpath = tzpath[start:] tzpath = tzpath[start:]
try: try:
return pytz.timezone(tzpath) return pytz.timezone(tzpath)

View File

@ -89,7 +89,7 @@
</div> </div>
</td> </td>
<td> <td>
% upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['video_path']), ('score', row['score'])]) % upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['video_path']), ('score', row['score']), ('languages', row['languages'])])
% if upgradable_criteria in upgradable_movies: % if upgradable_criteria in upgradable_movies:
% if row['languages'] != "None": % if row['languages'] != "None":
% desired_languages = ast.literal_eval(str(row['languages'])) % desired_languages = ast.literal_eval(str(row['languages']))

View File

@ -104,7 +104,7 @@
</div> </div>
</td> </td>
<td> <td>
% upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['path']), ('score', row['score'])]) % upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['path']), ('score', row['score']), ('languages', row['languages'])])
% if upgradable_criteria in upgradable_episodes: % if upgradable_criteria in upgradable_episodes:
% if row['languages'] != "None": % if row['languages'] != "None":
% desired_languages = ast.literal_eval(str(row['languages'])) % desired_languages = ast.literal_eval(str(row['languages']))

View File

@ -233,14 +233,14 @@
% if PY2: % if PY2:
<div class='ui left aligned grid'> <div class='ui left aligned grid'>
<div class='fluid column'> <div class='fluid column'>
<div class="ui yellow icon message"> <div class="ui red icon message">
<i class="python icon"></i> <i class="python icon"></i>
<div class="content"> <div class="content">
<div class="header">Python deprecation warning</div> <div class="header">Python deprecation warning</div>
Bazarr is now compatible with Python 3.6 and newer. You should upgrade Python as we'll drop support for Python 2.7.x by the end of January 2020. Bazarr is now compatible with Python 3.6 and newer. You must upgrade Python as we don't support Python 2.x anymore.
<div class="ui bulleted list"> <div class="ui bulleted list">
% if os.name == 'posix': % if os.name == 'posix':
<div class="item">If you are running under Docker, don't worry, we'll take care of this for you. Just pull the new image that should be available within a couple of days.</div> <div class="item">If you are running under Docker, don't worry, we'll take care of this for you. Just pull the new image.</div>
% end % end
% if os.name == 'nt': % if os.name == 'nt':
<div class="item">If you have installed using the Windows Installer, just download the new installer that will upgrade your current installation (make sure to not change installation directory).</div> <div class="item">If you have installed using the Windows Installer, just download the new installer that will upgrade your current installation (make sure to not change installation directory).</div>

View File

@ -144,6 +144,22 @@
</div> </div>
</div> </div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>BSplayer</label>
</div>
<div class="one wide column">
<div id="bsplayer" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="bsplayer_option" class="ui grid container">
</div>
<div class="middle aligned row"> <div class="middle aligned row">
<div class="right aligned four wide column"> <div class="right aligned four wide column">
<label>GreekSubtitles</label> <label>GreekSubtitles</label>
@ -210,6 +226,47 @@
</div> </div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>LegendasDivx</label>
</div>
<div class="one wide column">
<div id="legendasdivx" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Brazilian & Portuguese Subtitles Provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="legendasdivx_option" class="ui grid container">
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Username</label>
</div>
<div class="six wide column">
<div class="ui fluid input">
<input name="settings_legendasdivx_username" type="text" value="{{settings.legendasdivx.username if settings.legendasdivx.username != None else ''}}">
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Password</label>
</div>
<div class="six wide column">
<div class="ui fluid input">
<input name="settings_legendasdivx_password" type="password" value="{{settings.legendasdivx.password if settings.legendasdivx.password != None else ''}}">
</div>
</div>
</div>
</div>
<div class="middle aligned row"> <div class="middle aligned row">
<div class="right aligned four wide column"> <div class="right aligned four wide column">
<label>LegendasTV</label> <label>LegendasTV</label>

View File

@ -45,7 +45,7 @@
% include('menu.tpl') % include('menu.tpl')
<div id="fondblanc" class="ui container"> <div id="fondblanc" class="ui container">
<form name="settings_form" id="settings_form" action="{{base_url}}save_settings" method="post" class="ui form" autocomplete="off"> <form name="settings_form" id="settings_form" action="{{base_url}}save_settings" method="post" class="ui form" autocomplete="off" enctype="multipart/form-data">
<div id="form_validation_error" class="ui error message"> <div id="form_validation_error" class="ui error message">
<p>Some fields are in error and you can't save settings until you have corrected them. Be sure to check in every tabs.</p> <p>Some fields are in error and you can't save settings until you have corrected them. Be sure to check in every tabs.</p>
</div> </div>

View File

@ -538,7 +538,7 @@
</div> </div>
</div> </div>
<div class="ui dividing header">Post-processing</div> <div class="ui dividing header">Post-Processing</div>
<div class="twelve wide column"> <div class="twelve wide column">
<div class="ui orange message"> <div class="ui orange message">
<p>Be aware that the execution of post-processing command will prevent the user interface from being accessible until completion, when downloading subtitles in interactive mode (meaning you'll see a loader during post-processing).</p> <p>Be aware that the execution of post-processing command will prevent the user interface from being accessible until completion, when downloading subtitles in interactive mode (meaning you'll see a loader during post-processing).</p>
@ -546,7 +546,7 @@
<div class="ui grid"> <div class="ui grid">
<div class="middle aligned row"> <div class="middle aligned row">
<div class="right aligned four wide column"> <div class="right aligned four wide column">
<label>Use post-processing</label> <label>Use Post-Processing</label>
</div> </div>
<div class="one wide column"> <div class="one wide column">
<div id="settings_use_postprocessing" class="ui toggle checkbox" data-postprocessing={{settings.general.getboolean('use_postprocessing')}}> <div id="settings_use_postprocessing" class="ui toggle checkbox" data-postprocessing={{settings.general.getboolean('use_postprocessing')}}>
@ -893,4 +893,4 @@
} }
$( "#settings_auth_apikey" ).val( result ); $( "#settings_auth_apikey" ).val( result );
} }
</script> </script>

View File

@ -207,12 +207,12 @@
$.getJSON("{{base_url}}test_url/" + protocol + "/" + encodeURIComponent(radarr_url), function (data) { $.getJSON("{{base_url}}test_url/" + protocol + "/" + encodeURIComponent(radarr_url), function (data) {
if (data.status) { if (data.status) {
$('#radarr_validated').checkbox('check'); $('#radarr_validated').checkbox('check');
$('#radarr_validation_result').text('Test successful: Radarr v' + data.version).css('color', 'green'); $('#radarr_validation_result').text('Test Successful: Radarr v' + data.version).css('color', 'green');
$('.form').form('validate form'); $('.form').form('validate form');
$('#loader').removeClass('active'); $('#loader').removeClass('active');
} else { } else {
$('#radarr_validated').checkbox('uncheck'); $('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('Test failed').css('color', 'red'); $('#radarr_validation_result').text('Test Failed').css('color', 'red');
$('.form').form('validate form'); $('.form').form('validate form');
$('#loader').removeClass('active'); $('#loader').removeClass('active');
} }

View File

@ -3,7 +3,7 @@
<div class="ui grid"> <div class="ui grid">
<div class="middle aligned row"> <div class="middle aligned row">
<div class="right aligned four wide column"> <div class="right aligned four wide column">
<label>Search for missing Subtitles frequency (in hours)</label> <label>Search for Missing Subtitles Frequency (In Hours)</label>
</div> </div>
<div class="five wide column"> <div class="five wide column">
<div class='field'> <div class='field'>
@ -18,7 +18,7 @@
<div class="middle aligned row"> <div class="middle aligned row">
<div class="right aligned four wide column"> <div class="right aligned four wide column">
<label>Use Scene name when available</label> <label>Use Scene Name When Available</label>
</div> </div>
<div class="one wide column"> <div class="one wide column">
<div id="settings_scenename" class="ui toggle checkbox" data-scenename={{settings.general.getboolean('use_scenename')}}> <div id="settings_scenename" class="ui toggle checkbox" data-scenename={{settings.general.getboolean('use_scenename')}}>