summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorqxe5534 <[email protected]>2020-01-20 10:25:15 +0200
committerqxe5534 <[email protected]>2020-01-20 10:25:15 +0200
commit517454b2df3a5a23de4ce8621be306907f727007 (patch)
tree9534d53c67ad5fa1fd03cc3a839f806f3c6d4b49
parentd8050f792adbf8634e8ac8f7d59b92322ea8631f (diff)
parent3fa13b890fe02a3d5ec38ef277bab14e34e101e5 (diff)
downloadbazarr-517454b2df3a5a23de4ce8621be306907f727007.tar.gz
bazarr-517454b2df3a5a23de4ce8621be306907f727007.zip
Merge branch 'development' into feature/provider-titrari-ro
# Conflicts: # bazarr/get_providers.py
-rw-r--r--README.md2
-rw-r--r--bazarr.py115
-rw-r--r--bazarr/config.py4
-rw-r--r--bazarr/get_providers.py5
-rw-r--r--bazarr/get_series.py48
-rw-r--r--bazarr/get_subtitle.py97
-rw-r--r--bazarr/main.py68
-rw-r--r--libs/subliminal_patch/core.py4
-rw-r--r--libs/subliminal_patch/providers/bsplayer.py235
-rw-r--r--libs/subliminal_patch/providers/legendasdivx.py307
-rw-r--r--libs/subliminal_patch/providers/subdivx.py4
-rw-r--r--libs/subliminal_patch/subtitle.py33
-rw-r--r--libs/tzlocal/unix.py2
-rw-r--r--views/providers.tpl57
14 files changed, 854 insertions, 127 deletions
diff --git a/README.md b/README.md
index 03fe24a62..077eeecbf 100644
--- a/README.md
+++ b/README.md
@@ -45,8 +45,10 @@ If you need something that is not already part of Bazarr, feel free to create a
* Argenteam
* Assrt
* BetaSeries
+* BSPlayer
* GreekSubtitles
* Hosszupuska
+* LegendasDivx
* LegendasTV
* Napiprojekt
* Napisy24
diff --git a/bazarr.py b/bazarr.py
index ae5d88f13..f9228e2be 100644
--- a/bazarr.py
+++ b/bazarr.py
@@ -11,6 +11,7 @@ import os
import sys
import platform
import re
+import signal
from bazarr.get_args import args
@@ -39,15 +40,97 @@ check_python_version()
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:]
ep = sp.Popen(script, stdout=sp.PIPE, stderr=sp.STDOUT, stdin=sp.PIPE)
+ process_registry.register(ep)
print("Bazarr starting...")
try:
while True:
line = ep.stdout.readline()
if line == '' or not line:
+ # Process ended so let's unregister it
+ process_registry.unregister(ep)
break
if PY3:
sys.stdout.buffer.write(line)
@@ -73,7 +156,7 @@ if __name__ == '__main__':
pass
- def daemon():
+ def daemon(bazarr_runner = lambda: start_bazarr()):
if os.path.exists(stopfile):
try:
os.remove(stopfile)
@@ -89,12 +172,30 @@ if __name__ == '__main__':
except:
print('Unable to delete restart file.')
else:
- start_bazarr()
+ bazarr_runner()
+
+ bazarr_runner = lambda: start_bazarr()
+
+ should_stop = lambda: False
+
+ 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)
- start_bazarr()
+ bazarr_runner()
- # Keep the script running forever.
- while True:
- daemon()
+ # Keep the script running forever until stop is requested through term or keyboard interrupt
+ while not should_stop():
+ daemon(bazarr_runner)
time.sleep(1)
diff --git a/bazarr/config.py b/bazarr/config.py
index ca61871e0..b64040ebf 100644
--- a/bazarr/config.py
+++ b/bazarr/config.py
@@ -102,6 +102,10 @@ defaults = {
'password': '',
'random_agents': 'True'
},
+ 'legendasdivx': {
+ 'username': '',
+ 'password': ''
+ },
'legendastv': {
'username': '',
'password': ''
diff --git a/bazarr/get_providers.py b/bazarr/get_providers.py
index 3e1507138..90f28a1f8 100644
--- a/bazarr/get_providers.py
+++ b/bazarr/get_providers.py
@@ -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", "titrari", "argenteam", "assrt", "subscene"]
throttle_count = {}
@@ -114,6 +114,9 @@ def get_providers_auth():
'password': settings.subscene.password,
'only_foreign': False, # fixme
},
+ 'legendasdivx': {'username': settings.legendasdivx.username,
+ 'password': settings.legendasdivx.password,
+ },
'legendastv': {'username': settings.legendastv.username,
'password': settings.legendastv.password,
},
diff --git a/bazarr/get_series.py b/bazarr/get_series.py
index c909dda09..d34a509b5 100644
--- a/bazarr/get_series.py
+++ b/bazarr/get_series.py
@@ -62,19 +62,21 @@ def update_series():
seriesListLength = 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=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 'overview' in show:
+ overview = show['overview']
+ else:
+ overview = ''
+
+ 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]
if show['alternateTitles'] != None:
alternateTitles = str([item['title'] for item in show['alternateTitles']])
@@ -84,18 +86,18 @@ def update_series():
# Add shows in Sonarr to current shows list
current_shows_sonarr.append(show['id'])
- if show['tvdbId'] in current_shows_db_list:
- series_to_update.append({'title': six.text_type(show["title"]),
- 'path': six.text_type(show["path"]),
+ if show['id'] in current_shows_db_list:
+ series_to_update.append({'title': show["title"],
+ 'path': 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)})
+ 'overview': overview,
+ 'poster': poster,
+ 'fanart': fanart,
+ 'audio_language': profile_id_to_language((show['qualityProfileId'] if get_sonarr_version().startswith('2') else show['languageProfileId']), audio_profiles),
+ 'sortTitle': show['sortTitle'],
+ 'year': show['year'],
+ 'alternateTitles': alternateTitles})
else:
if serie_default_enabled is True:
series_to_add.append({'title': show["title"],
diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py
index 81d2e8a6e..698afd853 100644
--- a/bazarr/get_subtitle.py
+++ b/bazarr/get_subtitle.py
@@ -40,6 +40,7 @@ from analytics import track_event
import six
from six.moves import range
from functools import reduce
+from locale import getpreferredencoding
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,
downloaded_language_code2, downloaded_language_code3,
subtitle.language.forced)
- try:
- 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)
+ postprocessing(command, path)
# fixme: support multiple languages at once
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,
downloaded_language_code2, downloaded_language_code3,
subtitle.language.forced)
- try:
- 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)
+ postprocessing(command, path)
if media_type == 'series':
reversed_path = path_replace_reverse(path)
@@ -1173,3 +1120,41 @@ def upgrade_subtitles():
store_subtitles_movie(movie['video_path'], path_replace_movie(movie['video_path']))
history_log_movie(3, movie['radarrId'], message, path, language_code, provider, score)
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)
diff --git a/bazarr/main.py b/bazarr/main.py
index 696bc6f68..2115e2976 100644
--- a/bazarr/main.py
+++ b/bazarr/main.py
@@ -1,6 +1,6 @@
# coding=utf-8
-bazarr_version = '0.8.4'
+bazarr_version = '0.8.4.1'
import os
os.environ["SZ_USER_AGENT"] = "Bazarr/1"
@@ -398,6 +398,8 @@ def save_wizard():
settings.addic7ed.password = request.forms.get('settings_addic7ed_password')
settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents)
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.password = request.forms.get('settings_legendastv_password')
settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username')
@@ -1527,6 +1529,8 @@ def save_settings():
settings.addic7ed.password = request.forms.get('settings_addic7ed_password')
settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents)
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.password = request.forms.get('settings_legendastv_password')
settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username')
@@ -1749,9 +1753,9 @@ def execute_task(taskid):
@custom_auth_basic(check_credentials)
def remove_subtitles():
authorize()
- episodePath = request.forms.get('episodePath')
+ episodePath = request.forms.episodePath
language = request.forms.get('language')
- subtitlesPath = request.forms.get('subtitlesPath')
+ subtitlesPath = request.forms.subtitlesPath
sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
@@ -1768,9 +1772,9 @@ def remove_subtitles():
@custom_auth_basic(check_credentials)
def remove_subtitles_movie():
authorize()
- moviePath = request.forms.get('moviePath')
+ moviePath = request.forms.moviePath
language = request.forms.get('language')
- subtitlesPath = request.forms.get('subtitlesPath')
+ subtitlesPath = request.forms.subtitlesPath
radarrId = request.forms.get('radarrId')
try:
@@ -1788,14 +1792,14 @@ def get_subtitle():
authorize()
ref = request.environ['HTTP_REFERER']
- episodePath = request.forms.get('episodePath')
- sceneName = request.forms.get('sceneName')
+ episodePath = request.forms.episodePath
+ sceneName = request.forms.sceneName
language = request.forms.get('language')
hi = request.forms.get('hi')
forced = request.forms.get('forced')
sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
- title = request.forms.get('title')
+ title = request.forms.title
providers_list = get_providers()
providers_auth = get_providers_auth()
@@ -1823,12 +1827,12 @@ def get_subtitle():
def manual_search_json():
authorize()
- episodePath = request.forms.get('episodePath')
- sceneName = request.forms.get('sceneName')
+ episodePath = request.forms.episodePath
+ sceneName = request.forms.sceneName
language = request.forms.get('language')
hi = request.forms.get('hi')
forced = request.forms.get('forced')
- title = request.forms.get('title')
+ title = request.forms.title
providers_list = get_providers()
providers_auth = get_providers_auth()
@@ -1843,16 +1847,16 @@ def manual_get_subtitle():
authorize()
ref = request.environ['HTTP_REFERER']
- episodePath = request.forms.get('episodePath')
- sceneName = request.forms.get('sceneName')
+ episodePath = request.forms.episodePath
+ sceneName = request.forms.sceneName
language = request.forms.get('language')
hi = request.forms.get('hi')
forced = request.forms.get('forced')
selected_provider = request.forms.get('provider')
- subtitle = request.forms.get('subtitle')
+ subtitle = request.forms.subtitle
sonarrSeriesId = request.forms.get('sonarrSeriesId')
sonarrEpisodeId = request.forms.get('sonarrEpisodeId')
- title = request.forms.get('title')
+ title = request.forms.title
providers_auth = get_providers_auth()
@@ -1881,14 +1885,14 @@ def perform_manual_upload_subtitle():
authorize()
ref = request.environ['HTTP_REFERER']
- episodePath = request.forms.get('episodePath')
- sceneName = request.forms.get('sceneName')
+ episodePath = request.forms.episodePath
+ sceneName = request.forms.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')
+ title = request.forms.title
_, ext = os.path.splitext(upload.filename)
@@ -1925,13 +1929,13 @@ def get_subtitle_movie():
authorize()
ref = request.environ['HTTP_REFERER']
- moviePath = request.forms.get('moviePath')
- sceneName = request.forms.get('sceneName')
+ moviePath = request.forms.moviePath
+ sceneName = request.forms.sceneName
language = request.forms.get('language')
hi = request.forms.get('hi')
forced = request.forms.get('forced')
radarrId = request.forms.get('radarrId')
- title = request.forms.get('title')
+ title = request.forms.title
providers_list = get_providers()
providers_auth = get_providers_auth()
@@ -1959,12 +1963,12 @@ def get_subtitle_movie():
def manual_search_movie_json():
authorize()
- moviePath = request.forms.get('moviePath')
- sceneName = request.forms.get('sceneName')
+ moviePath = request.forms.moviePath
+ sceneName = request.forms.sceneName
language = request.forms.get('language')
hi = request.forms.get('hi')
forced = request.forms.get('forced')
- title = request.forms.get('title')
+ title = request.forms.title
providers_list = get_providers()
providers_auth = get_providers_auth()
@@ -1979,15 +1983,15 @@ def manual_get_subtitle_movie():
authorize()
ref = request.environ['HTTP_REFERER']
- moviePath = request.forms.get('moviePath')
- sceneName = request.forms.get('sceneName')
+ moviePath = request.forms.moviePath
+ sceneName = request.forms.sceneName
language = request.forms.get('language')
hi = request.forms.get('hi')
forced = request.forms.get('forced')
- selected_provider = request.forms.get('provider')
- subtitle = request.forms.get('subtitle')
+ selected_provider = request.forms.provider
+ subtitle = request.forms.subtitle
radarrId = request.forms.get('radarrId')
- title = request.forms.get('title')
+ title = request.forms.title
providers_auth = get_providers_auth()
@@ -2015,13 +2019,13 @@ def perform_manual_upload_subtitle_movie():
authorize()
ref = request.environ['HTTP_REFERER']
- moviePath = request.forms.get('moviePath')
- sceneName = request.forms.get('sceneName')
+ moviePath = request.forms.moviePath
+ sceneName = request.forms.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')
+ title = request.forms.title
_, ext = os.path.splitext(upload.filename)
diff --git a/libs/subliminal_patch/core.py b/libs/subliminal_patch/core.py
index d4685c289..38a603eed 100644
--- a/libs/subliminal_patch/core.py
+++ b/libs/subliminal_patch/core.py
@@ -543,6 +543,10 @@ def scan_video(path, dont_use_actual_file=False, hints=None, providers=None, ski
if video.size > 10485760:
logger.debug('Size is %d', video.size)
osub_hash = None
+
+ if "bsplayer" in providers:
+ video.hashes['bsplayer'] = osub_hash = hash_opensubtitles(hash_path)
+
if "opensubtitles" in providers:
video.hashes['opensubtitles'] = osub_hash = hash_opensubtitles(hash_path)
diff --git a/libs/subliminal_patch/providers/bsplayer.py b/libs/subliminal_patch/providers/bsplayer.py
new file mode 100644
index 000000000..9839a0331
--- /dev/null
+++ b/libs/subliminal_patch/providers/bsplayer.py
@@ -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')
+
+
diff --git a/libs/subliminal_patch/providers/legendasdivx.py b/libs/subliminal_patch/providers/legendasdivx.py
new file mode 100644
index 000000000..5537e5828
--- /dev/null
+++ b/libs/subliminal_patch/providers/legendasdivx.py
@@ -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')
diff --git a/libs/subliminal_patch/providers/subdivx.py b/libs/subliminal_patch/providers/subdivx.py
index ab7db33b6..74b3c4f7b 100644
--- a/libs/subliminal_patch/providers/subdivx.py
+++ b/libs/subliminal_patch/providers/subdivx.py
@@ -35,6 +35,10 @@ class SubdivxSubtitle(Subtitle):
def id(self):
return self.page_link
+ @property
+ def release_info(self):
+ return self.description
+
def get_matches(self, video):
matches = set()
diff --git a/libs/subliminal_patch/subtitle.py b/libs/subliminal_patch/subtitle.py
index 8116697bf..43e9a9716 100644
--- a/libs/subliminal_patch/subtitle.py
+++ b/libs/subliminal_patch/subtitle.py
@@ -358,6 +358,15 @@ class ModifiedSubtitle(Subtitle):
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):
"""Get matches between a `video` and a `guess`.
@@ -386,12 +395,15 @@ def guess_matches(video, guess, partial=False):
for title in titles:
if sanitize(title) in (sanitize(name) for name in [video.series] + video.alternative_series):
matches.add('series')
+
# title
if video.title and 'episode_title' in guess and sanitize(guess['episode_title']) == sanitize(video.title):
matches.add('title')
+
# season
if video.season and 'season' in guess and guess['season'] == video.season:
matches.add('season')
+
# episode
# 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
@@ -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
if episode == video.episode:
matches.add('episode')
+
# year
if video.year and 'year' in guess and guess['year'] == video.year:
matches.add('year')
+
# count "no year" as an information
if not partial and video.original_series and 'year' not in guess:
matches.add('year')
+
elif isinstance(video, Movie):
# 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]
if video.format:
- video_format = video.format
- if video_format in ("HDTV", "SDTV", "TV"):
- video_format = "TV"
- logger.debug("Treating HDTV/SDTV the same")
+ video_format = video.format.lower()
+ _video_gen_format = MERGED_FORMATS_REV.get(video_format)
+ if _video_gen_format:
+ logger.debug("Treating %s as %s the same", video_format, _video_gen_format)
for frmt in formats:
- if frmt in ("HDTV", "SDTV"):
- frmt = "TV"
+ _guess_gen_frmt = MERGED_FORMATS_REV.get(frmt.lower())
- if frmt.lower() == video_format.lower():
+ if _guess_gen_frmt == _video_gen_format:
matches.add('format')
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
if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec:
matches.add('video_codec')
+
# audio_codec
if video.audio_codec and 'audio_codec' in guess and guess['audio_codec'] == video.audio_codec:
matches.add('audio_codec')
diff --git a/libs/tzlocal/unix.py b/libs/tzlocal/unix.py
index 388273c27..8574965a5 100644
--- a/libs/tzlocal/unix.py
+++ b/libs/tzlocal/unix.py
@@ -138,7 +138,7 @@ def _get_localzone(_root='/'):
if os.path.exists(tzpath) and os.path.islink(tzpath):
tzpath = os.path.realpath(tzpath)
start = tzpath.find("/")+1
- while start is not 0:
+ while start != 0:
tzpath = tzpath[start:]
try:
return pytz.timezone(tzpath)
diff --git a/views/providers.tpl b/views/providers.tpl
index 2eada8f3d..8d677ffda 100644
--- a/views/providers.tpl
+++ b/views/providers.tpl
@@ -146,6 +146,22 @@
<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="right aligned four wide column">
<label>GreekSubtitles</label>
</div>
<div class="one wide column">
@@ -212,6 +228,47 @@
<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="right aligned four wide column">
<label>LegendasTV</label>
</div>
<div class="one wide column">