From 564db06179addb014e7de46af53157dabe355e80 Mon Sep 17 00:00:00 2001 From: BasioMeusPuga Date: Sun, 10 Feb 2019 23:43:17 +0530 Subject: [PATCH] Multiple fixes Images are now center aligned Better logging --- README.md | 1 + TODO | 6 ---- lector/__main__.py | 31 ++++++++----------- lector/guifunctions.py | 31 +++++++++---------- lector/parsers/pdf.py | 6 +--- lector/readers/read_epub.py | 60 +++++++++++++++++++++---------------- lector/sorter.py | 37 ++++++++++++++--------- lector/widgets.py | 35 +++++++++++++--------- 8 files changed, 109 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 8e3f3bf..44ef051 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Bitcoin: 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro | PyQt5 | 5.10.1 | | python-lxml | 4.3.0 | | python-beautifulsoup4 | 4.6.0 | +| python-xmltodict | 0.11.0 | ### Optional | Package | Version tested | diff --git a/TODO b/TODO index 08ebecc..f5b0a1f 100644 --- a/TODO +++ b/TODO @@ -92,15 +92,9 @@ TODO Bugs: Deselecting all directories in the settings dialog also filters out manually added books Last line in QTextBrowser should never be cut off - Does image alignment need to be centered? Bookmark name for a page that's not on the TOC and has nothing before Screen position still keeps jumping when inside a paragraph Better recursion needed for fb2 toc - Initial sort by author in tableview - Last column not filling up tableview - Comic view mode changing does not work for newly added books - Ctrl + A reports 10 times the number of books selected for deletion - Ordering for non TOC chapters is beyond borked Secondary: Tab tooltip diff --git a/lector/__main__.py b/lector/__main__.py index e62590e..0e735d5 100755 --- a/lector/__main__.py +++ b/lector/__main__.py @@ -302,9 +302,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): for count, i in enumerate(self.settings['main_window_headers']): self.tableView.horizontalHeader().resizeSection(count, int(i)) self.tableView.horizontalHeader().resizeSection(5, 30) - self.tableView.horizontalHeader().setStretchLastSection(False) + self.tableView.horizontalHeader().setStretchLastSection(True) self.tableView.horizontalHeader().sectionClicked.connect( self.lib_ref.tableProxyModel.sort_table_columns) + self.lib_ref.tableProxyModel.sort_table_columns(2) self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tableView.customContextMenuRequested.connect( self.generate_library_context_menu) @@ -360,7 +361,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): def process_post_hoc_files(self, file_list, open_files_after_processing): # Takes care of both dragged and dropped files # As well as files sent as command line arguments - file_list = [i for i in file_list if os.path.exists(i)] if not file_list: return @@ -390,7 +390,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): # This allows for threading file opening # Which should speed up multiple file opening # especially @ application start - file_paths = [i for i in path_hash_dictionary] for filename in path_hash_dictionary.items(): @@ -520,15 +519,15 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.thread.finished.connect(self.move_on) self.thread.start() - def get_selection(self, library_widget): + def get_selection(self): selected_indexes = None - if library_widget == self.listView: + if self.listView.isVisible(): selected_books = self.lib_ref.itemProxyModel.mapSelectionToSource( self.listView.selectionModel().selection()) selected_indexes = [i.indexes()[0] for i in selected_books] - elif library_widget == self.tableView: + elif self.tableView.isVisible(): selected_books = self.tableView.selectionModel().selectedRows() selected_indexes = [ self.lib_ref.tableProxyModel.mapToSource(i) for i in selected_books] @@ -536,16 +535,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): return selected_indexes def delete_books(self, selected_indexes=None): - if not selected_indexes: - # Get a list of QItemSelection objects - # What we're interested in is the indexes()[0] in each of them - # That gives a list of indexes from the view model - if self.listView.isVisible(): - selected_indexes = self.get_selection(self.listView) - - elif self.tableView.isVisible(): - selected_indexes = self.get_selection(self.tableView) - + # Get a list of QItemSelection objects + # What we're interested in is the indexes()[0] in each of them + # That gives a list of indexes from the view model + selected_indexes = self.get_selection() if not selected_indexes: return @@ -572,10 +565,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.thread.start() # Generate a message box to confirm deletion - selected_number = len(selected_indexes) confirm_deletion = QtWidgets.QMessageBox() deletion_prompt = self._translate( - 'Main_UI', f'Delete {selected_number} book(s)?') + 'Main_UI', f'Delete book(s)?') confirm_deletion.setText(deletion_prompt) confirm_deletion.setIcon(QtWidgets.QMessageBox.Question) confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion')) @@ -770,6 +762,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): signal_sender = None else: signal_sender = self.sender().objectName() + self.profile_functions.modify_comic_view( signal_sender, key_pressed) @@ -805,7 +798,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): # It's worth remembering that these are indexes of the libraryModel # and NOT of the proxy models - selected_indexes = self.get_selection(self.sender()) + selected_indexes = self.get_selection() context_menu = QtWidgets.QMenu() diff --git a/lector/guifunctions.py b/lector/guifunctions.py index 9f73beb..d2f6016 100644 --- a/lector/guifunctions.py +++ b/lector/guifunctions.py @@ -241,6 +241,7 @@ class ViewProfileModification: self.format_contentView() def modify_comic_view(self, signal_sender, key_pressed): + comic_profile = self.main_window.comic_profile current_tab = self.tabWidget.widget(self.tabWidget.currentIndex()) self.bookToolBar.fitWidth.setChecked(False) @@ -248,34 +249,35 @@ class ViewProfileModification: self.bookToolBar.originalSize.setChecked(False) if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus: - self.comic_profile['zoom_mode'] = 'manualZoom' - self.comic_profile['padding'] += 50 + comic_profile['zoom_mode'] = 'manualZoom' + comic_profile['padding'] += 50 # This prevents infinite zoom out - if self.comic_profile['padding'] * 2 > current_tab.contentView.viewport().width(): - self.comic_profile['padding'] -= 50 + if comic_profile['padding'] * 2 > current_tab.contentView.viewport().width(): + comic_profile['padding'] -= 50 - if signal_sender == 'zoomIn' or key_pressed in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal): - self.comic_profile['zoom_mode'] = 'manualZoom' - self.comic_profile['padding'] -= 50 + if signal_sender == 'zoomIn' or key_pressed in ( + QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal): + comic_profile['zoom_mode'] = 'manualZoom' + comic_profile['padding'] -= 50 # This prevents infinite zoom in - if self.comic_profile['padding'] < 0: - self.comic_profile['padding'] = 0 + if comic_profile['padding'] < 0: + comic_profile['padding'] = 0 if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W: - self.comic_profile['zoom_mode'] = 'fitWidth' - self.comic_profile['padding'] = 0 + comic_profile['zoom_mode'] = 'fitWidth' + comic_profile['padding'] = 0 self.bookToolBar.fitWidth.setChecked(True) # Padding in the following cases is decided by # the image pixmap loaded by the widget if signal_sender == 'bestFit' or key_pressed == QtCore.Qt.Key_B: - self.comic_profile['zoom_mode'] = 'bestFit' + comic_profile['zoom_mode'] = 'bestFit' self.bookToolBar.bestFit.setChecked(True) if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O: - self.comic_profile['zoom_mode'] = 'originalSize' + comic_profile['zoom_mode'] = 'originalSize' self.bookToolBar.originalSize.setChecked(True) self.format_contentView() @@ -290,7 +292,6 @@ class ViewProfileModification: if current_metadata['images_only']: background = self.comic_profile['background'] - padding = self.comic_profile['padding'] zoom_mode = self.comic_profile['zoom_mode'] if zoom_mode == 'fitWidth': @@ -304,7 +305,7 @@ class ViewProfileModification: 'background-color: %s' % background.name()) current_tab.format_view( - None, None, None, background, padding, None, None) + None, None, None, background, None, None, None) else: profile_index = self.bookToolBar.profileBox.currentIndex() diff --git a/lector/parsers/pdf.py b/lector/parsers/pdf.py index 6929798..6997038 100644 --- a/lector/parsers/pdf.py +++ b/lector/parsers/pdf.py @@ -27,11 +27,7 @@ class ParsePDF: self.book = None def read_book(self): - try: - self.book = fitz.open(self.filename) - return True - except RuntimeError: - return False + self.book = fitz.open(self.filename) def generate_metadata(self): title = self.book.metadata['title'] diff --git a/lector/readers/read_epub.py b/lector/readers/read_epub.py index 0dddf45..f39366c 100644 --- a/lector/readers/read_epub.py +++ b/lector/readers/read_epub.py @@ -17,7 +17,6 @@ # TODO # See if inserting chapters not in the toc.ncx can be avoided # Account for stylesheets... eventually -# Everything needs logging import os import zipfile @@ -40,6 +39,7 @@ class EPUB: self.zip_file = None self.file_list = None self.opf_dict = None + self.cover_image_name = None self.split_chapters = {} self.metadata = None @@ -89,7 +89,7 @@ class EPUB: return i # If the file isn't found - logging.error(filename + ' not found in ' + self.book_filename) + logger.error(filename + ' not found in ' + self.book_filename) return False def generate_toc(self): @@ -247,25 +247,19 @@ class EPUB: toc_chapters = [ unquote(i[2].split('#')[0]) for i in self.content] - # TODO - # This totally borks the order - - last_valid_index = -2 # Yes, but why? for i in spine_final: if not i in toc_chapters: - previous_chapter = spine_final[spine_final.index(i) - 1] - try: + spine_index = spine_final.index(i) + if spine_index == 0: # Or chapter insertion circles back to the end + previous_chapter_toc_index = -1 + else: + previous_chapter = spine_final[spine_final.index(i) - 1] previous_chapter_toc_index = toc_chapters.index(previous_chapter) - # In case of 2+ consecutive missing chapters - last_valid_index = previous_chapter_toc_index - except ValueError: - last_valid_index += 1 - # Chapters are currently named None - # Blank chapters will later be removed - # and the None will be replaced by a number + toc_chapters.insert( + previous_chapter_toc_index + 1, i) self.content.insert( - last_valid_index + 1, [1, None, i]) + previous_chapter_toc_index + 1, [1, None, i]) # Parse split chapters as below # They can be picked up during the iteration through the toc @@ -334,28 +328,42 @@ class EPUB: chapter_title = i[1] if not chapter_title: chapter_title = unnamed_chapter_title - unnamed_chapter_title += 1 content_copy.append(( i[0], str(chapter_title), i[2])) + unnamed_chapter_title += 1 self.content = content_copy - # TODO - # This can probably be circumvented by shifting the extraction - # to this module and simply getting the path to the cover - # Get cover image and put it in its place # I imagine this involves saying nasty things to it + # There's no point shifting this to the parser + # The performance increase is negligible cover_image = self.generate_book_cover() + if cover_image: cover_path = os.path.join( self.temp_dir, os.path.basename(self.book_filename)) + ' - cover' with open(cover_path, 'wb') as cover_temp: cover_temp.write(cover_image) - # There's probably some rationale to doing an insert here - # But a replacement seems... neater - self.content.insert( - 0, (1, 'Cover', f'
Cover
')) + # This is probably stupid, but I can't stand the idea of + # having to look at two book covers + cover_replacement_conditions = ( + self.cover_image_name.lower() + '.jpg' in self.content[0][2].lower(), + self.cover_image_name.lower() + '.png' in self.content[0][2].lower(), + 'cover' in self.content[0][1].lower()) + + if True in cover_replacement_conditions: + logger.info( + f'Replacing cover {cover_replacement_conditions}: {self.book_filename}') + self.content[0] = ( + 1, 'Cover', + f'
Cover
') + else: + logger.info('Adding cover: ' + self.book_filename) + self.content.insert( + 0, + (1, 'Cover', + f'
Cover
')) def generate_metadata(self): book_metadata = self.opf_dict['package']['metadata'] @@ -443,6 +451,8 @@ class EPUB: if i['@media-type'].split('/')[0] == 'image' and 'cover' in i['@id']][0] book_cover = self.zip_file.read(self.find_file(cover_image)) + self.cover_image_name = os.path.splitext( + os.path.basename(cover_image))[0] except: pass diff --git a/lector/sorter.py b/lector/sorter.py index d63cb31..8a9df1a 100644 --- a/lector/sorter.py +++ b/lector/sorter.py @@ -124,8 +124,8 @@ class BookSorter: self.queue = Manager().Queue() self.processed_books = [] - # if self.work_mode == 'addition': - progress_object_generator() + if self.work_mode == 'addition': + progress_object_generator() def database_hashes(self): all_hashes_and_paths = database.DatabaseFunctions( @@ -140,9 +140,6 @@ class BookSorter: i[0]: i[1] for i in all_hashes_and_paths} def database_entry_for_book(self, file_hash): - # TODO - # This will probably look a whole lot better with a namedtuple - database_return = database.DatabaseFunctions( self.database_path).fetch_data( ('Title', 'Author', 'Year', 'ISBN', 'Tags', @@ -187,7 +184,8 @@ class BookSorter: or os.path.exists(self.hashes_and_paths[file_md5])): if not self.hashes_and_paths[file_md5] == filename: - warning_string = f'{os.path.basename(filename)} is already in database' + warning_string = ( + f'{os.path.basename(filename)} is already in database') logger.warning(warning_string) return @@ -214,8 +212,9 @@ class BookSorter: try: book_ref.read_book() - except: - logger.error('Error initializing: ' + filename) + except Exception as e: + this_error = f'Error initializing: {filename} {type(e).__name__} Arguments: {e.args}' + logger.exception(this_error) return this_book = {} @@ -227,8 +226,10 @@ class BookSorter: if self.work_mode == 'addition': try: metadata = book_ref.generate_metadata() - except: - logger.error('Metadata generation error: ' + filename) + except Exception as e: + this_error = ( + f'Metadata generation error: {filename} {type(e).__name__} Arguments: {e.args}') + logger.exception(this_error) return title = metadata.title @@ -255,16 +256,24 @@ class BookSorter: if self.work_mode == 'reading': try: book_breakdown = book_ref.generate_content() - except KeyboardInterrupt: - logger.error('Content generation error: ' + filename) + except Exception as e: + this_error = ( + f'Content generation error: {filename} {type(e).__name__} Arguments: {e.args}') + logger.exception(this_error) return toc = book_breakdown[0] content = book_breakdown[1] images_only = book_breakdown[2] - book_data = self.database_entry_for_book(file_md5) - title = book_data[0] + try: + book_data = self.database_entry_for_book(file_md5) + except TypeError: + logger.error( + f'Database error: {filename}. Re-add book to program') + return + + title = book_data[0].replace('&', '&&') author = book_data[1] year = book_data[2] isbn = book_data[3] diff --git a/lector/widgets.py b/lector/widgets.py index 65a795e..9f8b1df 100644 --- a/lector/widgets.py +++ b/lector/widgets.py @@ -521,30 +521,37 @@ class Tab(QtWidgets.QWidget): 'center': QtCore.Qt.AlignCenter, 'justify': QtCore.Qt.AlignJustify} - # Adjusted for books without covers - current_position = self.metadata['position']['current_chapter'] - chapter_name = self.metadata['toc'][current_position - 1][1] - - if current_position == 1 and chapter_name == 'Cover': - block_format.setAlignment( - QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) - else: - block_format.setAlignment(alignment_dict[text_alignment]) - # Also for padding # Using setViewPortMargins for this disables scrolling in the margins block_format.setLeftMargin(padding) block_format.setRightMargin(padding) this_cursor = self.contentView.textCursor() - this_cursor.movePosition(QtGui.QTextCursor.Start, 0, 1) + this_cursor.setPosition(QtGui.QTextCursor.Start) - # Iterate over the entire document block by block - # The document ends when the cursor position can no longer be incremented while True: + # So this fixes the stupid repetitive iteration + # I was doing over the entire thing + # It also allows for all images to be center aligned. + # Magic. *jazz hands* + block_text = this_cursor.block().text().strip() + try: + # Object replacement char - Seems to work with images + if ord(block_text) == 65532: + block_format.setAlignment( + QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) + else: + raise TypeError + except TypeError: + block_format.setAlignment(alignment_dict[text_alignment]) + + # Iterate over the entire document block by block + # The document ends when the cursor position can no longer be incremented old_position = this_cursor.position() this_cursor.mergeBlockFormat(block_format) - this_cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1) + this_cursor.movePosition( + QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.MoveAnchor) + new_position = this_cursor.position() if old_position == new_position: break