From 9e22f571315026d2bbca68bf92ed6997613e8aba Mon Sep 17 00:00:00 2001
From: romanvm <roman1972@gmail.com>
Date: Mon, 24 Jun 2024 09:16:05 +0000
Subject: [PATCH] [service.subtitles.rvm.addic7ed] 3.2.0

---
 .../addic7ed/actions.py                       | 150 +++++------
 .../addic7ed/addon.py                         |  40 +--
 .../addic7ed/exception_logger.py              | 154 +++++++-----
 .../addic7ed/exceptions.py                    |  19 +-
 .../addic7ed/parser.py                        | 148 +++++++----
 .../addic7ed/simple_requests.py               | 235 ++++++++++++++++++
 .../addic7ed/utils.py                         | 181 ++++++--------
 .../addic7ed/webclient.py                     |  82 +++---
 service.subtitles.rvm.addic7ed/addon.xml      |  12 +-
 service.subtitles.rvm.addic7ed/main.py        |  25 +-
 10 files changed, 704 insertions(+), 342 deletions(-)
 create mode 100644 service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py

diff --git a/service.subtitles.rvm.addic7ed/addic7ed/actions.py b/service.subtitles.rvm.addic7ed/addic7ed/actions.py
index 8411f91ec..f16a9116d 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/actions.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/actions.py
@@ -1,32 +1,49 @@
-# coding: utf-8
-# Created on: 07.04.2016
-# Author: Roman Miroshnychenko aka Roman V.M. (roman1972@gmail.com)
+# Copyright (C) 2016, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-from __future__ import absolute_import, unicode_literals
+import logging
 import os
-import sys
 import re
 import shutil
+import sys
 from collections import namedtuple
-from six import PY2
-from six.moves import urllib_parse as urlparse
-from kodi_six import xbmc, xbmcplugin, xbmcgui
-from . import parser
-from .addon import addon, profile, get_ui_string, icon
-from .exceptions import DailyLimitError, ParseError, SubsSearchError, \
+from urllib import parse as urlparse
+
+import xbmc
+import xbmcgui
+import xbmcplugin
+
+from addic7ed import parser
+from addic7ed.addon import ADDON, PROFILE, ICON, get_ui_string
+from addic7ed.exceptions import NoSubtitlesReturned, ParseError, SubsSearchError, \
     Add7ConnectionError
-from .utils import logger, get_languages, get_now_played, parse_filename, \
-    normalize_showname
+from addic7ed.parser import parse_filename, normalize_showname, get_languages
+from addic7ed.utils import get_now_played
+from addic7ed.webclient import Session
 
 __all__ = ['router']
 
-temp_dir = os.path.join(profile, 'temp')
-handle = int(sys.argv[1])
+logger = logging.getLogger(__name__)
+
+TEMP_DIR = os.path.join(PROFILE, 'temp')
+HANDLE = int(sys.argv[1])
 
 
-VIDEOFILES = {'.avi', '.mkv', '.mp4', '.ts', '.m2ts', '.mov'}
-dialog = xbmcgui.Dialog()
-release_re = re.compile(r'-(.*?)(?:\[.*?\])?\.')
+VIDEOFILE_EXTENSIONS = {'.avi', '.mkv', '.mp4', '.ts', '.m2ts', '.mov'}
+DIALOG = xbmcgui.Dialog()
+RELEASE_RE = re.compile(r'-(.*?)(?:\[.*?\])?\.')
 
 EpisodeData = namedtuple('EpisodeData',
                          ['showname', 'season', 'episode', 'filename'])
@@ -42,13 +59,13 @@ def _detect_synced_subs(subs_list, filename):
     """
     listing = []
     for item in subs_list:
-        release_match = release_re.search(filename)
+        release_match = RELEASE_RE.search(filename)
         if release_match is not None:
             release = release_match.group(1).lower()
         else:
             release = ''
         lowercase_version = item.version.lower()
-        resync_pattern = r'sync.+?{}'.format(release)
+        resync_pattern = rf'sync.+?{release}'
         synced = (release and
                   release in lowercase_version and
                   re.search(resync_pattern, lowercase_version, re.I) is None)
@@ -92,7 +109,7 @@ def display_subs(subs_list, episode_url, filename):
             list_item.setProperty('hearing_imp', 'true')
         if synced:
             list_item.setProperty('sync', 'true')
-        url = '{0}?{1}'.format(
+        url = '{}?{}'.format(  # pylint: disable=consider-using-f-string
             sys.argv[0],
             urlparse.urlencode(
                 {'action': 'download',
@@ -101,7 +118,7 @@ def display_subs(subs_list, episode_url, filename):
                  'filename': filename}
             )
         )
-        xbmcplugin.addDirectoryItem(handle=handle, url=url, listitem=list_item,
+        xbmcplugin.addDirectoryItem(handle=HANDLE, url=url, listitem=list_item,
                                     isFolder=False)
 
 
@@ -118,22 +135,22 @@ def download_subs(link, referrer, filename):
         label - the download location for subs.
     """
     # Re-create a download location in a temporary folder
-    if not os.path.exists(profile):
-        os.mkdir(profile)
-    if os.path.exists(temp_dir):
-        shutil.rmtree(temp_dir)
-    os.mkdir(temp_dir)
+    if not os.path.exists(PROFILE):
+        os.mkdir(PROFILE)
+    if os.path.exists(TEMP_DIR):
+        shutil.rmtree(TEMP_DIR)
+    os.mkdir(TEMP_DIR)
     # Combine a path where to download the subs
     filename = os.path.splitext(filename)[0] + '.srt'
-    subspath = os.path.join(temp_dir, filename)
+    subspath = os.path.join(TEMP_DIR, filename)
     # Download the subs from addic7ed.com
     try:
-        parser.download_subs(link, referrer, subspath)
+        Session().download_subs(link, referrer, subspath)
     except Add7ConnectionError:
         logger.error('Unable to connect to addic7ed.com')
