From 8eda31aaca390a7ff68742dd18a0ae705e0a5d74 Mon Sep 17 00:00:00 2001 From: josdion Date: Sun, 29 Mar 2020 20:08:53 +0300 Subject: [PATCH 01/14] Use tmdbId as a key when updating table_movies if you delete a movie from radarr and then add it again, it will have the same tmdbId, but new radarrId. If this happens between two scans, bazarr won't be able to update the movie again. This fix should prevent this. --- bazarr/get_movies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bazarr/get_movies.py b/bazarr/get_movies.py index e4f545c29..36dd63c39 100644 --- a/bazarr/get_movies.py +++ b/bazarr/get_movies.py @@ -231,8 +231,8 @@ def update_movies(): for updated_movie in movies_to_update_list: query = dict_converter.convert(updated_movie) - database.execute('''UPDATE table_movies SET ''' + query.keys_update + ''' WHERE radarrId = ?''', - query.values + (updated_movie['radarrId'],)) + database.execute('''UPDATE table_movies SET ''' + query.keys_update + ''' WHERE tmdbId = ?''', + query.values + (updated_movie['tmdbId'],)) altered_movies.append([updated_movie['tmdbId'], updated_movie['path'], updated_movie['radarrId'], From 70b4a6c469e1f4b098d7c93df86d30691a487c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Mon, 30 Mar 2020 20:18:11 -0400 Subject: [PATCH 02/14] Fix for #883 --- libs/apprise/__init__.py | 4 +- libs/apprise/i18n/apprise.pot | 7 +- libs/apprise/plugins/NotifyEmail.py | 25 ++- libs/apprise/plugins/NotifyJoin.py | 26 +-- libs/apprise/plugins/NotifySlack.py | 6 +- libs/apprise/plugins/NotifyTelegram.py | 41 ++-- .../plugins/NotifyXMPP/SleekXmppAdapter.py | 208 ++++++++++++++++++ .../{NotifyXMPP.py => NotifyXMPP/__init__.py} | 126 ++--------- libs/apprise/plugins/__init__.py | 4 + 9 files changed, 297 insertions(+), 150 deletions(-) create mode 100644 libs/apprise/plugins/NotifyXMPP/SleekXmppAdapter.py rename libs/apprise/plugins/{NotifyXMPP.py => NotifyXMPP/__init__.py} (76%) diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py index cf080be1b..63da23f8c 100644 --- a/libs/apprise/__init__.py +++ b/libs/apprise/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2019 Chris Caron +# Copyright (C) 2020 Chris Caron # All rights reserved. # # This code is licensed under the MIT License. @@ -24,7 +24,7 @@ # THE SOFTWARE. __title__ = 'apprise' -__version__ = '0.8.4' +__version__ = '0.8.5' __author__ = 'Chris Caron' __license__ = 'MIT' __copywrite__ = 'Copyright (C) 2020 Chris Caron ' diff --git a/libs/apprise/i18n/apprise.pot b/libs/apprise/i18n/apprise.pot index ffd9b700a..ea3fdfad1 100644 --- a/libs/apprise/i18n/apprise.pot +++ b/libs/apprise/i18n/apprise.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: apprise 0.8.4\n" +"Project-Id-Version: apprise 0.8.5\n" "Report-Msgid-Bugs-To: lead2gold@gmail.com\n" -"POT-Creation-Date: 2020-02-01 12:59-0500\n" +"POT-Creation-Date: 2020-03-30 16:00-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -98,6 +98,9 @@ msgstr "" msgid "Device ID" msgstr "" +msgid "Device Name" +msgstr "" + msgid "Display Footer" msgstr "" diff --git a/libs/apprise/plugins/NotifyEmail.py b/libs/apprise/plugins/NotifyEmail.py index 222e32e48..de686c8b3 100644 --- a/libs/apprise/plugins/NotifyEmail.py +++ b/libs/apprise/plugins/NotifyEmail.py @@ -269,6 +269,14 @@ class NotifyEmail(NotifyBase): # Define object templates templates = ( + '{schema}://{host}', + '{schema}://{host}:{port}', + '{schema}://{host}/{targets}', + '{schema}://{host}:{port}/{targets}', + '{schema}://{user}@{host}', + '{schema}://{user}@{host}:{port}', + '{schema}://{user}@{host}/{targets}', + '{schema}://{user}@{host}:{port}/{targets}', '{schema}://{user}:{password}@{host}', '{schema}://{user}:{password}@{host}:{port}', '{schema}://{user}:{password}@{host}/{targets}', @@ -280,13 +288,11 @@ class NotifyEmail(NotifyBase): 'user': { 'name': _('User Name'), 'type': 'string', - 'required': True, }, 'password': { 'name': _('Password'), 'type': 'string', 'private': True, - 'required': True, }, 'host': { 'name': _('Domain'), @@ -388,7 +394,7 @@ class NotifyEmail(NotifyBase): self.from_name = from_name self.from_addr = from_addr - if not self.from_addr: + if self.user and not self.from_addr: # detect our email address self.from_addr = '{}@{}'.format( re.split(r'[\s@]+', self.user)[0], @@ -446,6 +452,10 @@ class NotifyEmail(NotifyBase): # Apply any defaults based on certain known configurations self.NotifyEmailDefaults() + # if there is still no smtp_host then we fall back to the hostname + if not self.smtp_host: + self.smtp_host = self.host + return def NotifyEmailDefaults(self): @@ -454,10 +464,11 @@ class NotifyEmail(NotifyBase): it was provided. """ - if self.smtp_host: + if self.smtp_host or not self.user: # SMTP Server was explicitly specified, therefore it is assumed # the caller knows what he's doing and is intentionally - # over-riding any smarts to be applied + # over-riding any smarts to be applied. We also can not apply + # any default if there was no user specified. return # detect our email address using our user/host combo @@ -683,7 +694,7 @@ class NotifyEmail(NotifyBase): args['bcc'] = ','.join(self.bcc) # pull email suffix from username (if present) - user = self.user.split('@')[0] + user = None if not self.user else self.user.split('@')[0] # Determine Authentication auth = '' @@ -693,7 +704,7 @@ class NotifyEmail(NotifyBase): password=self.pprint( self.password, privacy, mode=PrivacyMode.Secret, safe=''), ) - else: + elif user: # user url auth = '{user}@'.format( user=NotifyEmail.quote(user, safe=''), diff --git a/libs/apprise/plugins/NotifyJoin.py b/libs/apprise/plugins/NotifyJoin.py index 76011d984..278ddaef8 100644 --- a/libs/apprise/plugins/NotifyJoin.py +++ b/libs/apprise/plugins/NotifyJoin.py @@ -130,6 +130,11 @@ class NotifyJoin(NotifyBase): 'regex': (r'^[a-z0-9]{32}$', 'i'), 'map_to': 'targets', }, + 'device_name': { + 'name': _('Device Name'), + 'type': 'string', + 'map_to': 'targets', + }, 'group': { 'name': _('Group'), 'type': 'choice:string', @@ -210,18 +215,7 @@ class NotifyJoin(NotifyBase): 'group.{}'.format(group_re.group('name').lower())) continue - elif IS_DEVICE_RE.match(target): - self.targets.append(target) - continue - - self.logger.warning( - 'Ignoring invalid Join device/group "{}"'.format(target) - ) - - if not self.targets: - msg = 'No Join targets to notify.' - self.logger.warning(msg) - raise TypeError(msg) + self.targets.append(target) return @@ -247,12 +241,18 @@ class NotifyJoin(NotifyBase): url_args = { 'apikey': self.apikey, - 'deviceId': target, 'priority': str(self.priority), 'title': title, 'text': body, } + if IS_GROUP_RE.match(target) or IS_DEVICE_RE.match(target): + url_args['deviceId'] = target + + else: + # Support Device Names + url_args['deviceNames'] = target + # prepare our image for display if configured to do so image_url = None if not self.include_image \ else self.image_url(notify_type) diff --git a/libs/apprise/plugins/NotifySlack.py b/libs/apprise/plugins/NotifySlack.py index b17ecd858..d4e4f6112 100644 --- a/libs/apprise/plugins/NotifySlack.py +++ b/libs/apprise/plugins/NotifySlack.py @@ -176,7 +176,7 @@ class NotifySlack(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'^[A-Z0-9]{9}$', 'i'), + 'regex': (r'^[A-Z0-9]+$', 'i'), }, # Token required as part of the Webhook request # /........./BBBBBBBBB/........................ @@ -185,7 +185,7 @@ class NotifySlack(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'^[A-Z0-9]{9}$', 'i'), + 'regex': (r'^[A-Z0-9]+$', 'i'), }, # Token required as part of the Webhook request # /........./........./CCCCCCCCCCCCCCCCCCCCCCCC @@ -194,7 +194,7 @@ class NotifySlack(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'^[A-Za-z0-9]{24}$', 'i'), + 'regex': (r'^[A-Za-z0-9]+$', 'i'), }, 'target_encoded_id': { 'name': _('Target Encoded ID'), diff --git a/libs/apprise/plugins/NotifyTelegram.py b/libs/apprise/plugins/NotifyTelegram.py index 73bdf6585..0b6a2343f 100644 --- a/libs/apprise/plugins/NotifyTelegram.py +++ b/libs/apprise/plugins/NotifyTelegram.py @@ -477,6 +477,9 @@ class NotifyTelegram(NotifyBase): # Return our detected userid return _id + self.logger.warning( + 'Failed to detect a Telegram user; ' + 'try sending your bot a message first.') return 0 def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, @@ -505,8 +508,12 @@ class NotifyTelegram(NotifyBase): if self.notify_format == NotifyFormat.MARKDOWN: payload['parse_mode'] = 'MARKDOWN' - else: - # Either TEXT or HTML; if TEXT we'll make it HTML + payload['text'] = '{}{}'.format( + '{}\r\n'.format(title) if title else '', + body, + ) + + elif self.notify_format == NotifyFormat.HTML: payload['parse_mode'] = 'HTML' # HTML Spaces ( ) and tabs ( ) aren't supported @@ -524,31 +531,23 @@ class NotifyTelegram(NotifyBase): # Tabs become 3 spaces title = re.sub(' ?', ' ', title, re.I) - # HTML - title = NotifyTelegram.escape_html(title, whitespace=False) + payload['text'] = '{}{}'.format( + '{}\r\n'.format(title) if title else '', + body, + ) - # HTML + else: # TEXT + payload['parse_mode'] = 'HTML' + + # Escape content + title = NotifyTelegram.escape_html(title, whitespace=False) body = NotifyTelegram.escape_html(body, whitespace=False) - if title and self.notify_format == NotifyFormat.TEXT: - # Text HTML Formatting - payload['text'] = '%s\r\n%s' % ( - title, + payload['text'] = '{}{}'.format( + '{}\r\n'.format(title) if title else '', body, ) - elif title: - # Already HTML; trust developer has wrapped - # the title appropriately - payload['text'] = '%s\r\n%s' % ( - title, - body, - ) - - else: - # Assign the body - payload['text'] = body - # Create a copy of the chat_ids list targets = list(self.targets) while len(targets): diff --git a/libs/apprise/plugins/NotifyXMPP/SleekXmppAdapter.py b/libs/apprise/plugins/NotifyXMPP/SleekXmppAdapter.py new file mode 100644 index 000000000..a28e9ce54 --- /dev/null +++ b/libs/apprise/plugins/NotifyXMPP/SleekXmppAdapter.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +import ssl +from os.path import isfile +import logging + + +# Default our global support flag +SLEEKXMPP_SUPPORT_AVAILABLE = False + +try: + # Import sleekxmpp if available + import sleekxmpp + + SLEEKXMPP_SUPPORT_AVAILABLE = True + +except ImportError: + # No problem; we just simply can't support this plugin because we're + # either using Linux, or simply do not have sleekxmpp installed. + pass + + +class SleekXmppAdapter(object): + """ + Wrapper to sleekxmpp + + """ + + # Reference to XMPP client. + xmpp = None + + # Whether everything succeeded + success = False + + # The default protocol + protocol = 'xmpp' + + # The default secure protocol + secure_protocol = 'xmpps' + + # The default XMPP port + default_unsecure_port = 5222 + + # The default XMPP secure port + default_secure_port = 5223 + + # Taken from https://golang.org/src/crypto/x509/root_linux.go + CA_CERTIFICATE_FILE_LOCATIONS = [ + # Debian/Ubuntu/Gentoo etc. + "/etc/ssl/certs/ca-certificates.crt", + # Fedora/RHEL 6 + "/etc/pki/tls/certs/ca-bundle.crt", + # OpenSUSE + "/etc/ssl/ca-bundle.pem", + # OpenELEC + "/etc/pki/tls/cacert.pem", + # CentOS/RHEL 7 + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + ] + + # This entry is a bit hacky, but it allows us to unit-test this library + # in an environment that simply doesn't have the sleekxmpp package + # available to us. + # + # If anyone is seeing this had knows a better way of testing this + # outside of what is defined in test/test_xmpp_plugin.py, please + # let me know! :) + _enabled = SLEEKXMPP_SUPPORT_AVAILABLE + + def __init__(self, host=None, port=None, secure=False, + verify_certificate=True, xep=None, jid=None, password=None, + body=None, targets=None, before_message=None, logger=None): + """ + Initialize our SleekXmppAdapter object + """ + + self.host = host + self.port = port + self.secure = secure + self.verify_certificate = verify_certificate + + self.xep = xep + self.jid = jid + self.password = password + + self.body = body + self.targets = targets + self.before_message = before_message + + self.logger = logger or logging.getLogger(__name__) + + # Use the Apprise log handlers for configuring the sleekxmpp logger. + apprise_logger = logging.getLogger('apprise') + sleek_logger = logging.getLogger('sleekxmpp') + for handler in apprise_logger.handlers: + sleek_logger.addHandler(handler) + sleek_logger.setLevel(apprise_logger.level) + + if not self.load(): + raise ValueError("Invalid XMPP Configuration") + + def load(self): + + # Prepare our object + self.xmpp = sleekxmpp.ClientXMPP(self.jid, self.password) + + # Register our session + self.xmpp.add_event_handler("session_start", self.session_start) + + for xep in self.xep: + # Load xep entries + try: + self.xmpp.register_plugin('xep_{0:04d}'.format(xep)) + + except sleekxmpp.plugins.base.PluginNotFound: + self.logger.warning( + 'Could not register plugin {}'.format( + 'xep_{0:04d}'.format(xep))) + return False + + if self.secure: + # Don't even try to use the outdated ssl.PROTOCOL_SSLx + self.xmpp.ssl_version = ssl.PROTOCOL_TLSv1 + + # If the python version supports it, use highest TLS version + # automatically + if hasattr(ssl, "PROTOCOL_TLS"): + # Use the best version of TLS available to us + self.xmpp.ssl_version = ssl.PROTOCOL_TLS + + self.xmpp.ca_certs = None + if self.verify_certificate: + # Set the ca_certs variable for certificate verification + self.xmpp.ca_certs = next( + (cert for cert in self.CA_CERTIFICATE_FILE_LOCATIONS + if isfile(cert)), None) + + if self.xmpp.ca_certs is None: + self.logger.warning( + 'XMPP Secure comunication can not be verified; ' + 'no local CA certificate file') + return False + + # We're good + return True + + def process(self): + """ + Thread that handles the server/client i/o + + """ + + # Establish connection to XMPP server. + # To speed up sending messages, don't use the "reattempt" feature, + # it will add a nasty delay even before connecting to XMPP server. + if not self.xmpp.connect((self.host, self.port), + use_ssl=self.secure, reattempt=False): + + default_port = self.default_secure_port \ + if self.secure else self.default_unsecure_port + + default_schema = self.secure_protocol \ + if self.secure else self.protocol + + # Log connection issue + self.logger.warning( + 'Failed to authenticate {jid} with: {schema}://{host}{port}' + .format( + jid=self.jid, + schema=default_schema, + host=self.host, + port='' if not self.port or self.port == default_port + else ':{}'.format(self.port), + )) + return False + + # Process XMPP communication. + self.xmpp.process(block=True) + + return self.success + + def session_start(self, *args, **kwargs): + """ + Session Manager + """ + + targets = list(self.targets) + if not targets: + # We always default to notifying ourselves + targets.append(self.jid) + + while len(targets) > 0: + + # Get next target (via JID) + target = targets.pop(0) + + # Invoke "before_message" event hook. + self.before_message() + + # The message we wish to send, and the JID that will receive it. + self.xmpp.send_message(mto=target, mbody=self.body, mtype='chat') + + # Using wait=True ensures that the send queue will be + # emptied before ending the session. + self.xmpp.disconnect(wait=True) + + # Toggle our success flag + self.success = True diff --git a/libs/apprise/plugins/NotifyXMPP.py b/libs/apprise/plugins/NotifyXMPP/__init__.py similarity index 76% rename from libs/apprise/plugins/NotifyXMPP.py rename to libs/apprise/plugins/NotifyXMPP/__init__.py index 82623cb45..a1cd0073a 100644 --- a/libs/apprise/plugins/NotifyXMPP.py +++ b/libs/apprise/plugins/NotifyXMPP/__init__.py @@ -24,46 +24,17 @@ # THE SOFTWARE. import re -import ssl -from os.path import isfile -from .NotifyBase import NotifyBase -from ..URLBase import PrivacyMode -from ..common import NotifyType -from ..utils import parse_list -from ..AppriseLocale import gettext_lazy as _ +from ..NotifyBase import NotifyBase +from ...URLBase import PrivacyMode +from ...common import NotifyType +from ...utils import parse_list +from ...AppriseLocale import gettext_lazy as _ +from .SleekXmppAdapter import SleekXmppAdapter # xep string parser XEP_PARSE_RE = re.compile('^[^1-9]*(?P[1-9][0-9]{0,3})$') -# Default our global support flag -NOTIFY_XMPP_SUPPORT_ENABLED = False - -# Taken from https://golang.org/src/crypto/x509/root_linux.go -CA_CERTIFICATE_FILE_LOCATIONS = [ - # Debian/Ubuntu/Gentoo etc. - "/etc/ssl/certs/ca-certificates.crt", - # Fedora/RHEL 6 - "/etc/pki/tls/certs/ca-bundle.crt", - # OpenSUSE - "/etc/ssl/ca-bundle.pem", - # OpenELEC - "/etc/pki/tls/cacert.pem", - # CentOS/RHEL 7 - "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", -] - -try: - # Import sleekxmpp if available - import sleekxmpp - - NOTIFY_XMPP_SUPPORT_ENABLED = True - -except ImportError: - # No problem; we just simply can't support this plugin because we're - # either using Linux, or simply do not have sleekxmpp installed. - pass - class NotifyXMPP(NotifyBase): """ @@ -82,6 +53,9 @@ class NotifyXMPP(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_xmpp' + # Lower throttle rate for XMPP + request_rate_per_sec = 0.5 + # The default XMPP port default_unsecure_port = 5222 @@ -98,7 +72,7 @@ class NotifyXMPP(NotifyBase): # If anyone is seeing this had knows a better way of testing this # outside of what is defined in test/test_xmpp_plugin.py, please # let me know! :) - _enabled = NOTIFY_XMPP_SUPPORT_ENABLED + _enabled = SleekXmppAdapter._enabled # Define object templates templates = ( @@ -231,10 +205,11 @@ class NotifyXMPP(NotifyBase): result = XEP_PARSE_RE.match(xep) if result is not None: self.xep.append(int(result.group('xep'))) + self.logger.debug('Loaded XMPP {}'.format(xep)) else: self.logger.warning( - "Could not load XMPP xep {}".format(xep)) + "Could not load XMPP {}".format(xep)) # By default we send ourselves a message if targets: @@ -267,34 +242,7 @@ class NotifyXMPP(NotifyBase): jid = self.host password = self.password if self.password else self.user - # Prepare our object - xmpp = sleekxmpp.ClientXMPP(jid, password) - - for xep in self.xep: - # Load xep entries - xmpp.register_plugin('xep_{0:04d}'.format(xep)) - - if self.secure: - xmpp.ssl_version = ssl.PROTOCOL_TLSv1 - # If the python version supports it, use highest TLS version - # automatically - if hasattr(ssl, "PROTOCOL_TLS"): - # Use the best version of TLS available to us - xmpp.ssl_version = ssl.PROTOCOL_TLS - - xmpp.ca_certs = None - if self.verify_certificate: - # Set the ca_certs variable for certificate verification - xmpp.ca_certs = next( - (cert for cert in CA_CERTIFICATE_FILE_LOCATIONS - if isfile(cert)), None) - - if xmpp.ca_certs is None: - self.logger.warning( - 'XMPP Secure comunication can not be verified; ' - 'no CA certificate found') - - # Acquire our port number + # Compute port number if not self.port: port = self.default_secure_port \ if self.secure else self.default_unsecure_port @@ -302,48 +250,22 @@ class NotifyXMPP(NotifyBase): else: port = self.port - # Establish our connection - if not xmpp.connect((self.host, port)): - return False - - xmpp.send_presence() - try: - xmpp.get_roster() + # Communicate with XMPP. + xmpp_adapter = SleekXmppAdapter( + host=self.host, port=port, secure=self.secure, + verify_certificate=self.verify_certificate, xep=self.xep, + jid=jid, password=password, body=body, targets=self.targets, + before_message=self.throttle, logger=self.logger) - except sleekxmpp.exceptions.IqError as e: - self.logger.warning('There was an error getting the XMPP roster.') - self.logger.debug(e.iq['error']['condition']) - xmpp.disconnect() + except ValueError: + # We failed return False - except sleekxmpp.exceptions.IqTimeout: - self.logger.warning('XMPP Server is taking too long to respond.') - xmpp.disconnect() - return False + # Initialize XMPP machinery and begin processing the XML stream. + outcome = xmpp_adapter.process() - targets = list(self.targets) - if not targets: - # We always default to notifying ourselves - targets.append(jid) - - while len(targets) > 0: - - # Get next target (via JID) - target = targets.pop(0) - - # Always call throttle before any remote server i/o is made - self.throttle() - - # The message we wish to send, and the JID that - # will receive it. - xmpp.send_message(mto=target, mbody=body, mtype='chat') - - # Using wait=True ensures that the send queue will be - # emptied before ending the session. - xmpp.disconnect(wait=True) - - return True + return outcome def url(self, privacy=False, *args, **kwargs): """ diff --git a/libs/apprise/plugins/__init__.py b/libs/apprise/plugins/__init__.py index 21ff47fcd..fd41cb7fd 100644 --- a/libs/apprise/plugins/__init__.py +++ b/libs/apprise/plugins/__init__.py @@ -34,6 +34,7 @@ from os.path import abspath # Used for testing from . import NotifyEmail as NotifyEmailBase from .NotifyGrowl import gntp +from .NotifyXMPP import SleekXmppAdapter # NotifyBase object is passed in as a module not class from . import NotifyBase @@ -63,6 +64,9 @@ __all__ = [ # gntp (used for NotifyGrowl Testing) 'gntp', + + # sleekxmpp access points (used for NotifyXMPP Testing) + 'SleekXmppAdapter', ] # we mirror our base purely for the ability to reset everything; this From 605eeced566fbd36573a56690a2715fce25122cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Mon, 30 Mar 2020 21:08:11 -0400 Subject: [PATCH 03/14] Fix for #898 --- libs/subliminal_patch/providers/legendasdivx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/subliminal_patch/providers/legendasdivx.py b/libs/subliminal_patch/providers/legendasdivx.py index aa9f2a5f0..6247792af 100644 --- a/libs/subliminal_patch/providers/legendasdivx.py +++ b/libs/subliminal_patch/providers/legendasdivx.py @@ -118,7 +118,7 @@ class LegendasdivxProvider(Provider): 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', + 'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2"), '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', From 3e21376fe64ca08f92a9e8599f32e38b6983386a Mon Sep 17 00:00:00 2001 From: josdion Date: Tue, 31 Mar 2020 19:29:51 +0300 Subject: [PATCH 04/14] subsunacs - improve finding uploader user name --- libs/subliminal_patch/providers/subsunacs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/subliminal_patch/providers/subsunacs.py b/libs/subliminal_patch/providers/subsunacs.py index 9728e4c78..87c97c486 100644 --- a/libs/subliminal_patch/providers/subsunacs.py +++ b/libs/subliminal_patch/providers/subsunacs.py @@ -145,7 +145,7 @@ class SubsUnacsProvider(Provider): element = a_element_wrapper.find('a', {'class': 'tooltip'}) if element: link = element.get('href') - element = row.find('a', href = re.compile(r'.*/search\.php\?t=1\&memid=.*')) + element = row.find('a', href = re.compile(r'.*/search\.php\?t=1\&(memid|u)=.*')) uploader = element.get_text() if element else None logger.info('Found subtitle link %r', link) sub = self.download_archive_and_add_subtitle_files('https://subsunacs.net' + link, language, video) From 286a216ab6c9e3df56febb231ba27c3faed79720 Mon Sep 17 00:00:00 2001 From: josdion Date: Fri, 3 Apr 2020 20:18:44 +0300 Subject: [PATCH 05/14] Fix #896 - Improved release info - Remove search in alternative titles - Read the best scored subtitles from archive --- libs/subliminal_patch/providers/subdivx.py | 86 ++++++++++++---------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/libs/subliminal_patch/providers/subdivx.py b/libs/subliminal_patch/providers/subdivx.py index 6b693f391..8d04ad0fc 100644 --- a/libs/subliminal_patch/providers/subdivx.py +++ b/libs/subliminal_patch/providers/subdivx.py @@ -17,11 +17,15 @@ else: from subliminal import __short_version__ from subliminal.exceptions import ServiceUnavailable -from subliminal.providers import ParserBeautifulSoup, Provider -from subliminal.subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending,guess_matches +from subliminal.providers import ParserBeautifulSoup +from subliminal.subtitle import SUBTITLE_EXTENSIONS, fix_line_ending,guess_matches from subliminal.video import Episode, Movie from subliminal_patch.exceptions import APIThrottled from six.moves import range +from subliminal_patch.score import get_scores +from subliminal_patch.subtitle import Subtitle +from subliminal_patch.providers import Provider +from guessit import guessit logger = logging.getLogger(__name__) @@ -30,20 +34,20 @@ class SubdivxSubtitle(Subtitle): provider_name = 'subdivx' hash_verifiable = False - def __init__(self, language, page_link, title, description, uploader): + def __init__(self, language, video, page_link, title, description, uploader): super(SubdivxSubtitle, self).__init__(language, hearing_impaired=False, page_link=page_link) + self.video = video self.title = title self.description = description self.uploader = uploader + self.release_info = self.title + if self.description and self.description.strip(): + self.release_info += ' | ' + self.description @property def id(self): return self.page_link - @property - def release_info(self): - return self.description - def get_matches(self, video): matches = set() @@ -112,12 +116,14 @@ class SubdivxSubtitlesProvider(Provider): def terminate(self): self.session.close() - def query(self, keyword, season=None, episode=None, year=None): - query = keyword - if season and episode: - query += ' S{season:02d}E{episode:02d}'.format(season=season, episode=episode) - elif year: - query += ' {:4d}'.format(year) + def query(self, video, languages): + + if isinstance(video, Episode): + query = "{} S{:02d}E{:02d}".format(video.series, video.season, video.episode) + else: + query = video.title + if video.year: + query += ' {:4d}'.format(video.year) params = { 'q': query, # search string @@ -135,7 +141,7 @@ class SubdivxSubtitlesProvider(Provider): self._check_response(response) try: - page_subtitles = self._parse_subtitles_page(response, language) + page_subtitles = self._parse_subtitles_page(video, response, language) except Exception as e: logger.error('Error parsing subtitles list: ' + str(e)) break @@ -151,24 +157,7 @@ class SubdivxSubtitlesProvider(Provider): return subtitles def list_subtitles(self, video, languages): - if isinstance(video, Episode): - titles = [video.series] + video.alternative_series - elif isinstance(video, Movie): - titles = [video.title] + video.alternative_titles - else: - titles = [] - - subtitles = [] - for title in titles: - if isinstance(video, Episode): - subtitles += [s for s in self.query(title, season=video.season, - episode=video.episode, year=video.year) - if s.language in languages] - elif isinstance(video, Movie): - subtitles += [s for s in self.query(title, year=video.year) - if s.language in languages] - - return subtitles + return self.query(video, languages) def download_subtitle(self, subtitle): if isinstance(subtitle, SubdivxSubtitle): @@ -186,14 +175,14 @@ class SubdivxSubtitlesProvider(Provider): archive = self._get_archive(response.content) # extract the subtitle - subtitle_content = self._get_subtitle_from_archive(archive) + subtitle_content = self._get_subtitle_from_archive(archive, subtitle) subtitle.content = fix_line_ending(subtitle_content) def _check_response(self, response): if response.status_code != 200: raise ServiceUnavailable('Bad status code: ' + str(response.status_code)) - def _parse_subtitles_page(self, response, language): + def _parse_subtitles_page(self, video, response, language): subtitles = [] page_soup = ParserBeautifulSoup(response.content.decode('iso-8859-1', 'ignore'), ['lxml', 'html.parser']) @@ -214,7 +203,7 @@ class SubdivxSubtitlesProvider(Provider): # uploader uploader = body_soup.find("a", {'class': 'link1'}).text - subtitle = self.subtitle_class(language, page_link, title, description, uploader) + subtitle = self.subtitle_class(language, video, page_link, title, description, uploader) logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) @@ -253,7 +242,10 @@ class SubdivxSubtitlesProvider(Provider): return archive - def _get_subtitle_from_archive(self, archive): + def _get_subtitle_from_archive(self, archive, subtitle): + _max_score = 0 + _scores = get_scores (subtitle.video) + for name in archive.namelist(): # discard hidden files if os.path.split(name)[-1].startswith('.'): @@ -263,6 +255,26 @@ class SubdivxSubtitlesProvider(Provider): if not name.lower().endswith(SUBTITLE_EXTENSIONS): continue - return archive.read(name) + _guess = guessit (name) + if isinstance(subtitle.video, Episode): + logger.debug ("guessing %s" % name) + logger.debug("subtitle S{}E{} video S{}E{}".format(_guess['season'],_guess['episode'],subtitle.video.season,subtitle.video.episode)) + + if subtitle.video.episode != _guess['episode'] or subtitle.video.season != _guess['season']: + logger.debug('subtitle does not match video, skipping') + continue + + matches = set() + matches |= guess_matches (subtitle.video, _guess) + _score = sum ((_scores.get (match, 0) for match in matches)) + logger.debug('srt matches: %s, score %d' % (matches, _score)) + if _score > _max_score: + _max_name = name + _max_score = _score + logger.debug("new max: {} {}".format(name, _score)) + + if _max_score > 0: + logger.debug("returning from archive: {} scored {}".format(_max_name, _max_score)) + return archive.read(_max_name) raise APIThrottled('Can not find the subtitle in the compressed file') From 9c6663982862bb31301d561f72b0ddce91f65914 Mon Sep 17 00:00:00 2001 From: Ori Date: Wed, 8 Apr 2020 17:44:12 +0300 Subject: [PATCH 06/14] bugfix language id in wizdom provider --- libs/subliminal_patch/providers/wizdom.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/subliminal_patch/providers/wizdom.py b/libs/subliminal_patch/providers/wizdom.py index dfae411ff..f55df5167 100644 --- a/libs/subliminal_patch/providers/wizdom.py +++ b/libs/subliminal_patch/providers/wizdom.py @@ -4,7 +4,7 @@ import logging import os import zipfile -from babelfish import Language +from subzero.language import Language from guessit import guessit from requests import Session @@ -75,7 +75,7 @@ class WizdomSubtitle(Subtitle): class WizdomProvider(Provider): """Wizdom Provider.""" - languages = {Language.fromalpha2(l) for l in ['he']} + languages = {Language(l) for l in ['heb']} server_url = 'wizdom.xyz' _tmdb_api_key = 'a51ee051bcd762543373903de296e0a3' @@ -156,7 +156,7 @@ class WizdomProvider(Provider): # loop over results subtitles = {} for result in results: - language = Language.fromalpha2('he') + language = Language('heb') hearing_impaired = False subtitle_id = result['id'] release = result['version'] @@ -204,4 +204,4 @@ class WizdomProvider(Provider): if len(namelist) > 1: raise ProviderError('More than one file to unzip') - subtitle.content = fix_line_ending(zf.read(namelist[0])) \ No newline at end of file + subtitle.content = fix_line_ending(zf.read(namelist[0])) From 8dad86c8aa90edf69d7ae87b34731d65574c371f Mon Sep 17 00:00:00 2001 From: Ori Date: Wed, 8 Apr 2020 18:39:38 +0300 Subject: [PATCH 07/14] wizdom: some subtitle files are empty, skip --- libs/subliminal_patch/providers/wizdom.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/subliminal_patch/providers/wizdom.py b/libs/subliminal_patch/providers/wizdom.py index f55df5167..44808a3e8 100644 --- a/libs/subliminal_patch/providers/wizdom.py +++ b/libs/subliminal_patch/providers/wizdom.py @@ -197,6 +197,9 @@ class WizdomProvider(Provider): r = self.session.get(url, headers={'Referer': subtitle.page_link}, timeout=10) r.raise_for_status() + if len(r.content) == 0: + return + # open the zip with zipfile.ZipFile(io.BytesIO(r.content)) as zf: # remove some filenames from the namelist From 2a2e0d8c2ee9b935c4e3e1d95d08d9476ad9b828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Fri, 10 Apr 2020 09:35:04 -0400 Subject: [PATCH 08/14] Changed donate link to use Paypal as Beerpay.io is closing. --- README.md | 7 ++----- views/menu.tpl | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 92f4efa06..71baf783a 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,9 @@ Bazarr is a companion application to Sonarr and Radarr. It manages and downloads Be aware that Bazarr doesn't scan disk to detect series and movies: It only takes care of the series and movies that are indexed in Sonarr and Radarr. -## Support on Beerpay +## Support on Paypal At the request of some, here is a way to demonstrate your appreciation for the efforts made in the development of Bazarr: -[![Beerpay](https://beerpay.io/morpheus65535/bazarr/badge.svg?style=beer-square)](https://beerpay.io/morpheus65535/bazarr) - -You can also make a wish but keep in mind that we do not commit to make it happen: -[![Beerpay](https://beerpay.io/morpheus65535/bazarr/make-wish.svg?style=flat-square)](https://beerpay.io/morpheus65535/bazarr?focus=wish) +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url) # Status [![GitHub issues](https://img.shields.io/github/issues/morpheus65535/bazarr.svg?style=flat-square)](https://github.com/morpheus65535/bazarr/issues) diff --git a/views/menu.tpl b/views/menu.tpl index ed296bd53..fbfc6f269 100644 --- a/views/menu.tpl +++ b/views/menu.tpl @@ -134,7 +134,7 @@ System - @@ -203,7 +203,7 @@ System - From cab52af42e424f57cb927e545a8dfab21ef9361b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Sat, 11 Apr 2020 08:04:14 -0400 Subject: [PATCH 09/14] Fixed small deprecation with logging.warn --- bazarr/check_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bazarr/check_update.py b/bazarr/check_update.py index 965973828..4938724a2 100644 --- a/bazarr/check_update.py +++ b/bazarr/check_update.py @@ -60,7 +60,7 @@ def check_and_apply_update(): if releases is None: notifications.write(msg='Could not get releases from GitHub.', queue='check_update', type='warning') - logging.warn('BAZARR Could not get releases from GitHub.') + logging.warning('BAZARR Could not get releases from GitHub.') return else: release = releases[0] From 8f1d6883e72ba0498c6b1afe25da2d61244268d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Sat, 11 Apr 2020 08:05:12 -0400 Subject: [PATCH 10/14] Increased minimal version of Python 3.x to 3.7 instead of 3.6. We have too much trouble with 3.6 and some of our dependencies don't support it anymore. --- bazarr.py | 2 +- views/menu.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bazarr.py b/bazarr.py index 8f1b7da03..581689f76 100644 --- a/bazarr.py +++ b/bazarr.py @@ -14,7 +14,7 @@ from libs.six import PY3 def check_python_version(): python_version = platform.python_version_tuple() minimum_py2_tuple = (2, 7, 13) - minimum_py3_tuple = (3, 6, 0) + minimum_py3_tuple = (3, 7, 0) minimum_py2_str = ".".join(str(i) for i in minimum_py2_tuple) minimum_py3_str = ".".join(str(i) for i in minimum_py3_tuple) diff --git a/views/menu.tpl b/views/menu.tpl index fbfc6f269..db33bde24 100644 --- a/views/menu.tpl +++ b/views/menu.tpl @@ -237,7 +237,7 @@
Python deprecation warning
-

Bazarr won't update anymore until you upgrade Python!

Bazarr is now compatible with Python 3.6 and newer. You must upgrade Python as we don't support Python 2.x anymore. +

Bazarr won't update anymore until you upgrade Python!

Bazarr is now compatible with Python 3.7 and newer. You must upgrade Python as we don't support Python 2.x anymore.
% if os.name == 'posix':
If you are running under Docker, don't worry, we'll take care of this for you. Just pull the new image.
From 93b95f993652b723a611c6280fce2e27b0f9b751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Sat, 11 Apr 2020 10:43:03 -0400 Subject: [PATCH 11/14] Fix for #920. --- bazarr/get_subtitle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index 788f8d261..232d62858 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -481,6 +481,10 @@ def manual_upload_subtitle(path, language, forced, title, scene_name, media_type chmod = int(settings.general.chmod, 8) if not sys.platform.startswith( 'win') and settings.general.getboolean('chmod_enabled') else None + dest_directory = get_target_folder(path) + fake_video_path = None + if dest_directory: + fake_video_path = os.path.join(dest_directory, os.path.split(path)[1]) _, ext = os.path.splitext(subtitle.filename) language = alpha3_from_alpha2(language) @@ -493,7 +497,7 @@ def manual_upload_subtitle(path, language, forced, title, scene_name, media_type if forced: lang_obj = Language.rebuild(lang_obj, forced=True) - subtitle_path = get_subtitle_path(video_path=force_unicode(path), + subtitle_path = get_subtitle_path(video_path=force_unicode(fake_video_path if fake_video_path else path), language=None if single else lang_obj, extension=ext, forced_tag=forced) From 2ee437e45b6bc437f930ccc7e44dfcc6f2e4c609 Mon Sep 17 00:00:00 2001 From: Smaarn Date: Sun, 5 Apr 2020 02:30:27 +0200 Subject: [PATCH 12/14] [Python 3 only] Changed SIGTERM signal handling. Now the first SIGTERM will propagate a SIGINT to child processes. A second SIGTERM signal would send a SIGTERM to child processes and "interrupt" itself. A third SIGTERM signal would actually follow the default system behaviour. # Conflicts: # bazarr.py --- bazarr.py | 48 +++++++++++++++--------------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/bazarr.py b/bazarr.py index 581689f76..9d90e0619 100644 --- a/bazarr.py +++ b/bazarr.py @@ -55,34 +55,6 @@ class DaemonStatus(ProcessRegistry): def unregister(self, process): self.__processes.remove(process) - @staticmethod - def __wait_for_processes(processes, timeout): - """ - Waits all the provided processes for the specified amount of time in seconds. - """ - 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 subprocess.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 - @staticmethod def __send_signal(processes, signal_no, live_processes=None): """ @@ -105,9 +77,11 @@ class DaemonStatus(ProcessRegistry): Flags this instance as should stop and terminates as smoothly as possible children processes. """ 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) + DaemonStatus.__send_signal(self.__processes, signal.SIGINT, list()) + + def force_stop(self): + self.__should_stop = True + DaemonStatus.__send_signal(self.__processes, signal.SIGTERM) def should_stop(self): return self.__should_stop @@ -170,11 +144,19 @@ if __name__ == '__main__': if PY3: daemonStatus = DaemonStatus() + def force_shutdown(): + # force the killing of children processes + daemonStatus.force_stop() + # if a new SIGTERM signal is caught the standard behaviour should be followed + signal.signal(signal.SIGTERM, signal.SIG_DFL) + # emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break") + os.kill(os.getpid(), signal.SIGINT) + 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) + # if a new sigterm signal is caught it should force the shutdown of children processes + signal.signal(signal.SIGTERM, lambda signal_no, frame: force_shutdown()) signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown()) From f44f0f41f4a1912e28efd540a7042f82a5526b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Sun, 12 Apr 2020 08:49:28 -0400 Subject: [PATCH 13/14] Version bump --- bazarr/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bazarr/main.py b/bazarr/main.py index f96b1e810..24f2c9b39 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -1,6 +1,6 @@ # coding=utf-8 -bazarr_version = '0.8.4.3' +bazarr_version = '0.8.4.4' import os os.environ["SZ_USER_AGENT"] = "Bazarr/1" From 2bcb5a6030da2e55a24231311c056ecc40d14c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Sun, 12 Apr 2020 08:50:25 -0400 Subject: [PATCH 14/14] Bazarr won't start anymore when launched with Python ealier than 3.7. No more compatibility with 2.7.x --- bazarr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bazarr.py b/bazarr.py index 581689f76..dad287ab5 100644 --- a/bazarr.py +++ b/bazarr.py @@ -18,13 +18,13 @@ def check_python_version(): minimum_py2_str = ".".join(str(i) for i in minimum_py2_tuple) minimum_py3_str = ".".join(str(i) for i in minimum_py3_tuple) - if (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \ - (int(python_version[0]) != minimum_py3_tuple[0] and int(python_version[0]) != minimum_py2_tuple[0]): + if int(python_version[0]) < 3: print("Python " + minimum_py3_str + " or greater required. " "Current version is " + platform.python_version() + ". Please upgrade Python.") sys.exit(1) - elif int(python_version[0]) == minimum_py2_tuple[0] and int(python_version[1]) < minimum_py2_tuple[1]: - print("Python " + minimum_py2_str + " or greater required. " + elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \ + (int(python_version[0]) != minimum_py3_tuple[0] and int(python_version[0]) != minimum_py2_tuple[0]): + print("Python " + minimum_py3_str + " or greater required. " "Current version is " + platform.python_version() + ". Please upgrade Python.") sys.exit(1)