Compare commits

..

1 Commits

Author SHA1 Message Date
panni f8f99f0fb2 submod retry; WIP 2019-05-19 06:03:55 +02:00
27 changed files with 240 additions and 620 deletions
-51
View File
@@ -1,54 +1,3 @@
2.6.5.3109
subscene, addic7ed and titlovi
- either of those providers might impose a reCAPTCHA verification. In order to use those providers, please create an account at an AntiCaptcha service ([anti-captcha.com](http://getcaptchasolution.com/kkvviom7nh) or [deathbycaptcha.com](http://deathbycaptcha.com)), add funds, then supply your credentials/apikey in the configuration
Changelog
- providers: add Napisy24 (polish)
- providers: subscene: reduce provider load by possibly half
- providers: subscene: support logging in (username/password are now required)
- providers: subscene: fallback to non year results if none found with year
2.6.5.3099
subscene, addic7ed and titlovi
- either of those providers might impose a reCAPTCHA verification. In order to use those providers, please create an account at an AntiCaptcha service ([anti-captcha.com](http://getcaptchasolution.com/kkvviom7nh) or [deathbycaptcha.com](http://deathbycaptcha.com)), add funds, then supply your credentials/apikey in the configuration
Changelog
- core: allow system DNS again by putting "system" as the DNS
- providers: subscene: fix again (subscene, contact us please, so we can end this)
2.6.5.3092
subscene, addic7ed and titlovi
- either of those providers might impose a reCAPTCHA verification. In order to use those providers, please create an account at an AntiCaptcha service ([anti-captcha.com](http://getcaptchasolution.com/kkvviom7nh) or [deathbycaptcha.com](http://deathbycaptcha.com)), add funds, then supply your credentials/apikey in the configuration
Changelog
- providers: subscene: fix endpoint (hopefully for longer now)
- providers: subscene: don't search for season packs (broken for now; relieves 50% of server load on provider)
- providers: subscene: don't calculate video fn for now
- providers: argenteam: backport fixes from bazarr
- subtitle: try decoding with utf-16 by default as well (zho/farsi)
- submod: HI: remove music tags by default
- core: compat (bazarr): add env var SZ_KEEP_ENCODING to keep encoding of subtitles
2.6.5.3074
Changelog
- core: cf: bypass cf 95% of the time without captchas
- core: fix breaking line endings of certain languages (chinese, UTF-16); fixes #646
- core: update pysubs2 to 0.2.3
2.6.5.3062
Changelog
- core: cf: optimize
- core: http: don't query DNS with IPs. thanks @fgump (fixes sonarr/radarr)
2.6.5.3041
+1 -5
View File
@@ -119,23 +119,19 @@ def agent_extract_embedded(video_part_map):
for plexapi_part in get_all_parts(plexapi_item):
item_count = item_count + 1
used_one_unknown_stream = False
used_one_known_stream = False
for requested_language in config.lang_list:
skip_unknown = used_one_unknown_stream or used_one_known_stream
embedded_subs = stored_subs.get_by_provider(plexapi_part.id, requested_language, "embedded")
current = stored_subs.get_any(plexapi_part.id, requested_language) or \
requested_language in scanned_video.external_subtitle_languages
if not embedded_subs:
stream_data = get_embedded_subtitle_streams(plexapi_part, requested_language=requested_language,
skip_unknown=skip_unknown)
skip_unknown=used_one_unknown_stream)
if stream_data:
stream = stream_data[0]["stream"]
if stream_data[0]["is_unknown"]:
used_one_unknown_stream = True
else:
used_one_known_stream = True
to_extract.append(({scanned_video: part_info}, plexapi_part, str(stream.index),
str(requested_language), not current))
+2 -11
View File
@@ -761,7 +761,6 @@ class Config(object):
return {'opensubtitles': cast_bool(Prefs['provider.opensubtitles.enabled']),
# 'thesubdb': Prefs['provider.thesubdb.enabled'],
'podnapisi': cast_bool(Prefs['provider.podnapisi.enabled']),
'napisy24': cast_bool(Prefs['provider.napisy24.enabled']),
'titlovi': cast_bool(Prefs['provider.titlovi.enabled']),
'addic7ed': cast_bool(Prefs['provider.addic7ed.enabled']) and self.has_anticaptcha,
'tvsubtitles': cast_bool(Prefs['provider.tvsubtitles.enabled']),
@@ -802,7 +801,6 @@ class Config(object):
providers["argenteam"] = False
providers["assrt"] = False
providers["subscene"] = False
providers["napisy24"] = False
providers_forced_off = dict(providers)
if not self.unrar and providers["legendastv"]:
@@ -866,14 +864,8 @@ class Config(object):
'only_foreign': self.forced_only,
'also_foreign': self.forced_also,
},
'napisy24': {
'username': Prefs['provider.napisy24.username'],
'password': Prefs['provider.napisy24.password'],
},
'subscene': {
'only_foreign': self.forced_only,
'username': Prefs['provider.subscene.username'],
'password': Prefs['provider.subscene.password'],
},
'legendastv': {'username': Prefs['provider.legendastv.username'],
'password': Prefs['provider.legendastv.password'],
@@ -1092,12 +1084,11 @@ class Config(object):
def parse_custom_dns(self):
custom_dns = Prefs['use_custom_dns2'].strip()
os.environ["dns_resolvers"] = ""
if custom_dns and custom_dns != "system":
if custom_dns:
ips = filter(lambda x: x, [d.strip() for d in custom_dns.split(",")])
if ips:
os.environ["dns_resolvers"] = json.dumps(ips)
return os.environ["dns_resolvers"]
return os.environ["dns_resolvers"]
def init_subliminal_patches(self):
# configure custom subtitle destination folders for scanning pre-existing subs
+1 -1
View File
@@ -394,7 +394,7 @@ def get_language_from_stream(lang_code):
return Language.fromietf(lang)
elif lang:
try:
return language_from_stream(lang_code)
return language_from_stream(lang)
except LanguageError:
pass
+10 -24
View File
@@ -177,7 +177,6 @@ def get_all_parts(plex_item):
def get_embedded_subtitle_streams(part, requested_language=None, skip_duplicate_unknown=True, skip_unknown=False):
streams = []
streams_unknown = []
all_streams = []
has_unknown = False
found_requested_language = False
for stream in part.streams:
@@ -190,40 +189,27 @@ def get_embedded_subtitle_streams(part, requested_language=None, skip_duplicate_
is_unknown = False
found_requested_language = requested_language and requested_language == language
stream_data = None
if not language:
if not language and config.treat_und_as_first:
# only consider first unknown subtitle stream
if requested_language and config.treat_und_as_first:
if has_unknown and skip_duplicate_unknown:
Log.Debug("skipping duplicate unknown")
continue
if has_unknown and skip_duplicate_unknown:
continue
language = Language.rebuild(list(config.lang_list)[0], forced=is_forced)
else:
language = Language("unk")
language = Language.rebuild(list(config.lang_list)[0], forced=is_forced)
is_unknown = True
has_unknown = True
stream_data = {"stream": stream, "is_unknown": is_unknown, "language": language,
"is_forced": is_forced}
streams_unknown.append(stream_data)
streams_unknown.append({"stream": stream, "is_unknown": is_unknown, "language": language,
"is_forced": is_forced})
if not requested_language or found_requested_language:
stream_data = {"stream": stream, "is_unknown": is_unknown, "language": language,
"is_forced": is_forced}
streams.append(stream_data)
streams.append({"stream": stream, "is_unknown": is_unknown, "language": language,
"is_forced": is_forced})
if found_requested_language:
break
if stream_data:
all_streams.append(stream_data)
if requested_language:
if streams_unknown and not found_requested_language and not skip_unknown:
streams = streams_unknown
else:
streams = all_streams
if streams_unknown and not found_requested_language and not skip_unknown:
streams = streams_unknown
return streams
+1 -35
View File
@@ -335,26 +335,6 @@
"type": "bool",
"default": "true"
},
{
"id": "provider.napisy24.enabled",
"label": "Provider: Enable Napisy24 (pl)",
"type": "bool",
"default": "false"
},
{
"id": "provider.napisy24.username",
"label": "Napisy24 Username",
"type": "text",
"default": ""
},
{
"id": "provider.napisy24.password",
"label": "Napisy24 Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.addic7ed.enabled",
"label": "Provider: Enable Addic7ed (needs AntiCaptcha)",
@@ -451,20 +431,6 @@
"type": "bool",
"default": "true"
},
{
"id": "provider.subscene.username",
"label": "SubScene Username",
"type": "text",
"default": ""
},
{
"id": "provider.subscene.password",
"label": "SubScene Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.supersubtitles.enabled",
"label": "Provider: Enable feliratok.info (Hungarian)",
@@ -895,7 +861,7 @@
},
{
"id": "use_custom_dns2",
"label": "Use custom DNS (IPs, comma-separated, set to 'system' for system DNS. Default: Google/CF)",
"label": "Use custom DNS (IPs, comma-separated, leave empty for system DNS. Default: Google/CF)",
"type": "text",
"default": "1.1.1.1, 8.8.8.8"
},
+3 -3
View File
@@ -13,7 +13,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2.6.5.3124</string>
<string>2.6.5.3074</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexPluginClass</key>
@@ -23,7 +23,7 @@
<key>PlexPluginConsoleLogging</key>
<string>0</string>
<key>PlexPluginDevMode</key>
<string>0</string>
<string>1</string>
<key>PlexPluginCodePolicy</key>
<!-- this allows channels to access some python methods which are otherwise blocked, as well as import external code libraries, and interact with the PMS HTTP API -->
<string>Elevated</string>
@@ -32,7 +32,7 @@
&lt;h1&gt;Sub-Zero for Plex&lt;/h1&gt;&lt;i&gt;Subtitles done right&lt;/i&gt;
Version 2.6.5.3124
Version 2.6.5.3074 DEV
Originally based on @bramwalet's awesome &lt;a href=&quot;https://github.com/bramwalet/Subliminal.bundle&quot;&gt;Subliminal.bundle&lt;/a&gt;
@@ -12,13 +12,6 @@ from_subscene = {
'Malay': 'msa', 'Pashto': 'pus', 'Punjabi': 'pan', 'Swahili': 'swa'
}
from_subscene_with_country = {
'Brazillian Portuguese': ('por', 'BR')
}
to_subscene_with_country = {val: key for key, val in from_subscene_with_country.items()}
to_subscene = {v: k for k, v in from_subscene.items()}
exact_languages_alpha3 = [
@@ -41,12 +34,12 @@ language_ids = {
'mkd': 48, 'mal': 64, 'mni': 65, 'mon': 72, 'pus': 67, 'pol': 31,
'por': 32, 'pan': 66, 'rus': 34, 'srp': 35, 'sin': 58, 'slk': 36,
'slv': 37, 'som': 70, 'tgl': 53, 'tam': 59, 'tel': 63, 'tha': 40,
'tur': 41, 'ukr': 56, 'urd': 42, 'yor': 71, 'pt-BR': 4
'tur': 41, 'ukr': 56, 'urd': 42, 'yor': 71
}
# TODO: specify codes for unspecified_languages
unspecified_languages = [
'Big 5 code', 'Bulgarian/ English',
'Big 5 code', 'Brazillian Portuguese', 'Bulgarian/ English',
'Chinese BG code', 'Dutch/ English', 'English/ German',
'Hungarian/ English', 'Rohingya'
]
@@ -57,8 +50,6 @@ alpha3_of_code = {l.name: l.alpha3 for l in supported_languages}
supported_languages.update({Language(l) for l in to_subscene})
supported_languages.update({Language(lang, cr) for lang, cr in to_subscene_with_country})
class SubsceneConverter(LanguageReverseConverter):
codes = {l.name for l in supported_languages}
@@ -70,15 +61,9 @@ class SubsceneConverter(LanguageReverseConverter):
if alpha3 in to_subscene:
return to_subscene[alpha3]
if (alpha3, country) in to_subscene_with_country:
return to_subscene_with_country[(alpha3, country)]
raise ConfigurationError('Unsupported language for subscene: %s, %s, %s' % (alpha3, country, script))
def reverse(self, code):
if code in from_subscene_with_country:
return from_subscene_with_country[code]
if code in from_subscene:
return (from_subscene[code],)
@@ -309,8 +309,7 @@ class SZProviderPool(ProviderPool):
logger.error('Invalid subtitle')
return False
if not os.environ.get("SZ_KEEP_ENCODING", False):
subtitle.normalize()
subtitle.normalize()
return True
@@ -473,7 +472,7 @@ if is_windows_special_path:
SZAsyncProviderPool = SZProviderPool
def scan_video(path, dont_use_actual_file=False, hints=None, providers=None, skip_hashing=False, hash_from=None):
def scan_video(path, dont_use_actual_file=False, hints=None, providers=None, skip_hashing=False):
"""Scan a video from a `path`.
patch:
@@ -538,34 +537,28 @@ def scan_video(path, dont_use_actual_file=False, hints=None, providers=None, ski
video.alternative_titles.append(alt_guess["title"])
logger.debug("Adding alternative title: %s", alt_guess["title"])
if dont_use_actual_file and not hash_from:
if dont_use_actual_file:
return video
# size and hashes
if not skip_hashing:
hash_path = hash_from or path
video.size = os.path.getsize(hash_path)
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
osub_hash = None
if "opensubtitles" in providers:
video.hashes['opensubtitles'] = osub_hash = hash_opensubtitles(hash_path)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
if "shooter" in providers:
video.hashes['shooter'] = hash_shooter(hash_path)
video.hashes['shooter'] = hash_shooter(path)
if "thesubdb" in providers:
video.hashes['thesubdb'] = hash_thesubdb(hash_path)
video.hashes['thesubdb'] = hash_thesubdb(path)
if "napiprojekt" in providers:
try:
video.hashes['napiprojekt'] = hash_napiprojekt(hash_path)
video.hashes['napiprojekt'] = hash_napiprojekt(path)
except MemoryError:
logger.warning(u"Couldn't compute napiprojekt hash for %s", hash_path)
if "napisy24" in providers:
# Napisy24 uses the same hash as opensubtitles
video.hashes['napisy24'] = osub_hash or hash_opensubtitles(hash_path)
logger.warning(u"Couldn't compute napiprojekt hash for %s", path)
logger.debug('Computed hashes %r', video.hashes)
else:
@@ -581,7 +574,7 @@ def _search_external_subtitles(path, languages=None, only_one=False, scandir_gen
subtitles = {}
_scandir = _scandir_generic if scandir_generic else scandir
for entry in _scandir(dirpath):
if (not entry.name or entry.name in ('\x0c', '$', ',', '\x7f')) and not scandir_generic:
if not entry.name and not scandir_generic:
logger.debug('Could not determine the name of the file, retrying with scandir_generic')
return _search_external_subtitles(path, languages, only_one, True)
if not entry.is_file(follow_symlinks=False):
@@ -354,6 +354,7 @@ def patch_create_connection():
return _orig_create_connection((ip, port), *args, **kwargs)
except dns.exception.DNSException:
logger.warning("DNS: Couldn't resolve %s with DNS: %s", host, custom_resolver.nameservers)
raise
logger.debug("DNS: Falling back to default DNS or IP on %s", host)
return _orig_create_connection((host, port), *args, **kwargs)
@@ -23,10 +23,9 @@ class ArgenteamSubtitle(Subtitle):
hearing_impaired_verifiable = False
_release_info = None
def __init__(self, language, page_link, download_link, movie_kind, title, season, episode, year, release, version, source,
def __init__(self, language, download_link, movie_kind, title, season, episode, year, release, version, source,
video_codec, tvdb_id, imdb_id, asked_for_episode=None, asked_for_release_group=None, *args, **kwargs):
super(ArgenteamSubtitle, self).__init__(language, page_link=page_link, *args, **kwargs)
self.page_link = page_link
super(ArgenteamSubtitle, self).__init__(language, download_link, *args, **kwargs)
self.download_link = download_link
self.movie_kind = movie_kind
self.title = title
@@ -136,8 +135,7 @@ class ArgenteamProvider(Provider, ProviderSubtitleArchiveMixin):
provider_name = 'argenteam'
languages = {Language.fromalpha2(l) for l in ['es']}
video_types = (Episode, Movie)
BASE_URL = "http://www.argenteam.net/"
API_URL = BASE_URL + "api/v1/"
API_URL = "http://argenteam.net/api/v1/"
subtitle_class = ArgenteamSubtitle
hearing_impaired_verifiable = False
language_list = list(languages)
@@ -242,13 +240,12 @@ class ArgenteamProvider(Provider, ProviderSubtitleArchiveMixin):
for r in content['releases']:
for s in r['subtitles']:
movie_kind = "episode" if is_episode else "movie"
page_link = self.BASE_URL + movie_kind + "/" + str(aid)
sub = ArgenteamSubtitle(language, page_link, s['uri'], movie_kind, returned_title,
sub = ArgenteamSubtitle(language, s['uri'], "episode" if is_episode else "movie", returned_title,
season, episode, year, r.get('team'), r.get('tags'),
r.get('source'), r.get('codec'), content.get("tvdb"), imdb_id,
asked_for_release_group=video.release_group,
asked_for_episode=episode)
asked_for_episode=episode
)
subtitles.append(sub)
if has_multiple_ids:
@@ -1,124 +0,0 @@
import logging
import os
from io import BytesIO
from zipfile import ZipFile
from requests import Session
from subliminal_patch.subtitle import Subtitle
from subliminal_patch.providers import Provider
from subliminal import __short_version__
from subliminal.exceptions import AuthenticationError, ConfigurationError
from subliminal.subtitle import fix_line_ending
from subzero.language import Language
logger = logging.getLogger(__name__)
class Napisy24Subtitle(Subtitle):
'''Napisy24 Subtitle.'''
provider_name = 'napisy24'
def __init__(self, language, hash, imdb_id, napis_id):
super(Napisy24Subtitle, self).__init__(language)
self.hash = hash
self.imdb_id = imdb_id
self.napis_id = napis_id
@property
def id(self):
return self.hash
def get_matches(self, video):
matches = set()
# hash
if 'napisy24' in video.hashes and video.hashes['napisy24'] == self.hash:
matches.add('hash')
# imdb_id
if video.imdb_id and self.imdb_id == video.imdb_id:
matches.add('imdb_id')
return matches
class Napisy24Provider(Provider):
'''Napisy24 Provider.'''
languages = {Language(l) for l in ['pol']}
required_hash = 'napisy24'
api_url = 'http://napisy24.pl/run/CheckSubAgent.php'
def __init__(self, username=None, password=None):
if all((username, password)):
self.username = username
self.password = password
else:
self.username = 'subliminal'
self.password = 'lanimilbus'
self.session = None
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
self.session.headers['Content-Type'] = 'application/x-www-form-urlencoded'
def terminate(self):
self.session.close()
def query(self, language, size, name, hash):
params = {
'postAction': 'CheckSub',
'ua': self.username,
'ap': self.password,
'fs': size,
'fh': hash,
'fn': os.path.basename(name),
'n24pref': 1
}
response = self.session.post(self.api_url, data=params, timeout=10)
response.raise_for_status()
response_content = response.content.split(b'||', 1)
n24_data = response_content[0].decode()
if n24_data[:2] != 'OK':
if n24_data[:11] == 'login error':
raise AuthenticationError('Login failed')
logger.error('Unknown response: %s', response.content)
return None
n24_status = n24_data[:4]
if n24_status == 'OK-0':
logger.info('No subtitles found')
return None
subtitle_info = dict(p.split(':', 1) for p in n24_data.split('|')[1:])
logger.debug('Subtitle info: %s', subtitle_info)
if n24_status == 'OK-1':
logger.info('No subtitles found but got video info')
return None
elif n24_status == 'OK-2':
logger.info('Found subtitles')
elif n24_status == 'OK-3':
logger.info('Found subtitles but not from Napisy24 database')
return None
subtitle_content = response_content[1]
subtitle = Napisy24Subtitle(language, hash, 'tt%s' % subtitle_info['imdb'].zfill(7), subtitle_info['napisId'])
with ZipFile(BytesIO(subtitle_content)) as zf:
subtitle.content = fix_line_ending(zf.open(zf.namelist()[0]).read())
return subtitle
def list_subtitles(self, video, languages):
subtitles = [self.query(l, video.size, video.name, video.hashes['napisy24']) for l in languages]
return [s for s in subtitles if s is not None]
def download_subtitle(self, subtitle):
# there is no download step, content is already filled from listing subtitles
pass
@@ -4,34 +4,24 @@ import io
import logging
import os
import time
import traceback
import requests
import inflect
import re
import json
import HTMLParser
import urlparse
from zipfile import ZipFile
from babelfish import language_converters
from guessit import guessit
from dogpile.cache.api import NO_VALUE
from subliminal import Episode, ProviderError
from subliminal.exceptions import ConfigurationError, ServiceUnavailable
from subliminal.utils import sanitize_release_group
from subliminal.cache import region
from subliminal_patch.http import RetryingCFSession
from subliminal_patch.providers import Provider
from subliminal_patch.providers.mixins import ProviderSubtitleArchiveMixin
from subliminal_patch.subtitle import Subtitle, guess_matches
from subliminal_patch.converters.subscene import language_ids, supported_languages
from subscene_api.subscene import search, Subtitle as APISubtitle, SITE_DOMAIN
from subscene_api.subscene import search, Subtitle as APISubtitle
from subzero.language import Language
p = inflect.engine()
language_converters.register('subscene = subliminal_patch.converters.subscene:SubsceneConverter')
logger = logging.getLogger(__name__)
@@ -122,106 +112,28 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
skip_wrong_fps = False
hearing_impaired_verifiable = True
only_foreign = False
username = None
password = None
search_throttle = 8 # seconds
def __init__(self, only_foreign=False, username=None, password=None):
if not all((username, password)):
raise ConfigurationError('Username and password must be specified')
search_throttle = 2 # seconds
def __init__(self, only_foreign=False):
self.only_foreign = only_foreign
self.username = username
self.password = password
def initialize(self):
logger.info("Creating session")
self.session = RetryingCFSession()
prev_cookies = region.get("subscene_cookies2")
if prev_cookies != NO_VALUE:
logger.debug("Re-using old subscene cookies: %r", prev_cookies)
self.session.cookies.update(prev_cookies)
else:
logger.debug("Logging in")
self.login()
def login(self):
r = self.session.get("https://subscene.com/account/login")
if "Server Error" in r.content:
logger.error("Login unavailable; Maintenance?")
raise ServiceUnavailable("Login unavailable; Maintenance?")
match = re.search(r"<script id='modelJson' type='application/json'>\s*(.+)\s*</script>", r.content)
if match:
h = HTMLParser.HTMLParser()
data = json.loads(h.unescape(match.group(1)))
login_url = urlparse.urljoin(data["siteUrl"], data["loginUrl"])
time.sleep(1.0)
r = self.session.post(login_url,
{
"username": self.username,
"password": self.password,
data["antiForgery"]["name"]: data["antiForgery"]["value"]
})
pep_content = re.search(r"<form method=\"post\" action=\"https://subscene\.com/\">"
r".+name=\"id_token\".+?value=\"(?P<id_token>.+?)\".*?"
r"access_token\".+?value=\"(?P<access_token>.+?)\".+?"
r"token_type.+?value=\"(?P<token_type>.+?)\".+?"
r"expires_in.+?value=\"(?P<expires_in>.+?)\".+?"
r"scope.+?value=\"(?P<scope>.+?)\".+?"
r"state.+?value=\"(?P<state>.+?)\".+?"
r"session_state.+?value=\"(?P<session_state>.+?)\"",
r.content, re.MULTILINE | re.DOTALL)
if pep_content:
r = self.session.post(SITE_DOMAIN, pep_content.groupdict())
try:
r.raise_for_status()
except Exception:
raise ProviderError("Something went wrong when trying to log in: %s", traceback.format_exc())
else:
cj = self.session.cookies.copy()
store_cks = ("scene", "idsrv", "idsrv.xsrf", "idsvr.clients", "idsvr.session", "idsvr.username")
for cn in self.session.cookies.iterkeys():
if cn not in store_cks:
del cj[cn]
logger.debug("Storing cookies: %r", cj)
region.set("subscene_cookies2", cj)
return
raise ProviderError("Something went wrong when trying to log in #1")
def terminate(self):
logger.info("Closing session")
self.session.close()
def _create_filters(self, languages):
self.filters = dict(HearingImpaired="2")
acc_filters = self.filters.copy()
if self.only_foreign:
self.filters["ForeignOnly"] = "True"
acc_filters["ForeignOnly"] = self.filters["ForeignOnly"].lower()
logger.info("Only searching for foreign/forced subtitles")
selected_ids = []
for l in languages:
lid = language_ids.get(l.basename, language_ids.get(l.alpha3, None))
if lid:
selected_ids.append(str(lid))
acc_filters["SelectedIds"] = selected_ids
self.filters["LanguageFilter"] = ",".join(acc_filters["SelectedIds"])
last_filters = region.get("subscene_filters")
if last_filters != acc_filters:
region.set("subscene_filters", acc_filters)
logger.debug("Setting account filters to %r", acc_filters)
self.session.post("https://u.subscene.com/filter", acc_filters, allow_redirects=False)
self.filters["LanguageFilter"] = ",".join((str(language_ids[l.alpha3]) for l in languages
if l.alpha3 in language_ids))
logger.debug("Filter created: '%s'" % self.filters)
@@ -264,11 +176,7 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
def parse_results(self, video, film):
subtitles = []
for s in film.subtitles:
try:
subtitle = SubsceneSubtitle.from_api(s)
except NotImplementedError, e:
logger.info(e)
continue
subtitle = SubsceneSubtitle.from_api(s)
subtitle.asked_for_release_group = video.release_group
if isinstance(video, Episode):
subtitle.asked_for_episode = video.episode
@@ -281,16 +189,10 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
return subtitles
def do_search(self, *args, **kwargs):
try:
return search(*args, **kwargs)
except requests.HTTPError:
region.delete("subscene_cookies2")
def query(self, video):
# vfn = get_video_filename(video)
vfn = get_video_filename(video)
subtitles = []
# logger.debug(u"Searching for: %s", vfn)
#logger.debug(u"Searching for: %s", vfn)
# film = search(vfn, session=self.session)
#
# if film and film.subtitles:
@@ -299,17 +201,16 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
# else:
# logger.debug('No release results found')
# time.sleep(self.search_throttle)
#time.sleep(self.search_throttle)
# re-search for episodes without explicit release name
if isinstance(video, Episode):
titles = list(set([video.series] + video.alternative_series))[:2]
# term = u"%s S%02iE%02i" % (video.series, video.season, video.episode)
more_than_one = len(titles) > 1
for series in titles:
#term = u"%s S%02iE%02i" % (video.series, video.season, video.episode)
more_than_one = len([video.series] + video.alternative_series) > 1
for series in [video.series] + video.alternative_series:
term = u"%s - %s Season" % (series, p.number_to_words("%sth" % video.season).capitalize())
logger.debug('Searching for alternative results: %s', term)
film = self.do_search(term, session=self.session, release=False, throttle=self.search_throttle)
film = search(term, session=self.session, release=False, throttle=self.search_throttle)
if film and film.subtitles:
logger.debug('Alternative results found: %s', len(film.subtitles))
subtitles += self.parse_results(video, film)
@@ -317,27 +218,26 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
logger.debug('No alternative results found')
# packs
# if video.season_fully_aired:
# term = u"%s S%02i" % (series, video.season)
# logger.debug('Searching for packs: %s', term)
# time.sleep(self.search_throttle)
# film = search(term, session=self.session, throttle=self.search_throttle)
# if film and film.subtitles:
# logger.debug('Pack results found: %s', len(film.subtitles))
# subtitles += self.parse_results(video, film)
# else:
# logger.debug('No pack results found')
# else:
# logger.debug("Not searching for packs, because the season hasn't fully aired")
if video.season_fully_aired:
term = u"%s S%02i" % (series, video.season)
logger.debug('Searching for packs: %s', term)
time.sleep(self.search_throttle)
film = search(term, session=self.session, throttle=self.search_throttle)
if film and film.subtitles:
logger.debug('Pack results found: %s', len(film.subtitles))
subtitles += self.parse_results(video, film)
else:
logger.debug('No pack results found')
else:
logger.debug("Not searching for packs, because the season hasn't fully aired")
if more_than_one:
time.sleep(self.search_throttle)
else:
titles = list(set([video.title] + video.alternative_titles))[:2]
more_than_one = len(titles) > 1
for title in titles:
logger.debug('Searching for movie results: %r', title)
film = self.do_search(title, year=video.year, session=self.session, limit_to=None, release=False,
throttle=self.search_throttle)
more_than_one = len([video.title] + video.alternative_titles) > 1
for title in [video.title] + video.alternative_titles:
logger.debug('Searching for movie results: %s', title)
film = search(title, year=video.year, session=self.session, limit_to=None, release=False,
throttle=self.search_throttle)
if film and film.subtitles:
subtitles += self.parse_results(video, film)
if more_than_one:
@@ -226,7 +226,7 @@ class TitloviProvider(Provider, ProviderSubtitleArchiveMixin):
page_link = self.server_url + sub.a.attrs['href']
# subtitle language
_lang = sub.select_one('.lang')
match = lang_re.search(_lang.attrs.get('src', _lang.attrs.get('data-cfsrc', '')))
match = lang_re.search(_lang.attrs.get('src', _lang.attrs.get('cfsrc', '')))
if match:
try:
# decode language
@@ -117,14 +117,14 @@ class Subtitle(Subtitle_):
logger.info('Guessing encoding for language %s', self.language)
encodings = ['utf-8']
encodings = ['utf-8', 'utf-16']
# add language-specific encodings
# http://scratchpad.wikia.com/wiki/Character_Encoding_Recommendation_for_Languages
if self.language.alpha3 == 'zho':
encodings.extend(['cp936', 'gb2312', 'gbk', 'gb18030', 'hz', 'iso2022_jp_2', 'cp950', 'gb18030', 'big5',
'big5hkscs', 'utf-16'])
'big5hkscs'])
elif self.language.alpha3 == 'jpn':
encodings.extend(['shift-jis', 'cp932', 'euc_jp', 'iso2022_jp', 'iso2022_jp_1', 'iso2022_jp_2',
'iso2022_jp_2004', 'iso2022_jp_3', 'iso2022_jp_ext', ])
@@ -133,7 +133,7 @@ class Subtitle(Subtitle_):
# arabian/farsi
elif self.language.alpha3 in ('ara', 'fas', 'per'):
encodings.extend(['windows-1256', 'utf-16'])
encodings.append('windows-1256')
elif self.language.alpha3 == 'heb':
encodings.extend(['windows-1255', 'iso-8859-8'])
elif self.language.alpha3 == 'tur':
@@ -251,7 +251,8 @@ class Subtitle(Subtitle_):
subs = pysubs2.SSAFile.from_string(text, fps=self.plex_media_fps)
unicontent = self.pysubs2_to_unicode(subs)
self.content = unicontent.encode(self._guessed_encoding)
self.content = unicontent.encode("utf-8")
self._guessed_encoding = "utf-8"
except:
logger.exception("Couldn't convert subtitle %s to .srt format: %s", self, traceback.format_exc())
return False
@@ -319,8 +320,7 @@ class Subtitle(Subtitle_):
:return: string
"""
if not self.mods:
return fix_text(self.content.decode(encoding=self._guessed_encoding), **ftfy_defaults).encode(
encoding=self._guessed_encoding)
return fix_text(self.content.decode("utf-8"), **ftfy_defaults).encode(encoding="utf-8")
submods = SubtitleModifications(debug=debug)
if submods.load(content=self.text, language=self.language):
@@ -329,7 +329,7 @@ class Subtitle(Subtitle_):
self.mods = submods.mods_used
content = fix_text(self.pysubs2_to_unicode(submods.f, format=format), **ftfy_defaults)\
.encode(encoding=self._guessed_encoding)
.encode(encoding="utf-8")
submods.f = None
del submods
return content
+1 -1
View File
@@ -21,7 +21,7 @@ if debug:
logging.basicConfig(level=logging.DEBUG)
#sub = Subtitle(Language.fromietf("eng:forced"), mods=["common", "remove_HI", "OCR_fixes", "fix_uppercase", "shift_offset(ms=-500)", "shift_offset(ms=500)", "shift_offset(s=2,ms=800)"])
sub = Subtitle(Language.fromietf("eng"), mods=["common", "remove_HI", "OCR_fixes", "fix_uppercase", "shift_offset(ms=0,s=1)"])
sub = Subtitle(Language.fromietf("eng:forced"), mods=["common", "remove_HI", "OCR_fixes", "fix_uppercase", "shift_offset(ms=0,s=1)"])
sub.content = open(fn).read()
sub.normalize()
content = sub.get_modified_content(debug=True)
@@ -30,7 +30,6 @@ import enum
import sys
import requests
import time
import logging
is_PY2 = sys.version_info[0] < 3
if is_PY2:
@@ -40,13 +39,8 @@ else:
from contextlib import suppress
from urllib2.request import Request, urlopen
from dogpile.cache.api import NO_VALUE
from subliminal.cache import region
from bs4 import BeautifulSoup, NavigableString
logger = logging.getLogger(__name__)
# constants
HEADERS = {
}
@@ -56,21 +50,14 @@ DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWeb"\
"Kit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36"
ENDPOINT_RE = re.compile(ur'(?uis)<form.+?action="/subtitles/(.+)">.*?<input type="text"')
class NewEndpoint(Exception):
pass
# utils
def soup_for(url, data=None, session=None, user_agent=DEFAULT_USER_AGENT):
def soup_for(url, session=None, user_agent=DEFAULT_USER_AGENT):
url = re.sub("\s", "+", url)
if not session:
r = Request(url, data=None, headers=dict(HEADERS, **{"User-Agent": user_agent}))
html = urlopen(r).read().decode("utf-8")
else:
ret = session.post(url, data=data)
ret = session.get(url)
ret.raise_for_status()
html = ret.text
return BeautifulSoup(html, "html.parser")
@@ -125,7 +112,7 @@ class Subtitle(object):
subtitles = []
for row in rows:
if row.td.a is not None and row.td.get("class", ["lazy"])[0] != "empty":
if row.td.a is not None:
subtitles.append(cls.from_row(row))
return subtitles
@@ -255,42 +242,29 @@ def get_first_film(soup, section, year=None, session=None):
url = SITE_DOMAIN + t.div.a.get("href")
break
if not url:
# fallback to non-year results
logger.info("Falling back to non-year results as year wasn't found (%s)", year)
url = SITE_DOMAIN + tag.findNext("ul").find("li").div.a.get("href")
return
return Film.from_url(url, session=session)
def find_endpoint(session, content=None):
endpoint = region.get("subscene_endpoint2")
if endpoint is NO_VALUE:
if not content:
content = session.get(SITE_DOMAIN).text
m = ENDPOINT_RE.search(content)
if m:
endpoint = m.group(1).strip()
logger.debug("Switching main endpoint to %s", endpoint)
region.set("subscene_endpoint2", endpoint)
return endpoint
def search(term, release=True, session=None, year=None, limit_to=SearchTypes.Exact, throttle=0):
# note to subscene: if you actually start to randomize the endpoint, we'll have to query your server even more
endpoints = ["searching", "search", "srch", "find"]
if release:
endpoint = "release"
else:
endpoint = find_endpoint(session)
time.sleep(throttle)
endpoints = ["release"]
if not endpoint:
logger.error("Couldn't find endpoint, exiting")
return
soup = soup_for("%s/subtitles/%s" % (SITE_DOMAIN, endpoint), data={"query": term},
session=session)
soup = None
for endpoint in endpoints:
try:
soup = soup_for("%s/subtitles/%s?q=%s" % (SITE_DOMAIN, endpoint, term),
session=session)
except requests.HTTPError, e:
if e.response.status_code == 404:
time.sleep(throttle)
# fixme: detect endpoint from html
continue
raise
break
if soup:
if "Subtitle search by" in str(soup):
@@ -8,25 +8,6 @@ repl_map = {
"dk": "da",
"nld": "nl",
"english": "en",
"alb": "sq",
"arm": "hy",
"baq": "eu",
"bur": "my",
"chi": "zh",
"cze": "cs",
"dut": "nl",
"fre": "fr",
"geo": "ka",
"ger": "de",
"gre": "el",
"ice": "is",
"mac": "mk",
"mao": "mi",
"may": "ms",
"per": "fa",
"rum": "ro",
"slo": "sk",
"tib": "bo",
}
File diff suppressed because one or more lines are too long
@@ -36,7 +36,6 @@ SZ_FIX_DATA = {
u" l ": u" I ",
u"'sjust": u"'s just",
u"'tjust": u"'t just",
u"\";": u"'s",
},
"WholeWords": {
u"I'11": u"I'll",
@@ -6,7 +6,7 @@ import pysubs2
import logging
import time
from mods import EMPTY_TAG_PROCESSOR, EmptyEntryError
from mods import EMPTY_TAG_PROCESSOR, EmptyEntryError, FullContentRep
from registry import registry
from subzero.language import Language
@@ -257,7 +257,16 @@ class SubtitleModifications(object):
mod.modify(None, debug=self.debug, parent=self, **args)
def apply_line_mods(self, new_entries, mods):
for index, entry in enumerate(self.f, 1):
index = 1
entries = self.f[:]
entry_count = len(entries)
while 1:
if index > entry_count - 1:
break
entry = entries[index]
applied_mods = []
lines = []
@@ -265,116 +274,110 @@ class SubtitleModifications(object):
start_tags = []
end_tags = []
t = entry.text.strip()
if not t:
text = entry.text.replace(ur"\N", "\n").strip()
if not text:
if self.debug:
logger.debug(u"Skipping empty line: %s", index)
index += 1
continue
skip_entry = False
for line in t.split(ur"\N"):
# don't bother the mods with surrounding tags
old_line = line
line = line.strip()
skip_line = False
line_count += 1
try:
for line in text.split("\n"):
# don't bother the mods with surrounding tags
old_line = line
line = line.strip()
skip_line = False
line_count += 1
if not line:
continue
if not line:
continue
# clean {\X0} tags before processing
# fixme: handle nested tags?
start_tag = u""
end_tag = u""
if line.startswith(self.font_style_tag_start):
start_tag = line[:5]
line = line[5:]
if line[-5:-3] == self.font_style_tag_start:
end_tag = line[-5:]
line = line[:-5]
# clean {\X0} tags before processing
# fixme: handle nested tags?
start_tag = u""
end_tag = u""
if line.startswith(self.font_style_tag_start):
start_tag = line[:5]
line = line[5:]
if line[-5:-3] == self.font_style_tag_start:
end_tag = line[-5:]
line = line[:-5]
last_procs_mods = []
last_procs_mods = []
# fixme: this double loop is ugly
for order, identifier, args in mods:
mod = self.initialized_mods[identifier]
# fixme: this double loop is ugly
for order, identifier, args in mods:
mod = self.initialized_mods[identifier]
try:
line = mod.modify(line.strip(), entry=entry.text, debug=self.debug, parent=self, index=index,
line = mod.modify(line.strip(), entry=text, debug=self.debug, parent=self, index=index,
**args)
except EmptyEntryError:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, identifier, entry.text)
skip_entry = True
break
if not line:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, identifier, old_line)
skip_line = True
break
if not line:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, identifier, old_line)
skip_line = True
break
applied_mods.append(identifier)
if mod.last_processors:
last_procs_mods.append([identifier, args])
applied_mods.append(identifier)
if mod.last_processors:
last_procs_mods.append([identifier, args])
if skip_entry:
lines = []
break
if skip_line:
continue
if skip_line:
continue
for identifier, args in last_procs_mods:
mod = self.initialized_mods[identifier]
for identifier, args in last_procs_mods:
mod = self.initialized_mods[identifier]
try:
line = mod.modify(line.strip(), entry=entry.text, debug=self.debug, parent=self, index=index,
line = mod.modify(line.strip(), entry=text, debug=self.debug, parent=self, index=index,
procs=["last_process"], **args)
except EmptyEntryError:
if not line:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, identifier, old_line)
skip_line = True
break
if skip_line:
continue
if start_tag:
start_tags.append(start_tag)
if end_tag:
end_tags.append(end_tag)
# append new line and clean possibly newly added empty tags
cleaned_line = EMPTY_TAG_PROCESSOR.process(start_tag + line + end_tag, debug=self.debug).strip()
if cleaned_line:
# we may have a single closing tag, if so, try appending it to the previous line
if len(cleaned_line) == 5 and cleaned_line.startswith("{\\") and cleaned_line.endswith("0}"):
if lines:
prev_line = lines.pop()
lines.append(prev_line + cleaned_line)
continue
lines.append(cleaned_line)
else:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, identifier, entry.text)
skip_entry = True
break
logger.debug(u"%d: Ditching now empty line (%r)", index, line)
if not line:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, identifier, old_line)
skip_line = True
break
if skip_entry:
lines = []
break
if skip_line:
if not lines:
# don't bother logging when the entry only had one line
if self.debug and line_count > 1:
logger.debug(u"%d: %r -> ''", index, text)
index += 1
continue
except EmptyEntryError, e:
if self.debug:
logger.debug(u"%d: %s: %r -> ''", index, e.mod.identifier, e.entry)
index += 1
continue
if start_tag:
start_tags.append(start_tag)
if end_tag:
end_tags.append(end_tag)
# append new line and clean possibly newly added empty tags
cleaned_line = EMPTY_TAG_PROCESSOR.process(start_tag + line + end_tag, debug=self.debug).strip()
if cleaned_line:
# we may have a single closing tag, if so, try appending it to the previous line
if len(cleaned_line) == 5 and cleaned_line.startswith("{\\") and cleaned_line.endswith("0}"):
if lines:
prev_line = lines.pop()
lines.append(prev_line + cleaned_line)
continue
lines.append(cleaned_line)
else:
if self.debug:
logger.debug(u"%d: Ditching now empty line (%r)", index, line)
if not lines:
# don't bother logging when the entry only had one line
if self.debug and line_count > 1:
logger.debug(u"%d: %r -> ''", index, entry.text)
except FullContentRep, e:
if self.debug:
logger.debug(u"%d: %s: %r -> %r", index, e.mod.identifier, text, e.new_content)
new_entries.append(e.new_content.replace("\n", ur"\N"))
index += 1
continue
new_text = ur"\N".join(lines)
@@ -403,6 +406,8 @@ class SubtitleModifications(object):
entry.text = new_text
new_entries.append(entry)
index += 1
SubMod = SubtitleModifications
@@ -47,7 +47,7 @@ class SubtitleModification(object):
continue
old_content = new_content
new_content = processor.process(new_content, debug=debug, **kwargs)
new_content = processor.process(new_content, debug=debug, mod=self, **kwargs)
if not new_content:
if debug:
logger.debug("Processor returned empty line: %s", processor.name)
@@ -107,9 +107,22 @@ empty_line_post_processors = [
]
class EmptyEntryError(Exception):
class ModEvent(Exception):
def __init__(self, *args, **kwargs):
self.mod = kwargs.pop("mod", None)
self.entry = kwargs.pop("entry", None)
super(ModEvent, self).__init__(*args, **kwargs)
class EmptyEntryError(ModEvent):
pass
class EmptyLineError(Exception):
class EmptyLineError(ModEvent):
pass
class FullContentRep(ModEvent):
def __init__(self, *args, **kwargs):
self.new_content = kwargs.pop("new_content", None)
super(FullContentRep, self).__init__(*args, **kwargs)
@@ -1,7 +1,8 @@
# coding=utf-8
import re
from subzero.modification.mods import SubtitleTextModification, empty_line_post_processors, EmptyEntryError, TAG
from subzero.modification.mods import SubtitleTextModification, empty_line_post_processors, EmptyEntryError, TAG, \
FullContentRep
from subzero.modification.processors.re_processor import NReProcessor
from subzero.modification import registry
@@ -10,9 +11,11 @@ class FullBracketEntryProcessor(NReProcessor):
def process(self, content, debug=False, **kwargs):
entry = kwargs.get("entry")
if entry:
rep_content = super(FullBracketEntryProcessor, self).process(entry, debug=debug, **kwargs)
if not rep_content.strip():
raise EmptyEntryError()
rep_content = super(FullBracketEntryProcessor, self).process(entry, debug=debug, **kwargs).strip()
if not rep_content:
raise EmptyEntryError(mod=self.mod, entry=entry)
if content != rep_content:
raise FullContentRep(new_content=rep_content, mod=self.mod, entry=entry)
return content
@@ -49,8 +52,8 @@ class HearingImpaired(SubtitleTextModification):
NReProcessor(re.compile(ur'(?sux)-?%(t)s[([][^([)\]]+?(?=[A-zÀ-ž"\'.]{3,})[^([)\]]+[)\]][\s:]*%(t)s' %
{"t": TAG}), "", name="HI_brackets"),
#NReProcessor(re.compile(ur'(?sux)-?%(t)s[([]%(t)s(?=[A-zÀ-ž"\'.]{3,})[^([)\]]+%(t)s$' % {"t": TAG}),
# "", name="HI_bracket_open_start"),
FullBracketEntryProcessor(re.compile(ur'(?sux)-?%(t)s[([]%(t)s(?=[A-zÀ-ž"\'.]{3,})[^([)\]]+%(t)s$' % {"t": TAG}),
"", name="HI_bracket_open_start"),
#NReProcessor(re.compile(ur'(?sux)-?%(t)s(?=[A-zÀ-ž"\'.]{3,})[^([)\]]+[)\]][\s:]*%(t)s' % {"t": TAG}), "",
# name="HI_bracket_open_end"),
@@ -7,12 +7,14 @@ class Processor(object):
"""
name = None
parent = None
mod = None
supported = None
enabled = True
def __init__(self, name=None, parent=None, supported=None):
def __init__(self, name=None, parent=None, mod=None, supported=None):
self.name = name
self.parent = parent
self.mod = mod
self.supported = supported if supported else lambda parent: True
@property
@@ -20,6 +22,8 @@ class Processor(object):
return self.name
def process(self, content, debug=False, **kwargs):
if not self.mod:
self.mod = kwargs.get("mod", None)
return content
def __repr__(self):
@@ -14,12 +14,13 @@ class ReProcessor(Processor):
pattern = None
replace_with = None
def __init__(self, pattern, replace_with, name=None, supported=None):
super(ReProcessor, self).__init__(name=name, supported=supported)
def __init__(self, pattern, replace_with, name=None, supported=None, **kwargs):
super(ReProcessor, self).__init__(name=name, supported=supported, **kwargs)
self.pattern = pattern
self.replace_with = replace_with
def process(self, content, debug=False, **kwargs):
super(ReProcessor, self).process(content, debug=debug, **kwargs)
return self.pattern.sub(self.replace_with, content)
+2 -2
View File
@@ -52,10 +52,10 @@ def set_existing_languages(video, video_info, external_subtitles=False, embedded
video.subtitle_languages.add(language)
def parse_video(fn, hints, skip_hashing=False, dry_run=False, providers=None, hash_from=None):
def parse_video(fn, hints, skip_hashing=False, dry_run=False, providers=None):
logger.debug("Parsing video: %s, hints: %s", os.path.basename(fn), hints)
return scan_video(fn, hints=hints, dont_use_actual_file=dry_run, providers=providers,
skip_hashing=skip_hashing, hash_from=hash_from)
skip_hashing=skip_hashing)
def refine_video(video, no_refining=False, refiner_settings=None):
+12 -12
View File
@@ -91,28 +91,28 @@ the.vbm, mmgoodnow, Vertig0ne, thliu78, tattoomees, ostman, count_confucius, ehe
## Changelog
2.6.5.3124
2.6.5.3074
subscene, addic7ed and titlovi
- either of those providers might impose a reCAPTCHA verification. In order to use those providers, please create an account at an AntiCaptcha service ([anti-captcha.com](http://getcaptchasolution.com/kkvviom7nh) or [deathbycaptcha.com](http://deathbycaptcha.com)), add funds, then supply your credentials/apikey in the configuration
Changelog
- core: http: fallback to default DNS when normal resolving fails; fixes #657
- core: extract embedded/menu: fix detection of unknown streams; don't use unknown streams if a known language was previously found
- core: language: use replacement map from bazarr
- providers: titlovi: fix matching
- providers: subscene: fix unknown language code error when "empty" result is returned
- providers: subscene: add support for pt-BR (based on Diaoul/subliminal@b22cf08)
- providers: subscene: explicitly set account filters for languages
- providers: subscene: limit alternative searches to 3; set throttle to 8
- providers: subscene: move login/cookies to initialization sequence
- submod: generic: en: fix ";='s
- core: cf: bypass cf 95% of the time without captchas
- core: fix breaking line endings of certain languages (chinese, UTF-16); fixes #646
- core: update pysubs2 to 0.2.3
2.6.5.3062
Changelog
- core: cf: optimize
- core: http: don't query DNS with IPs. thanks @fgump (fixes sonarr/radarr)
[older changes](CHANGELOG.md)
Subtitles provided by [OpenSubtitles.org](http://www.opensubtitles.org/), [Podnapisi.NET](https://www.podnapisi.net/), [TVSubtitles.net](http://www.tvsubtitles.net/), [Addic7ed.com](http://www.addic7ed.com/), [Legendas TV](http://legendas.tv/), [Napi Projekt](http://www.napiprojekt.pl/), [Shooter](http://shooter.cn/), [Titlovi](http://titlovi.com), [aRGENTeaM](http://argenteam.net), [SubScene](https://subscene.com/), [Hosszupuska](http://hosszupuskasub.com/), [Napisy24](https://napisy24.pl/)
Subtitles provided by [OpenSubtitles.org](http://www.opensubtitles.org/), [Podnapisi.NET](https://www.podnapisi.net/), [TVSubtitles.net](http://www.tvsubtitles.net/), [Addic7ed.com](http://www.addic7ed.com/), [Legendas TV](http://legendas.tv/), [Napi Projekt](http://www.napiprojekt.pl/), [Shooter](http://shooter.cn/), [Titlovi](http://titlovi.com), [aRGENTeaM](http://argenteam.net), [SubScene](https://subscene.com/), [Hosszupuska](http://hosszupuskasub.com/)
[3rd party licenses](https://github.com/pannal/Sub-Zero.bundle/tree/master/Licenses)