-        dialog.notification(get_ui_string(32002), get_ui_string(32005), 'error')
-    except DailyLimitError:
-        dialog.notification(get_ui_string(32002), get_ui_string(32003), 'error',
+        DIALOG.notification(get_ui_string(32002), get_ui_string(32005), 'error')
+    except NoSubtitlesReturned:
+        DIALOG.notification(get_ui_string(32002), get_ui_string(32003), 'error',
                             3000)
         logger.error('Exceeded daily limit for subs downloads.')
     else:
@@ -144,11 +161,11 @@ def download_subs(link, referrer, filename):
         # in 'Settings > Video > Subtitles' section.
         # A 2-letter language code will be added to subs filename.
         list_item = xbmcgui.ListItem(label=subspath)
-        xbmcplugin.addDirectoryItem(handle=handle,
+        xbmcplugin.addDirectoryItem(handle=HANDLE,
                                     url=subspath,
                                     listitem=list_item,
                                     isFolder=False)
-        dialog.notification(get_ui_string(32000), get_ui_string(32001), icon,
+        DIALOG.notification(get_ui_string(32000), get_ui_string(32001), ICON,
                             3000, False)
         logger.info('Subs downloaded.')
 
@@ -161,44 +178,41 @@ def extract_episode_data():
     :raises ParseError: if cannot determine episode data
     """
     now_played = get_now_played()
+    logger.debug('Played file info: %s', now_played)
+    showname = now_played['showtitle'] or xbmc.getInfoLabel('VideoPlayer.TVshowtitle')
     parsed = urlparse.urlparse(now_played['file'])
     filename = os.path.basename(parsed.path)
-    if addon.getSetting('use_filename') == 'true' or not now_played['showtitle']:
+    if ADDON.getSetting('use_filename') == 'true' or not showname:
         # Try to get showname/season/episode data from
         # the filename if 'use_filename' setting is true
         # or if the video-file does not have library metadata.
         try:
-            logger.debug('Using filename: {0}'.format(filename))
+            logger.debug('Using filename: %s', filename)
             showname, season, episode = parse_filename(filename)
         except ParseError:
-            logger.debug(
-                'Filename {0} failed. Trying ListItem.Label...'.format(filename)
-            )
+            logger.debug('Filename %s failed. Trying ListItem.Label...', filename)
             try:
                 filename = now_played['label']
-                logger.debug('Using filename: {0}'.format(filename))
+                logger.debug('Using filename: %s', filename)
                 showname, season, episode = parse_filename(filename)
             except ParseError:
-                logger.error(
-                    'Unable to determine episode data for {0}'.format(filename)
-                )
-                dialog.notification(get_ui_string(32002), get_ui_string(32006),
+                logger.error('Unable to determine episode data for %s', filename)
+                DIALOG.notification(get_ui_string(32002), get_ui_string(32006),
                                     'error', 3000)
                 raise
     else:
         # Get get showname/season/episode data from
         # Kodi if the video-file is being played from
         # the TV-Shows library.
-        showname = now_played['showtitle']
-        season = str(now_played['season']).zfill(2)
-        episode = str(now_played['episode']).zfill(2)
-        if not os.path.splitext(filename)[1].lower() in VIDEOFILES:
-            filename = '{0}.{1}x{2}.foo'.format(
-                showname, season, episode
-            )
-        logger.debug('Using library metadata: {0} - {1}x{2}'.format(
-            showname, season, episode)
-        )
+        season = str(now_played['season'] if now_played['season'] > -1
+                     else xbmc.getInfoLabel('VideoPlayer.Season'))
+        season = season.zfill(2)
+        episode = str(now_played['episode'] if now_played['episode'] > -1
+                      else xbmc.getInfoLabel('VideoPlayer.Episode'))
+        episode = episode.zfill(2)
+        if not os.path.splitext(filename)[1].lower() in VIDEOFILE_EXTENSIONS:
+            filename = f'{showname}.{season}x{episode}.foo'
+        logger.debug('Using library metadata: %s - %sx%s', showname, season, episode)
     return EpisodeData(showname, season, episode, filename)
 
 
@@ -214,31 +228,28 @@ def search_subs(params):
         except ParseError:
             return
         # Create a search query string
-        query = '{0} {1}x{2}'.format(
-            normalize_showname(episode_data.showname),
-            episode_data.season,
-            episode_data.episode
-        )
+        showname = normalize_showname(episode_data.showname)
+        query = f'{showname} {episode_data.season}x{episode_data.episode}'
         filename = episode_data.filename
     else:
         # Get the query string typed on the on-screen keyboard
         query = params['searchstring']
         filename = query
     if query:
-        logger.debug('Search query: {0}'.format(query))
+        logger.debug('Search query: %s', query)
         try:
             results = parser.search_episode(query, languages)
         except Add7ConnectionError:
             logger.error('Unable to connect to addic7ed.com')
-            dialog.notification(
+            DIALOG.notification(
                 get_ui_string(32002), get_ui_string(32005), 'error'
             )
         except SubsSearchError:
-            logger.info('No subs for "{}" found.'.format(query))
+            logger.info('No subs for "%s" found.', query)
         else:
             if isinstance(results, list):
-                logger.info('Multiple episodes found:\n{0}'.format(results))
-                i = dialog.select(
+                logger.info('Multiple episodes found:\n%s', results)
+                i = DIALOG.select(
                     get_ui_string(32008), [item.title for item in results]
                 )
                 if i >= 0:
@@ -246,7 +257,7 @@ def search_subs(params):
                         results = parser.get_episode(results[i].link, languages)
                     except Add7ConnectionError:
                         logger.error('Unable to connect to addic7ed.com')
-                        dialog.notification(get_ui_string(32002),
+                        DIALOG.notification(get_ui_string(32002),
                                             get_ui_string(32005), 'error')
                         return
                     except SubsSearchError:
@@ -255,9 +266,8 @@ def search_subs(params):
                 else:
                     logger.info('Episode selection cancelled.')
                     return
-            logger.info('Found subs for "{0}"'.format(query))
-            display_subs(results.subtitles, results.episode_url,
-                         filename)
+            logger.info('Found subs for "%s"', query)
+            display_subs(results.subtitles, results.episode_url, filename)
 
 
 def router(paramstring):
@@ -268,8 +278,6 @@ def router(paramstring):
     :type paramstring: str
     """
     # Get plugin call params
-    if PY2:
-        paramstring = urlparse.unquote(paramstring).decode('utf-8')
     params = dict(urlparse.parse_qsl(paramstring))
     if params['action'] in ('search', 'manualsearch'):
         # Search and display subs.
@@ -279,4 +287,4 @@ def router(paramstring):
             params['link'], params['ref'],
             urlparse.unquote(params['filename'])
         )
-    xbmcplugin.endOfDirectory(handle)
+    xbmcplugin.endOfDirectory(HANDLE)
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/addon.py b/service.subtitles.rvm.addic7ed/addic7ed/addon.py
index 80db3e1d5..1fd95a245 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/addon.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/addon.py
@@ -1,23 +1,31 @@
-# coding: utf-8
-
-from __future__ import unicode_literals
+# Copyright (C) 2016, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import os
 
-from kodi_six import xbmcaddon
-
-try:
-    from kodi_six.xbmcvfs import translatePath
-except (ImportError, AttributeError):
-    from kodi_six.xbmc import translatePath
+import xbmcaddon
+from xbmcvfs import translatePath
 
-__all__ = ['ADDON_ID', 'addon', 'path', 'profile', 'icon', 'get_ui_string']
+__all__ = ['ADDON_ID', 'ADDON', 'ADDON_VERSION', 'PATH', 'PROFILE', 'ICON', 'get_ui_string']
 
-ADDON_ID = 'service.subtitles.rvm.addic7ed'
-addon = xbmcaddon.Addon(ADDON_ID)
-path = translatePath(addon.getAddonInfo('path'))
-profile = translatePath(addon.getAddonInfo('profile'))
-icon = os.path.join(path, 'icon.png')
+ADDON = xbmcaddon.Addon()
+ADDON_ID = ADDON.getAddonInfo('id')
+ADDON_VERSION = ADDON.getAddonInfo('version')
+PATH = translatePath(ADDON.getAddonInfo('path'))
+PROFILE = translatePath(ADDON.getAddonInfo('profile'))
+ICON = os.path.join(PATH, 'icon.png')
 
 
 def get_ui_string(string_id):
@@ -27,4 +35,4 @@ def get_ui_string(string_id):
     :param string_id: UI string ID
     :return: UI string
     """
-    return addon.getLocalizedString(string_id)
+    return ADDON.getLocalizedString(string_id)
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/exception_logger.py b/service.subtitles.rvm.addic7ed/addic7ed/exception_logger.py
index e3c7b9c63..aa1c8ec2b 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/exception_logger.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/exception_logger.py
@@ -1,5 +1,4 @@
-# coding: utf-8
-# (c) Roman Miroshnychenko <roman1972@gmail.com> 2020
+# (c) Roman Miroshnychenko <roman1972@gmail.com> 2023
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -14,51 +13,44 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """Exception logger with extended diagnostic info"""
-from __future__ import absolute_import, unicode_literals
 
 import inspect
+import logging
 import sys
 from contextlib import contextmanager
 from platform import uname
 from pprint import pformat
+from typing import Any, Dict, Callable, Generator, Iterable, Optional
 
-import six
-from kodi_six import xbmc
+import xbmc
 
-from .utils import logger
+logger = logging.getLogger(__name__)
 
-try:
-    from typing import Text, Dict, Callable, Generator  # pylint: disable=unused-import
-except ImportError:
-    pass
 
-
-def _format_vars(variables):
-    # type: (dict) -> Text
+def _format_vars(variables: Dict[str, Any]) -> str:
     """
     Format variables dictionary
 
     :param variables: variables dict
     :return: formatted string with sorted ``var = val`` pairs
     """
-    var_list = [(var, val) for var, val in six.iteritems(variables)
+    var_list = [(var, val) for var, val in variables.items()
                 if not (var.startswith('__') or var.endswith('__'))]
     var_list.sort(key=lambda i: i[0])
     lines = []
     for var, val in var_list:
-        lines.append('{} = {}'.format(var, pformat(val)))
+        lines.append(f'{var} = {pformat(val)}')
     return '\n'.join(lines)
 
 
-def _format_code_context(frame_info):
-    # type: (tuple) -> Text
+def _format_code_context(frame_info: inspect.FrameInfo) -> str:
     context = ''
-    if frame_info[4] is not None:
-        for i, line in enumerate(frame_info[4], frame_info[2] - frame_info[5]):
-            if i == frame_info[2]:
-                context += '{}:>{}'.format(six.text_type(i).rjust(5), line)
+    if frame_info.code_context is not None:
+        for i, line in enumerate(frame_info.code_context, frame_info.lineno - frame_info.index):
+            if i == frame_info.lineno:
+                context += f'{str(i).rjust(5)}:>{line}'
             else:
-                context += '{}: {}'.format(six.text_type(i).rjust(5), line)
+                context += f'{str(i).rjust(5)}: {line}'
     return context
 
 
@@ -74,41 +66,97 @@ def _format_code_context(frame_info):
 """
 
 
-def _format_frame_info(frame_info):
-    # type: (tuple) -> Text
+def _format_frame_info(frame_info: inspect.FrameInfo) -> str:
     return FRAME_INFO_TEMPLATE.format(
-        file_path=frame_info[1],
-        lineno=frame_info[2],
+        file_path=frame_info.filename,
+        lineno=frame_info.lineno,
         code_context=_format_code_context(frame_info),
-        local_vars=_format_vars(frame_info[0].f_locals)
+        local_vars=_format_vars(frame_info.frame.f_locals)
     )
 
 
+STACK_TRACE_TEMPLATE = """
+####################################################################################################
+                                            Stack Trace
+====================================================================================================
+{stack_trace}
+************************************* End of diagnostic info ***************************************
+"""
+
+
+def _format_stack_trace(frames: Iterable[inspect.FrameInfo]) -> str:
+    stack_trace = ''
+    for frame_info in frames:
+        stack_trace += _format_frame_info(frame_info)
+    return STACK_TRACE_TEMPLATE.format(stack_trace=stack_trace)
+
+
 EXCEPTION_TEMPLATE = """
-*********************************** Unhandled exception detected ***********************************
 ####################################################################################################
-                                           Diagnostic info
+                                     Exception Diagnostic Info
 ----------------------------------------------------------------------------------------------------
-Exception type  : {exc_type}
-Exception value : {exc}
-System info     : {system_info}
-Python version  : {python_version}
-Kodi version    : {kodi_version}
-sys.argv        : {sys_argv}
+Exception type    : {exc_type}
+Exception message : {exc}
+System info       : {system_info}
+Python version    : {python_version}
+Kodi version      : {kodi_version}
+sys.argv          : {sys_argv}
 ----------------------------------------------------------------------------------------------------
 sys.path:
 {sys_path}
-####################################################################################################
-                                            Stack Trace
-====================================================================================================
-{stack_trace}
-************************************* End of diagnostic info ***************************************
+{stack_trace_info}
 """
 
 
+def format_trace(frames_to_exclude: int = 1) -> str:
+    """
+    Returns a pretty stack trace with code context and local variables
+
+    Stack trace info includes the following:
+
+    * File path and line number
+    * Code fragment
+    * Local variables
+
+    It allows to inspect execution state at the point of this function call
+
+    :param frames_to_exclude: How many top frames are excluded from the trace
+        to skip unnecessary info. Since each function call creates a stack frame
+        you need to exclude at least this function frame.
+    """
+    frames = inspect.stack(5)[frames_to_exclude:]
+    return _format_stack_trace(reversed(frames))
+
+
+def format_exception(exc_obj: Optional[Exception] = None) -> str:
+    """
+    Returns a pretty exception stack trace with code context and local variables
+
+    :param exc_obj: exception object (optional)
+    :raises ValueError: if no exception is being handled
+    """
+    if exc_obj is None:
+        _, exc_obj, _ = sys.exc_info()
+    if exc_obj is None:
+        raise ValueError('No exception is currently being handled')
+    stack_trace = inspect.getinnerframes(exc_obj.__traceback__, context=5)
+    stack_trace_info = _format_stack_trace(stack_trace)
+    message = EXCEPTION_TEMPLATE.format(
+        exc_type=exc_obj.__class__.__name__,
+        exc=exc_obj,
+        system_info=uname(),
+        python_version=sys.version.replace('\n', ' '),
+        kodi_version=xbmc.getInfoLabel('System.BuildVersion'),
+        sys_argv=pformat(sys.argv),
+        sys_path=pformat(sys.path),
+        stack_trace_info=stack_trace_info
+    )
+    return message
+
+
 @contextmanager
-def log_exception(logger_func=logger.error):
-    # type: (Callable[[Text], None]) -> Generator[None, None, None]
+def catch_exception(logger_func: Callable[[str], None] = logger.error
+                    ) -> Generator[None, None, None]:
     """
     Diagnostic helper context manager
 
@@ -129,7 +177,7 @@ def log_exception(logger_func=logger.error):
 
     Example::
 
-        with debug_exception():
+        with catch_exception():
             # Some risky code
             raise RuntimeError('Fatal error!')
 
@@ -139,18 +187,8 @@ def log_exception(logger_func=logger.error):
     try:
         yield
     except Exception as exc:
-        stack_trace = ''
-        for frame_info in inspect.trace(5):
-            stack_trace += _format_frame_info(frame_info)
-        message = EXCEPTION_TEMPLATE.format(
-            exc_type=exc.__class__.__name__,
-            exc=exc,
-            system_info=uname(),
-            python_version=sys.version.replace('\n', ' '),
-            kodi_version=xbmc.getInfoLabel('System.BuildVersion'),
-            sys_argv=pformat(sys.argv),
-            sys_path=pformat(sys.path),
-            stack_trace=stack_trace
-        )
-        logger_func(message)
-        raise exc
+        message = format_exception(exc)
+        # pylint: disable=line-too-long logging-not-lazy
+        logger_func('\n*********************************** Unhandled exception detected ***********************************\n'
+                    + message)
+        raise
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/exceptions.py b/service.subtitles.rvm.addic7ed/addic7ed/exceptions.py
index 8f07b126e..6731792a0 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/exceptions.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/exceptions.py
@@ -1,6 +1,17 @@
-# coding: utf-8
-# Created on: 06.04.2016
-# Author: Roman Miroshnychenko aka Roman V.M. (roman1972@gmail.com)
+# Copyright (C) 2016, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 
 class Add7Exception(Exception):
@@ -19,5 +30,5 @@ class Add7ConnectionError(Add7Exception):
     pass
 
 
-class DailyLimitError(Add7Exception):
+class NoSubtitlesReturned(Add7Exception):
     pass
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/parser.py b/service.subtitles.rvm.addic7ed/addic7ed/parser.py
index 9a90dffec..d03a5be8c 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/parser.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/parser.py
@@ -1,34 +1,60 @@
-# -*- coding: utf-8 -*-
-#-------------------------------------------------------------------------------
-# Name:        addic7ed
-# Purpose:     Parsing and downloading subs from addic7ed.com
-# Author:      Roman Miroshnychenko
-# Created on:  05.03.2013
-# Copyright:   (c) Roman Miroshnychenko, 2013
-# Licence:     GPL v.3 http://www.gnu.org/licenses/gpl.html
-#-------------------------------------------------------------------------------
-
-from __future__ import absolute_import, unicode_literals
+## Copyright (C) 2013, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
 import re
-from contextlib import closing
 from collections import namedtuple
+
 from bs4 import BeautifulSoup
-from kodi_six.xbmcvfs import File
-from .exceptions import SubsSearchError, DailyLimitError
-from .utils import LanguageData
-from .webclient import Session
 
-__all__ = ['search_episode', 'get_episode', 'download_subs']
+from addic7ed.exceptions import SubsSearchError, ParseError
+from addic7ed.webclient import Session
+
+__all__ = [
+    'search_episode',
+    'get_episode',
+    'parse_filename',
+    'normalize_showname',
+    'get_languages',
+]
 
 session = Session()
+
 SubsSearchResult = namedtuple('SubsSearchResult', ['subtitles', 'episode_url'])
 EpisodeItem = namedtuple('EpisodeItem', ['title', 'link'])
 SubsItem = namedtuple('SubsItem', ['language', 'version', 'link', 'hi', 'unfinished'])
+LanguageData = namedtuple('LanguageData', ['kodi_lang', 'add7_lang'])
+
 serie_re = re.compile(r'^serie')
 version_re = re.compile(r'Version (.*?),')
 original_download_re = re.compile(r'^/original')
 updated_download_re = re.compile(r'^/updated')
 jointranslation_re = re.compile('^/jointranslation')
+spanish_re = re.compile(r'Spanish \(.*?\)')
+
+episode_patterns = (
+    re.compile(r'^(.*?)[ \.](?:\d*?[ \.])?s(\d+)[ \.]?e(\d+)\.', re.I | re.U),
+    re.compile(r'^(.*?)[ \.](?:\d*?[ \.])?(\d+)x(\d+)\.', re.I | re.U),
+    re.compile(r'^(.*?)[ \.](?:\d*?[ \.])?(\d{1,2}?)[ \.]?(\d{2})\.', re.I | re.U),
+)
+# Convert show names from TheTVDB format to Addic7ed.com format
+# Keys must be all lowercase
+NAME_CONVERSIONS = {
+    'castle (2009)': 'castle',
+    'law & order: special victims unit': 'Law and order SVU',
+    'bodyguard (2018)': 'bodyguard',
+}
 
 
 def search_episode(query, languages=None):
@@ -60,9 +86,8 @@ def search_episode(query, languages=None):
                       )
     if table is not None:
         results = list(parse_search_results(table))
-        if not results:
-            raise SubsSearchError
-        return results
+        if results:
+            return results
     else:
         sub_cells = soup.find_all(
             'table',
@@ -73,8 +98,7 @@ def search_episode(query, languages=None):
             return SubsSearchResult(
                 parse_episode(sub_cells, languages), session.last_url
             )
-        else:
-            raise SubsSearchError
+    raise SubsSearchError
 
 
 def parse_search_results(table):
@@ -92,12 +116,11 @@ def get_episode(link, languages=None):
         'table',
         {'width': '100%', 'border': '0', 'align': 'center', 'class': 'tabel95'}
     )
-    if sub_cells:
-        return SubsSearchResult(
-            parse_episode(sub_cells, languages), session.last_url
-        )
-    else:
+    if not sub_cells:
         raise SubsSearchError
+    return SubsSearchResult(
+        parse_episode(sub_cells, languages), session.last_url
+    )
 
 
 def parse_episode(sub_cells, languages):
@@ -128,7 +151,7 @@ def parse_episode(sub_cells, languages):
             'td', {'class': 'newsDate', 'colspan': '3'}
         ).get_text(strip=True)
         if works_with:
-            version += u', ' + works_with
+            version += ', ' + works_with
         lang_cells = sub_cell.find_all('td', {'class': 'language'})
         for lang_cell in lang_cells:
             for language in languages:
@@ -159,20 +182,59 @@ def parse_episode(sub_cells, languages):
                     break
 
 
-def download_subs(link, referer, filename='subtitles.srt'):
+def parse_filename(filename):
+    """
+    Filename parser for extracting show name, season # and episode # from
+    a filename.
+
+    :param filename: episode filename
+    :return: parsed showname, season and episode
+    :raises ParseError: if the filename does not match any episode patterns
     """
-    Download subtitles from addic7ed.com
-
-    :param link: relative lint to .srt file
-    :param referer: episode page for referer header
-    :param filename: file name for subtitles
-    :raises ConnectionError: if addic7ed.com cannot be opened
-    :raises DailyLimitError: if a user exceeded their daily download quota
-        (10 subtitles).
+    filename = filename.replace(' ', '.')
+    for regexp in episode_patterns:
+        episode_data = regexp.search(filename)
+        if episode_data is not None:
+            showname = episode_data.group(1).replace('.', ' ')
+            season = episode_data.group(2).zfill(2)
+            episode = episode_data.group(3).zfill(2)
+            return showname, season, episode
+    raise ParseError
+
+
+def normalize_showname(showname):
     """
-    subtitles = session.download_subs(link, referer=referer)
-    if subtitles[:9].lower() != b'<!doctype':
-        with closing(File(filename, 'w')) as fo:
-            fo.write(bytearray(subtitles))
-    else:
-        raise DailyLimitError
+    Normalize showname if there are differences
+    between TheTVDB and Addic7ed
+
+    :param showname: TV show name
+    :return: normalized show name
+    """
+    showname = showname.strip().lower()
+    showname = NAME_CONVERSIONS.get(showname, showname)
+    return showname.replace(':', '')
+
+
+def get_languages(languages_raw):
+    """
+    Create the list of pairs of language names.
+    The 1st item in a pair is used by Kodi.
+    The 2nd item in a pair is used by
+    the addic7ed web site parser.
+
+    :param languages_raw: the list of subtitle languages from Kodi
+    :return: the list of language pairs
+    """
+    languages = []
+    for language in languages_raw:
+        kodi_lang = language
+        if 'English' in kodi_lang:
+            add7_lang = 'English'
+        elif kodi_lang == 'Portuguese (Brazil)':
+            add7_lang = 'Portuguese (Brazilian)'
+        elif spanish_re.search(kodi_lang) is not None:
+            add7_lang = 'Spanish (Latin America)'
+        else:
+            add7_lang = language
+        languages.append(LanguageData(kodi_lang, add7_lang))
+    return languages
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py b/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py
new file mode 100644
index 000000000..0d755e4d8
--- /dev/null
+++ b/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py
@@ -0,0 +1,235 @@
+# Copyright (C) 2019, Roman Miroshnychenko aka Roman V.M. <roman1972@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+A simple library for making HTTP requests with API similar to the popular "requests" library
+
+It depends only on the Python standard library.
+
+Supported:
+* HTTP methods: GET, POST
+* HTTP and HTTPS.
+* Disabling SSL certificates validation.
+* Request payload as form data and JSON.
+* Custom headers.
+* Basic authentication.
+* Gzipped response content.
+Not supported:
+* Cookies.
+* File upload.
+"""
+# pylint: skip-file
+import gzip
+import io
+import json as _json
+import ssl
+from base64 import b64encode
+from email.message import Message
+from typing import Optional, Dict, Any, Tuple, Union, List
+from urllib import request as url_request
+from urllib.error import HTTPError as _HTTPError
+from urllib.parse import urlparse, urlencode
+
+__all__ = [
+    'RequestException',
+    'ConnectionError',
+    'HTTPError',
+    'get',
+    'post',
+]
+
+
+class RequestException(IOError):
+
+    def __repr__(self) -> str:
+        return self.__str__()
+
+
+class ConnectionError(RequestException):
+
+    def __init__(self, message: str, url: str):
+        super().__init__(message)
+        self.message = message
+        self.url = url
+
+    def __str__(self) -> str:
+        return f'ConnectionError for url {self.url}: {self.message}'
+
+
+class HTTPError(RequestException):
+
+    def __init__(self, response: 'Response'):
+        self.response = response
+
+    def __str__(self) -> str:
+        return f'HTTPError: {self.response.status_code} for url: {self.response.url}'
+
+
+class HTTPMessage(Message):
+
+    def update(self, dct: Dict[str, str]) -> None:
+        for key, value in dct.items():
+            self[key] = value
+
+
+class Response:
+    NULL = object()
+
+    def __init__(self):
+        self.encoding: str = 'utf-8'
+        self.status_code: int = -1
+        self.headers: Dict[str, str] = {}
+        self.url: str = ''
+        self.content: bytes = b''
+        self._text = None
+        self._json = self.NULL
+
+    def __str__(self) -> str:
+        return f'<Response [{self.status_code}]>'
+
+    def __repr__(self) -> str:
+        return self.__str__()
+
+    @property
+    def ok(self) -> bool:
+        return self.status_code < 400
+
+    @property
+    def text(self) -> str:
+        """
+        :return: Response payload as decoded text
+        """
+        if self._text is None:
+            self._text = self.content.decode(self.encoding)
+        return self._text
+
+    def json(self) -> Optional[Union[Dict[str, Any], List[Any]]]:
+        try:
+            if self._json is self.NULL:
+                self._json = _json.loads(self.content)
+            return self._json
+        except ValueError as exc:
+            raise ValueError('Response content is not a valid JSON') from exc
+
+    def raise_for_status(self) -> None:
+        if not self.ok:
+            raise HTTPError(self)
+
+
+def _create_request(url_structure, params=None, data=None, headers=None, auth=None, json=None):
+    query = url_structure.query
+    if params is not None:
+        separator = '&' if query else ''
+        query += separator + urlencode(params, doseq=True)
+    full_url = url_structure.scheme + '://' + url_structure.netloc + url_structure.path
+    if query:
+        full_url += '?' + query
+    prepared_headers = HTTPMessage()
+    if headers is not None:
+        prepared_headers.update(headers)
+    body = None
+    if json is not None:
+        body = _json.dumps(json).encode('utf-8')
+        prepared_headers['Content-Type'] = 'application/json'
+    if body is None and data is not None:
+        body = urlencode(data, doseq=True).encode('ascii')
+        prepared_headers['Content-Type'] = 'application/x-www-form-urlencoded'
+    if auth is not None:
+        encoded_credentials = b64encode((auth[0] + ':' + auth[1]).encode('utf-8')).decode('ascii')
+        prepared_headers['Authorization'] = f'Basic {encoded_credentials}'
+    if 'Accept-Encoding' not in prepared_headers:
+        prepared_headers['Accept-Encoding'] = 'gzip'
+    return url_request.Request(full_url, body, prepared_headers)
+
+
+def post(url: str,
+         params: Optional[Dict[str, Any]] = None,
+         data: Optional[Dict[str, Any]] = None,
+         headers: Optional[Dict[str, str]] = None,
+         auth: Optional[Tuple[str, str]] = None,
+         timeout: Optional[float] = None,
+         verify: bool = True,
+         json: Optional[Dict[str, Any]] = None) -> Response:
+    """
+    POST request
+
+    This function assumes that a request body should be encoded with UTF-8
+    and by default sends Accept-Encoding: gzip header to receive response content compressed.
+
+    :param url: URL
+    :param params: URL query params
+    :param data: request payload as form data. If "data" or "json" are passed
+        then a POST request is sent
+    :param headers: additional headers
+    :param auth: a tuple of (login, password) for Basic authentication
+    :param timeout: request timeout in seconds
+    :param verify: verify SSL certificates
+    :param json: request payload as JSON. This parameter has precedence over "data", that is,
+        if it's present then "data" is ignored.
+    :return: Response object
+    """
+    url_structure = urlparse(url)
+    request = _create_request(url_structure, params, data, headers, auth, json)
+    context = None
+    if url_structure.scheme == 'https':
+        context = ssl.SSLContext()
+        if not verify:
+            context.verify_mode = ssl.CERT_NONE
+            context.check_hostname = False
+    fp = None
+    try:
+        r = fp = url_request.urlopen(request, timeout=timeout, context=context)
+        content = fp.read()
+    except _HTTPError as exc:
+        r = exc
+        fp = exc.fp
+        content = fp.read()
+    except Exception as exc:
+        raise ConnectionError(str(exc), request.full_url) from exc
+    finally:
+        if fp is not None:
+            fp.close()
+    response = Response()
+    response.status_code = r.status if hasattr(r, 'status') else r.getstatus()
+    response.headers = r.headers
+    response.url = r.url if hasattr(r, 'url') else r.geturl()
+    if r.headers.get('Content-Encoding') == 'gzip':
+        temp_fo = io.BytesIO(content)
+        gzip_file = gzip.GzipFile(fileobj=temp_fo)
+        content = gzip_file.read()
+    response.content = content
+    return response
+
+
+def get(url: str,
+        params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+        auth: Optional[Tuple[str, str]] = None,
+        timeout: Optional[float] = None,
+        verify: bool = True) -> Response:
+    """
+    GET request
+
+    This function by default sends Accept-Encoding: gzip header
+    to receive response content compressed.
+
+    :param url: URL
+    :param params: URL query params
+    :param headers: additional headers
+    :param auth: a tuple of (login, password) for Basic authentication
+    :param timeout: request timeout in seconds
+    :param verify: verify SSL certificates
+    :return: Response object
+    """
+    return post(url=url, params=params, headers=headers, auth=auth, timeout=timeout, verify=verify)
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/utils.py b/service.subtitles.rvm.addic7ed/addic7ed/utils.py
index 792c63a6f..340081ba8 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/utils.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/utils.py
@@ -1,59 +1,87 @@
-# -*- coding: utf-8 -*-
-# Module: functions
-# Author: Roman V. M.
-# Created on: 03.12.2014
-# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
+# Copyright (C) 2016, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-from __future__ import absolute_import, unicode_literals
 import json
+import logging
+import os
 import re
 from collections import namedtuple
-from kodi_six import xbmc
-from .addon import ADDON_ID
-from .exceptions import ParseError
+
+import xbmc
+
+from addic7ed.addon import ADDON_ID, ADDON_VERSION
+from addic7ed.exception_logger import format_exception, format_trace
 
 __all__ = [
-    'logger',
+    'initialize_logging',
     'get_now_played',
-    'normalize_showname',
-    'get_languages',
-    'parse_filename',
 ]
 
-# Convert show names from TheTVDB format to Addic7ed.com format
-# Keys must be all lowercase
-NAME_CONVERSIONS = {
-    'castle (2009)': 'castle',
-    'law & order: special victims unit': 'Law and order SVU',
-    'bodyguard (2018)': 'bodyguard',
-}
-
-episode_patterns = (
-    re.compile(r'^(.*?)[ \.](?:\d*?[ \.])?s(\d+)[ \.]?e(\d+)\.', re.I | re.U),
-    re.compile(r'^(.*?)[ \.](?:\d*?[ \.])?(\d+)x(\d+)\.', re.I | re.U),
-    re.compile(r'^(.*?)[ \.](?:\d*?[ \.])?(\d{1,2}?)[ \.]?(\d{2})\.', re.I | re.U),
-    )
-spanish_re = re.compile(r'Spanish \(.*?\)')
-
-LanguageData = namedtuple('LanguageData', ['kodi_lang', 'add7_lang'])
+logger = logging.getLogger(__name__)
 
 
-class logger(object):
-    @staticmethod
-    def log(message, level=xbmc.LOGDEBUG):
-        xbmc.log('{0}: {1}'.format(ADDON_ID, message), level)
+class KodiLogHandler(logging.Handler):
+    """
+    Logging handler that writes to the Kodi log with correct levels
 
-    @staticmethod
-    def info(message):
-        logger.log(message, xbmc.LOGINFO)
+    It also adds {addon_id} and {addon_version} variables available to log format.
+    """
+    LOG_FORMAT = '[{addon_id} v.{addon_version}] {filename}:{lineno} - {message}'
+    LEVEL_MAP = {
+        logging.NOTSET: xbmc.LOGNONE,
+        logging.DEBUG: xbmc.LOGDEBUG,
+        logging.INFO: xbmc.LOGINFO,
+        logging.WARN: xbmc.LOGWARNING,
+        logging.WARNING: xbmc.LOGWARNING,
+        logging.ERROR: xbmc.LOGERROR,
+        logging.CRITICAL: xbmc.LOGFATAL,
+    }
+
+    def emit(self, record):
+        record.addon_id = ADDON_ID
+        record.addon_version = ADDON_VERSION
+        extended_trace_info = getattr(self, 'extended_trace_info', False)
+        if extended_trace_info:
+            if record.exc_info is not None:
+                record.exc_text = format_exception(record.exc_info[1])
+            if record.stack_info is not None:
+                record.stack_info = format_trace(7)
+        message = self.format(record)
+        kodi_log_level = self.LEVEL_MAP.get(record.levelno, xbmc.LOGDEBUG)
+        xbmc.log(message, level=kodi_log_level)
+
+
+def initialize_logging(extended_trace_info=True):
+    """
+    Initialize the root logger that writes to the Kodi log
 
-    @staticmethod
-    def error(message):
-        logger.log(message, xbmc.LOGERROR)
+    After initialization, you can use Python logging facilities as usual.
 
-    @staticmethod
-    def debug(message):
-        logger.log(message, xbmc.LOGDEBUG)
+    :param extended_trace_info: write extended trace info when exc_info=True
+        or stack_info=True parameters are passed to logging methods.
+    """
+    handler = KodiLogHandler()
+    # pylint: disable=attribute-defined-outside-init
+    handler.extended_trace_info = extended_trace_info
+    logging.basicConfig(
+        format=KodiLogHandler.LOG_FORMAT,
+        style='{',
+        level=logging.DEBUG,
+        handlers=[handler],
+        force=True
+    )
 
 
 def get_now_played():
@@ -72,65 +100,12 @@ def get_now_played():
          },
         'id': '1'
     })
