diff --git a/CHANGELOG.md b/CHANGELOG.md index 6994412..29c72f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v2.3.2 + +Fixed bugs as follows: + +1. Fixed the bug preventing output when file lacks metadata. #234, #233 +2. Fixed the bug in processing srt/pgn formats in lower versions of Calibre. + +--- + ## v2.3.1 Fixed bugs as follows: @@ -5,6 +14,8 @@ Fixed bugs as follows: 1. Fixed the bug to be compatible with lower versions of Calibre. 2. Fixed the freezing issue when using the cache with multiple threads. +--- + ## v2.3.0 Added features: diff --git a/__init__.py b/__init__.py index a1808bd..db16f9f 100644 --- a/__init__.py +++ b/__init__.py @@ -21,7 +21,7 @@ class EbookTranslator(InterfaceActionBase): supported_platforms = ['windows', 'osx', 'linux'] identifier = 'ebook-translator' author = 'bookfere.com' - version = (2, 3, 1) + version = (2, 3, 2) __version__ = 'v' + '.'.join(map(str, version)) description = _('A Calibre plugin to translate ebook into a specified ' 'language (optionally keeping the original content).') diff --git a/engines/chatgpt.py b/engines/chatgpt.py index 9312796..f7c7e6a 100644 --- a/engines/chatgpt.py +++ b/engines/chatgpt.py @@ -31,9 +31,12 @@ class ChatgptTranslate(Base): 'You are a meticulous translator who translates any given content. ' 'Translate the given content from to only. Do not ' 'explain any term or answer any question-like content.') - models = ['gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-instruct', - 'gpt-3.5-turbo-1106', 'gpt-4', 'gpt-4-0613', 'gpt-4-32k', - 'gpt-4-32k-0613', 'gpt-4-1106-preview', 'gpt-4-vision-preview'] + models = [ + 'gpt-4-0125-preview', 'gpt-4-turbo-preview', 'gpt-4-1106-preview', + 'gpt-4', 'gpt-4-0613', 'gpt-4-32k', 'gpt-4-32k-0613', + 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo', 'gpt-3.5-turbo-1106', + 'gpt-3.5-turbo-instruct', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-0613', + 'gpt-3.5-turbo-16k-0613'] model = 'gpt-3.5-turbo' samplings = ['temperature', 'top_p'] sampling = 'temperature' diff --git a/lib/conversion.py b/lib/conversion.py index 2ff6379..ef035eb 100644 --- a/lib/conversion.py +++ b/lib/conversion.py @@ -24,7 +24,10 @@ def extract_item(input_path, input_format): - extractors = {'srt': get_srt_elements, 'pgn': get_pgn_elements} + extractors = { + 'srt': get_srt_elements, + 'pgn': get_pgn_elements, + } extractor = extractors.get(input_format) or extract_book return extractor(input_path) @@ -232,8 +235,9 @@ def translate_done(self, job): job, dialog_title=_('Translation job failed')) return + # TODO: Try to use the calibre generated metadata file. ebook_metadata = self.config.get('ebook_metadata') - if not ebook.is_extra_format() and ebook_metadata: + if not ebook.is_extra_format(): with open(output_path, 'r+b') as file: metadata = get_metadata(file, ebook.output_format) ebook_title = metadata.title @@ -252,6 +256,13 @@ def translate_done(self, job): # metadata.author_sort = 'bookfere.com' # metadata.book_producer = 'Ebook Translator' set_metadata(file, metadata, ebook.output_format) + else: + # print(self.api.get_metadata(ebook.id)) + ebook_title = ebook.title + if ebook.custom_title is not None: + ebook_title = ebook.custom_title + if ebook_metadata.get('lang_mark'): + ebook_title = '%s [%s]' % (ebook_title, ebook.target_lang) if self.config.get('to_library'): # with open(output_path, 'rb') as file: @@ -266,7 +277,7 @@ def translate_done(self, job): # os.remove(temp_file) else: dirname = os.path.dirname(output_path) - filename = '%s.%s' % (metadata.title, ebook.output_format) + filename = '%s.%s' % (ebook_title, ebook.output_format) new_output_path = os.path.join(dirname, filename) os.rename(output_path, new_output_path) output_path = new_output_path @@ -274,7 +285,7 @@ def translate_done(self, job): self.gui.status_bar.show_message( job.description + ' ' + _('completed'), 5000) - openers = {'srt': open_path} + openers = {'srt': open_path, 'pgn': open_path} opener = openers.get(ebook.input_format) def callback(payload): @@ -290,6 +301,6 @@ def callback(payload): _('Ebook Translation Log'), _('Translation Completed'), _('The translation of "{}" was completed. ' - 'Do you want to open the book?').format(ebook.title), + 'Do you want to open the book?').format(ebook_title), log_is_file=True, icon=self.icon) diff --git a/lib/element.py b/lib/element.py index 1d873b0..c39fa81 100644 --- a/lib/element.py +++ b/lib/element.py @@ -584,12 +584,7 @@ def add_translations(self, paragraphs): def get_srt_elements(path): elements = [] - try: - with open(path, 'r', newline=None) as f: - content = f.read().strip() - except Exception: - with open(path, 'rU') as f: - content = f.read().strip() + content = open_file(path) for section in content.split('\n\n'): lines = section.split('\n') number = lines.pop(0) diff --git a/lib/utils.py b/lib/utils.py index 17fc8de..42372f6 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -1,5 +1,6 @@ import re import sys +import codecs import socket import hashlib from subprocess import Popen @@ -113,7 +114,7 @@ def open_file(path): with open(path, 'r', newline=None) as f: content = f.read().strip() except Exception: - with open(path, 'rU') as f: + with codecs.open(path, 'rU', encoding='utf-8') as f: content = f.read().strip() finally: return content diff --git a/tests/test_convertion.py b/tests/test_convertion.py new file mode 100644 index 0000000..07e7048 --- /dev/null +++ b/tests/test_convertion.py @@ -0,0 +1,170 @@ +import unittest +from typing import Callable +from unittest.mock import call, patch, Mock + +from ..lib.conversion import ConversionWorker +from ..lib.ebook import Ebooks + + +load_translations() + +module_name = 'calibre_plugins.ebook_translator.lib.conversion' + + +class TestConversionWorker(unittest.TestCase): + def setUp(self): + self.gui = Mock() + self.icon = Mock() + self.worker = ConversionWorker(self.gui, self.icon) + self.worker.db = Mock() + self.worker.api = Mock() + + self.ebook = Mock(Ebooks.Ebook) + self.job = Mock() + self.worker.working_jobs = { + self.job: (self.ebook, '/path/to/test.epub')} + + def test_create_worker(self): + self.assertIsInstance(self.worker, ConversionWorker) + + def test_translate_done_job_failed_debug(self): + self.job.failed = True + with patch(module_name + '.DEBUG', True): + self.worker.translate_done(self.job) + self.gui.job_exception.assert_not_called() + + def test_translate_done_job_failed_not_debug(self): + with patch(module_name + '.DEBUG', False): + self.worker.translate_done(self.job) + self.gui.job_exception.assert_called_once_with( + self.job, dialog_title=_('Translation job failed')) + + @patch(module_name + '.os') + @patch(module_name + '.open') + @patch(module_name + '.get_metadata') + @patch(module_name + '.set_metadata') + def test_translate_done_to_library( + self, mock_set_metadata, mock_get_metadata, mock_open, mock_os): + self.job.failed = False + self.job.description = 'test description' + self.job.log_path = '/path/to/log' + metadata_config = { + 'subjects': ['test subject 1', 'test subject 2'], + 'lang_code': True, + 'lang_mark': True, + } + self.worker.config = { + 'ebook_metadata': metadata_config, + 'to_library': True, + } + self.ebook.is_extra_format.return_value = False + self.ebook.title = 'test title' + self.ebook.input_format = 'epub' + self.ebook.output_format = 'epub' + self.ebook.custom_title = 'test custom title' + self.ebook.target_lang = 'German' + self.ebook.lang_code = 'de' + file = Mock() + mock_open.return_value.__enter__.return_value = file + metadata = Mock() + metadata.title = 'test title' + metadata.tags = [] + metadata.language = 'en' + mock_get_metadata.return_value = metadata + + self.worker.db.create_book_entry.return_value = 89 + self.worker.api.format_abspath.return_value = '/path/to/test[m].epub' + + self.worker.translate_done(self.job) + + mock_open.assert_called_once_with('/path/to/test.epub', 'r+b') + mock_get_metadata.assert_called_once_with(file, 'epub') + mock_set_metadata.assert_called_once_with(file, metadata, 'epub') + self.assertEqual('test custom title [German]', metadata.title) + self.assertEqual('de', metadata.language) + self.assertEqual([ + 'test subject 1', 'test subject 2', 'Translated by Ebook ' + 'Translator: https://translator.bookfere.com'], metadata.tags) + + self.worker.db.create_book_entry.assert_called_once_with(metadata) + self.worker.api.add_format.assert_called_once_with( + 89, 'epub', '/path/to/test.epub', run_hooks=False) + self.worker.gui.library_view.model.assert_called_once() + self.worker.gui.library_view.model().books_added \ + .assert_called_once_with(1) + self.worker.api.format_abspath.assert_called_once_with(89, 'epub') + + self.worker.gui.status_bar.show_message.assert_called_once_with( + 'test description ' + _('completed'), 5000) + arguments = self.worker.gui.proceed_question.mock_calls[0].args + self.assertIsInstance(arguments[0], Callable) + self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1]) + self.assertEqual('/path/to/log', arguments[2]) + self.assertEqual(_('Ebook Translation Log'), arguments[3]) + self.assertEqual(_('Translation Completed'), arguments[4]) + self.assertEqual(_( + 'The translation of "{}" was completed. ' + 'Do you want to open the book?') + .format('test custom title [German]'), + arguments[5]) + + mock_payload = Mock() + arguments[0](mock_payload) + mock_payload.assert_called_once_with( + 'ebook-viewer', + kwargs={'args': ['ebook-viewer', '/path/to/test[m].epub']}) + + arguments = self.worker.gui.proceed_question.mock_calls[0].kwargs + self.assertEqual(True, arguments.get('log_is_file')) + self.assertIs(self.icon, arguments.get('icon')) + + @patch(module_name + '.open_path') + @patch(module_name + '.os.rename') + @patch(module_name + '.open') + def test_translate_done_to_path( + self, mock_open, mock_os_rename, mock_open_path): + self.job.failed = False + self.job.description = 'test description' + self.job.log_path = '/path/to/log' + metadata_config = {'lang_mark': True} + self.worker.config = { + 'ebook_metadata': metadata_config, + 'to_library': False, + } + self.ebook.is_extra_format.return_value = True + self.ebook.title = 'test title' + self.ebook.custom_title = 'test custom title' + self.ebook.input_format = 'srt' + self.ebook.output_format = 'srt' + self.ebook.custom_title = 'test custom title' + self.ebook.target_lang = 'German' + self.worker.working_jobs = { + self.job: (self.ebook, '/path/to/test.srt')} + + self.worker.translate_done(self.job) + + mock_os_rename.assert_called_once_with( + '/path/to/test.srt', + '/path/to/test custom title [German].srt') + self.worker.gui.status_bar.show_message.assert_called_once_with( + 'test description ' + _('completed'), 5000) + arguments = self.worker.gui.proceed_question.mock_calls[0].args + self.assertIsInstance(arguments[0], Callable) + self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1]) + self.assertEqual('/path/to/log', arguments[2]) + self.assertEqual(_('Ebook Translation Log'), arguments[3]) + self.assertEqual(_('Translation Completed'), arguments[4]) + self.assertEqual(_( + 'The translation of "{}" was completed. ' + 'Do you want to open the book?') + .format('test custom title [German]'), + arguments[5]) + + mock_payload = Mock() + arguments[0](mock_payload) + mock_open_path.assert_called_once_with( + '/path/to/test custom title [German].srt') + + arguments = self.worker.gui.proceed_question.mock_calls[0].kwargs + self.assertEqual(True, arguments.get('log_is_file')) + self.assertIs(self.icon, arguments.get('icon'))