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:])