-    item = json.loads(xbmc.executeJSONRPC(request))['result']['item']
-    item['file'] = xbmc.Player().getPlayingFile()  # It provides more correct result
+    response = xbmc.executeJSONRPC(request)
+    item = json.loads(response)['result']['item']
+    path = xbmc.getInfoLabel('Window(10000).Property(videoinfo.current_path)')
+    if path:
+        item['file'] = os.path.basename(path)
+        logger.debug("Using file path from addon: %s", item['file'])
+    else:
+        item['file'] = xbmc.Player().getPlayingFile()  # It provides more correct result
     return item
-
-
-def normalize_showname(showname):
-    """
-    Normalize showname if there are differences
-    between TheTVDB and Addic7ed
-
-    :param showname: TV show name
-    :return: normalized show name
-    """
-    showname = showname.strip().lower()
-    if showname in NAME_CONVERSIONS:
-        showname = NAME_CONVERSIONS[showname]
-    return showname.replace(':', '')
-
-
-def get_languages(languages_raw):
-    """
-    Create the list of pairs of language names.
-    The 1st item in a pair is used by Kodi.
-    The 2nd item in a pair is used by
-    the addic7ed web site parser.
-
-    :param languages_raw: the list of subtitle languages from Kodi
-    :return: the list of language pairs
-    """
-    languages = []
-    for language in languages_raw:
-        kodi_lang = language
-        if 'English' in kodi_lang:
-            add7_lang = 'English'
-        elif kodi_lang == 'Portuguese (Brazil)':
-            add7_lang = 'Portuguese (Brazilian)'
-        elif spanish_re.search(kodi_lang) is not None:
-            add7_lang = 'Spanish (Latin America)'
-        else:
-            add7_lang = language
-        languages.append(LanguageData(kodi_lang, add7_lang))
-    return languages
-
-
-def parse_filename(filename):
-    """
-    Filename parser for extracting show name, season # and episode # from
-    a filename.
-
-    :param filename: episode filename
-    :return: parsed showname, season and episode
-    :raises ParseError: if the filename does not match any episode patterns
-    """
-    filename = filename.replace(' ', '.')
-    for regexp in episode_patterns:
-        episode_data = regexp.search(filename)
-        if episode_data is not None:
-            showname = episode_data.group(1).replace('.', ' ')
-            season = episode_data.group(2).zfill(2)
-            episode = episode_data.group(3).zfill(2)
-            return showname, season, episode
-    raise ParseError
diff --git a/service.subtitles.rvm.addic7ed/addic7ed/webclient.py b/service.subtitles.rvm.addic7ed/addic7ed/webclient.py
index 581f85f47..00ec56091 100644
--- a/service.subtitles.rvm.addic7ed/addic7ed/webclient.py
+++ b/service.subtitles.rvm.addic7ed/addic7ed/webclient.py
@@ -1,57 +1,67 @@
-# coding: utf-8
+# Copyright (C) 2016, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-from __future__ import absolute_import, unicode_literals
-import requests
-from .exceptions import Add7ConnectionError
-from .utils import logger
+import logging
+
+from xbmcvfs import File
+
+from addic7ed import simple_requests as requests
+from addic7ed.exceptions import Add7ConnectionError, NoSubtitlesReturned
 
 __all__ = ['Session']
 
