diff --git a/service.subtitles.rvm.addic7ed/addic7ed/actions.py b/service.subtitles.rvm.addic7ed/addic7ed/actions.py index f16a9116d..1367d53c6 100644 --- a/service.subtitles.rvm.addic7ed/addic7ed/actions.py +++ b/service.subtitles.rvm.addic7ed/addic7ed/actions.py @@ -26,7 +26,7 @@ import xbmcplugin from addic7ed import parser -from addic7ed.addon import ADDON, PROFILE, ICON, get_ui_string +from addic7ed.addon import ADDON, PROFILE, ICON, GettextEmulator from addic7ed.exceptions import NoSubtitlesReturned, ParseError, SubsSearchError, \ Add7ConnectionError from addic7ed.parser import parse_filename, normalize_showname, get_languages @@ -35,9 +35,11 @@ __all__ = ['router'] +_ = GettextEmulator.gettext + logger = logging.getLogger(__name__) -TEMP_DIR = os.path.join(PROFILE, 'temp') +TEMP_DIR = PROFILE / 'temp' HANDLE = int(sys.argv[1]) @@ -135,24 +137,24 @@ 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 PROFILE.exists(): + PROFILE.mkdir() + if TEMP_DIR.exists(): + shutil.rmtree(str(TEMP_DIR)) + TEMP_DIR.mkdir() # Combine a path where to download the subs filename = os.path.splitext(filename)[0] + '.srt' - subspath = os.path.join(TEMP_DIR, filename) + subspath = str(TEMP_DIR / filename) # Download the subs from addic7ed.com try: 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') + DIALOG.notification(_('Error!'), _('Unable to connect to addic7ed.com.'), 'error') except NoSubtitlesReturned: - DIALOG.notification(get_ui_string(32002), get_ui_string(32003), 'error', + DIALOG.notification(_('Error!'), _('Exceeded daily limit for subs downloads.'), 'error', 3000) - logger.error('Exceeded daily limit for subs downloads.') + logger.error('A HTML page returned instead of subtitles for link: %s', link) else: # Create a ListItem for downloaded subs and pass it # to the Kodi subtitles engine to move the downloaded subs file @@ -165,8 +167,7 @@ def download_subs(link, referrer, filename): url=subspath, listitem=list_item, isFolder=False) - DIALOG.notification(get_ui_string(32000), get_ui_string(32001), ICON, - 3000, False) + DIALOG.notification(_('Success!'), _('Subtitles downloaded.'), ICON, 3000, False) logger.info('Subs downloaded.') @@ -197,7 +198,7 @@ def extract_episode_data(): showname, season, episode = parse_filename(filename) except ParseError: logger.error('Unable to determine episode data for %s', filename) - DIALOG.notification(get_ui_string(32002), get_ui_string(32006), + DIALOG.notification(_('Error!'), _('Unable to determine episode data.'), 'error', 3000) raise else: @@ -241,24 +242,22 @@ def search_subs(params): results = parser.search_episode(query, languages) except Add7ConnectionError: logger.error('Unable to connect to addic7ed.com') - DIALOG.notification( - get_ui_string(32002), get_ui_string(32005), 'error' - ) + DIALOG.notification(_('Error!'), _('Unable to connect to addic7ed.com.'), 'error') except SubsSearchError: logger.info('No subs for "%s" found.', query) else: if isinstance(results, list): logger.info('Multiple episodes found:\n%s', results) i = DIALOG.select( - get_ui_string(32008), [item.title for item in results] + _('Select episode'), [item.title for item in results] ) if i >= 0: try: results = parser.get_episode(results[i].link, languages) except Add7ConnectionError: logger.error('Unable to connect to addic7ed.com') - DIALOG.notification(get_ui_string(32002), - get_ui_string(32005), 'error') + DIALOG.notification(_('Error!'), + _('Unable to connect to addic7ed.com.'), 'error') return except SubsSearchError: logger.info('No subs found.') diff --git a/service.subtitles.rvm.addic7ed/addic7ed/addon.py b/service.subtitles.rvm.addic7ed/addic7ed/addon.py index 1fd95a245..549915b51 100644 --- a/service.subtitles.rvm.addic7ed/addic7ed/addon.py +++ b/service.subtitles.rvm.addic7ed/addic7ed/addon.py @@ -13,26 +13,105 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os +import hashlib +import json +import re +from pathlib import Path import xbmcaddon from xbmcvfs import translatePath -__all__ = ['ADDON_ID', 'ADDON', 'ADDON_VERSION', 'PATH', 'PROFILE', 'ICON', 'get_ui_string'] +__all__ = ['ADDON_ID', 'ADDON', 'ADDON_VERSION', 'PATH', 'PROFILE', 'ICON', 'GettextEmulator'] 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') +PATH = Path(translatePath(ADDON.getAddonInfo('path'))) +PROFILE = Path(translatePath(ADDON.getAddonInfo('profile'))) +ICON = str(PATH / 'icon.png') -def get_ui_string(string_id): - """ - Get language string by ID - :param string_id: UI string ID - :return: UI string +class GettextEmulator: + """ + Emulate GNU Gettext by mapping resource.language.en_gb UI strings to their numeric string IDs """ - return ADDON.getLocalizedString(string_id) + _instance = None + + class LocalizationError(Exception): # pylint: disable=missing-docstring + pass + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + self._en_gb_string_po_path = (PATH / 'resources' / 'language' / + 'resource.language.en_gb' / 'strings.po') + if not self._en_gb_string_po_path.exists(): + raise self.LocalizationError( + 'Missing resource.language.en_gb strings.po localization file') + if not PROFILE.exists(): + PROFILE.mkdir() + self._string_mapping_path = PROFILE / 'strings-map.json' + self.strings_mapping = self._load_strings_mapping() + + def _load_strings_po(self): # pylint: disable=missing-docstring + with self._en_gb_string_po_path.open('r', encoding='utf-8') as fo: + return fo.read() + + def _load_strings_mapping(self): + """ + Load mapping of resource.language.en_gb UI strings to their IDs + + If a mapping file is missing or resource.language.en_gb strins.po file has been updated, + a new mapping file is created. + + :return: UI strings mapping + """ + strings_po = self._load_strings_po() + strings_po_md5 = hashlib.md5(strings_po.encode('utf-8')).hexdigest() + try: + with self._string_mapping_path.open('r', encoding='utf-8') as fo: + mapping = json.load(fo) + if mapping['md5'] != strings_po_md5: + raise IOError('resource.language.en_gb strings.po has been updated') + except (IOError, ValueError): + strings_mapping = self._parse_strings_po(strings_po) + mapping = { + 'strings': strings_mapping, + 'md5': strings_po_md5, + } + with self._string_mapping_path.open('w', encoding='utf-8') as fo: + json.dump(mapping, fo) + return mapping['strings'] + + @staticmethod + def _parse_strings_po(strings_po): + """ + Parse resource.language.en_gb strings.po file contents into a mapping of UI strings + to their numeric IDs. + + :param strings_po: the content of strings.po file as a text string + :return: UI strings mapping + """ + id_string_pairs = re.findall(r'^msgctxt "#(\d+?)"\r?\nmsgid "(.*)"\r?$', strings_po, re.M) + return {string: int(string_id) for string_id, string in id_string_pairs if string} + + @classmethod + def gettext(cls, en_string: str) -> str: + """ + Return a localized UI string by a resource.language.en_gb source string + + :param en_string: resource.language.en_gb UI string + :return: localized UI string + """ + emulator = cls() + try: + string_id = emulator.strings_mapping[en_string] + except KeyError as exc: + raise cls.LocalizationError( + f'Unable to find "{en_string}" string in resource.language.en_gb/strings.po' + ) from exc + return ADDON.getLocalizedString(string_id) diff --git a/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py b/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py index 7da42d896..8838ec7e7 100644 --- a/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py +++ b/service.subtitles.rvm.addic7ed/addic7ed/simple_requests.py @@ -108,7 +108,7 @@ def headers(self) -> HTTPMessage: @headers.setter def headers(self, value: HTTPMessage): charset = value.get_content_charset() - if charset is not None: + if charset: self.encoding = charset self._headers = value @@ -124,7 +124,7 @@ def text(self) -> str: if self._text is None: try: self._text = self.content.decode(self.encoding) - except UnicodeDecodeError: + except (UnicodeDecodeError, LookupError): self._text = self.content.decode('utf-8', 'replace') return self._text diff --git a/service.subtitles.rvm.addic7ed/addon.xml b/service.subtitles.rvm.addic7ed/addon.xml index a33b2e6e3..6e5820295 100644 --- a/service.subtitles.rvm.addic7ed/addon.xml +++ b/service.subtitles.rvm.addic7ed/addon.xml @@ -1,7 +1,7 @@ @@ -29,8 +29,9 @@ icon.png fanart.jpg - 3.2.1: -- Fixed issues with downloading subtitles. + 3.2.2: +- Fixed the issue with an empty content-charset header. +- Internal changes. true