+logger = logging.getLogger(__name__)
+
 SITE = 'https://www.addic7ed.com'
 HEADERS = {
     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
-                  '(KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
+                  '(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
     'Host': SITE[8:],
     'Accept-Charset': 'UTF-8',
-    'Accept-Encoding': 'gzip,deflate'
 }
 
 
-class Session(object):
+class Session:
     """
     Webclient Session class
     """
-    def __init__(self):
-        self._session = requests.Session()
-        self._session.headers = HEADERS.copy()
-        self._last_url = ''
+    _instance = None
 
-    @property
-    def last_url(self):
-        """
-        Get actual url (with redirect) of the last loaded webpage
+    def __new__(cls):
+        if cls._instance is None:
+            cls._instance = super().__new__(cls)
+        return cls._instance
 
-        :return: URL of the last webpage
-        """
-        return self._last_url
+    def __init__(self):
+        self.last_url = ''
 
     def _open_url(self, url, params, referer):
-        logger.debug('Opening URL: {0}'.format(url))
-        self._session.headers['Referer'] = referer
+        logger.debug('Opening URL: %s', url)
+        headers = HEADERS.copy()
+        headers['Referer'] = referer
         try:
-            response = self._session.get(url, params=params, verify=False)
-        except requests.RequestException:
+            response = requests.get(url, params=params, headers=headers, verify=False)
+        except requests.RequestException as exc:
             logger.error('Unable to connect to Addic7ed.com!')
-            raise Add7ConnectionError
-        response.encoding = 'utf-8'  # Encoding is auto-detected incorrectly
-        logger.debug('Addic7ed.com returned page:\n{}'.format(response.text))
+            raise Add7ConnectionError from exc
+        logger.debug('Addic7ed.com returned page:\n%s', response.text)
         if not response.ok:
-            logger.error('Addic7ed.com returned status: {0}'.format(
-                response.status_code)
-            )
+            logger.error('Addic7ed.com returned status: %s', response.status_code)
             raise Add7ConnectionError
-        self._last_url = response.url
+        self.last_url = response.url
         return response
 
     def load_page(self, path, params=None):
@@ -64,17 +74,23 @@ def load_page(self, path, params=None):
         :raises ConnectionError: if unable to connect to the server
         """
         response = self._open_url(SITE + path, params, referer=SITE + '/')
-        self._last_url = response.url
+        self.last_url = response.url
         return response.text
 
-    def download_subs(self, path, referer):
+    def download_subs(self, path, referer, filename='subtitles.srt'):
         """
         Download subtitles by their URL
 
         :param path: relative path to .srt starting from '/'
         :param referer: referer page
+        :param filename: subtitles filename
         :return: subtitles file contents as a byte string
         :raises ConnectionError: if unable to connect to the server
+        :raises NoSubtitlesReturned: if a HTML page is returned instead of subtitles
         """
         response = self._open_url(SITE + path, params=None, referer=referer)
-        return response.content
+        subtitles = response.content
+        if subtitles[:9].lower() == b'<!doctype':
+            raise NoSubtitlesReturned
+        with File(filename, 'w') as fo:
+            fo.write(bytearray(subtitles))
diff --git a/service.subtitles.rvm.addic7ed/addon.xml b/service.subtitles.rvm.addic7ed/addon.xml
index 2ed56eb1c..258a7eaec 100644
--- a/service.subtitles.rvm.addic7ed/addon.xml
+++ b/service.subtitles.rvm.addic7ed/addon.xml
@@ -1,15 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <addon id="service.subtitles.rvm.addic7ed"
   name="Addic7ed.com"
-  version="3.1.8+matrix.1"
+  version="3.2.0"
   provider-name="Roman V.M.">
 <requires>
   <import addon="xbmc.python" version="3.0.0"/>
   <import addon="script.module.beautifulsoup4"/>
   <import addon="script.module.html5lib"/>
-  <import addon="script.module.requests"/>
-  <import addon="script.module.six"/>
-  <import addon="script.module.kodi-six"/>
   <!--<import addon="script.module.web-pdb"/>-->
 </requires>
 <extension point="xbmc.subtitle.module" library="main.py" />
@@ -32,11 +29,8 @@
     <icon>icon.png</icon>
     <fanart>fanart.jpg</fanart>
   </assets>
-  <news>3.1.8:
-- Performance optimization.
-
-3.1.7:
-- Fixed crashes when playing non-medialibrary items.</news>
+  <news>3.2.0:
+- Removed Python 2 compatibility.</news>
   <reuselanguageinvoker>true</reuselanguageinvoker>
 </extension>
 </addon>
diff --git a/service.subtitles.rvm.addic7ed/main.py b/service.subtitles.rvm.addic7ed/main.py
index 759b7f293..e0cbbd52b 100644
--- a/service.subtitles.rvm.addic7ed/main.py
+++ b/service.subtitles.rvm.addic7ed/main.py
@@ -1,11 +1,26 @@
-# -*- coding: utf-8 -*-
-# Licence: GPL v.3 http://www.gnu.org/licenses/gpl.html
-# The main script contains minimum code to speed up launching on slower systems
+# Copyright (C) 2019, Roman Miroshnychenko aka Roman V.M.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import sys
+
 from addic7ed.actions import router
-from addic7ed.exception_logger import log_exception
+from addic7ed.exception_logger import catch_exception
+from addic7ed.utils import initialize_logging
+
+initialize_logging()
 
 if __name__ == '__main__':
-    with log_exception():
+    with catch_exception():
         router(sys.argv[2][1:])