Initial file loading

epub content parsing is horribly borked
This commit is contained in:
BasioMeusPuga
2017-11-11 23:21:49 +05:30
parent 5d495cfde3
commit 7fbea194c0
6 changed files with 236 additions and 120 deletions

View File

@@ -5,6 +5,7 @@
Check files (hashes) upon restart
Recursive file addition
Show what on startup
If cache large files
Library:
✓ sqlite3 for cover images cache
✓ sqlite3 for storing metadata
@@ -13,8 +14,9 @@
✓ Image reflow
✓ Search bar in toolbar
✓ Shift focus to the tab that has the book open
? Create emblem per filetype
Look into how you might group icons
Ignore a / the / numbers for sorting purposes
Maybe create emblem per filetype
Put the path in the scope of the search
maybe as a type: switch
Mass tagging
@@ -26,17 +28,18 @@
✓ Override the keypress event of the textedit
✓ Use format* icons for toolbar buttons
✓ Implement book view settings with a(nother) toolbar
Consider substituting the textedit for another widget
? Substitute textedit for another widget
All ebooks should first be added to the database and then returned as HTML
Pagination
Theming
Set context menu for definitions and the like
Keep fontsize and margins consistent - Let page increase in length
Filetypes:
? Plugin system for parsers
? pdf support
epub support
mobi, azw support
txt, doc, djvu support
pdf support?
cbz, cbr support
Keep font settings enabled but only for background color
Internet:
@@ -44,7 +47,7 @@
Get ISBN using python-isbnlib
Other:
✓ Define every widget in code
Maybe include icons for emblems
? Include icons for emblems
"""
import os
@@ -91,6 +94,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.bookToolBar = BookToolBar(self)
self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)
self.bookToolBar.tocBox.activated.connect(self.set_toc_position)
self.addToolBar(self.bookToolBar)
# Make the correct toolbar visible
@@ -99,10 +103,12 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
# New tabs and their contents
self.current_tab = None
self.current_textEdit = None
self.current_contentView = None
# Tab closing
self.tabWidget.setTabsClosable(True)
# TODO
# It's possible to add a widget to the Library tab here
self.tabWidget.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, None)
self.tabWidget.tabCloseRequested.connect(self.close_tab)
@@ -162,7 +168,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if my_file[0]:
self.listView.setEnabled(False)
self.last_open_path = os.path.dirname(my_file[0][0])
books = sorter.BookSorter(my_file[0], self.database_path)
books = sorter.BookSorter(my_file[0], 'addition', self.database_path)
parsed_books = books.initiate_threads()
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
self.listView.setEnabled(True)
@@ -176,11 +182,14 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if box_button.text() == '&Yes':
selected_hashes = []
for i in selected_books:
book_data = i.data(QtCore.Qt.UserRole + 3)
selected_hashes.append(book_data['book_hash'])
data = i.data(QtCore.Qt.UserRole + 3)
selected_hashes.append(data['hash'])
database.DatabaseFunctions(
self.database_path).delete_from_database(selected_hashes)
self.viewModel = None
self.viewModel = None # TODO
# Delete the item from the model instead
# of reconstructing it
# The same goes for addition
self.reload_listview()
selected_number = len(selected_books)
@@ -205,8 +214,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
def tab_switch(self):
if self.tabWidget.currentIndex() == 0:
self.bookToolBar.hide()
self.libraryToolBar.show()
if self.lib_ref.proxy_model:
# Making the proxy model available doesn't affect
# memory utilization at all. Bleh.
@@ -215,51 +226,76 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
else:
self.bookToolBar.show()
self.libraryToolBar.hide()
current_metadata = self.tabWidget.widget(
self.tabWidget.currentIndex()).book_metadata
current_title = current_metadata['book_title']
current_author = current_metadata['book_author']
self.tabWidget.currentIndex()).metadata
current_title = current_metadata['title']
current_author = current_metadata['author']
current_position = current_metadata['position']
current_toc = current_metadata['content'].keys()
self.bookToolBar.tocBox.blockSignals(True)
self.bookToolBar.tocBox.clear()
self.bookToolBar.tocBox.addItems(current_toc)
if current_position:
self.bookToolBar.tocBox.setCurrentIndex(current_position)
self.bookToolBar.tocBox.blockSignals(False)
self.statusMessage.setText(
current_author + ' - ' + current_title)
def set_toc_position(self, event=None):
self.tabWidget.widget(
self.tabWidget.currentIndex()).metadata[
'position'] = event
chapter_name = self.bookToolBar.tocBox.currentText()
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
required_content = current_tab.metadata['content'][chapter_name]
current_tab.contentView.setHtml(required_content)
def set_fullscreen(self):
self.current_tab = self.tabWidget.currentIndex()
self.current_textEdit = self.tabWidget.widget(self.current_tab)
self.current_contentView = self.tabWidget.widget(self.current_tab)
self.exit_shortcut = QtWidgets.QShortcut(
QtGui.QKeySequence('Escape'), self.current_textEdit)
QtGui.QKeySequence('Escape'), self.current_contentView)
self.exit_shortcut.activated.connect(self.set_normalsize)
self.current_textEdit.setWindowFlags(QtCore.Qt.Window)
self.current_textEdit.setWindowState(QtCore.Qt.WindowFullScreen)
self.current_contentView.setWindowFlags(QtCore.Qt.Window)
self.current_contentView.setWindowState(QtCore.Qt.WindowFullScreen)
self.hide()
self.current_textEdit.show()
self.current_contentView.show()
def set_normalsize(self):
self.current_textEdit.setWindowState(QtCore.Qt.WindowNoState)
self.current_textEdit.setWindowFlags(QtCore.Qt.Widget)
self.current_contentView.setWindowState(QtCore.Qt.WindowNoState)
self.current_contentView.setWindowFlags(QtCore.Qt.Widget)
self.show()
self.current_textEdit.show()
self.current_contentView.show()
def list_doubleclick(self, myindex):
# TODO
# Load the book.
index = self.listView.model().index(myindex.row(), 0)
book_metadata = self.listView.model().data(index, QtCore.Qt.UserRole + 3)
metadata = self.listView.model().data(index, QtCore.Qt.UserRole + 3)
# Shift focus to the tab that has the book open (if there is one)
for i in range(1, self.tabWidget.count()):
tab_book_metadata = self.tabWidget.widget(i).book_metadata
if tab_book_metadata['book_hash'] == book_metadata['book_hash']:
tab_metadata = self.tabWidget.widget(i).metadata
if tab_metadata['hash'] == metadata['hash']:
self.tabWidget.setCurrentIndex(i)
return
tab_ref = Tab(book_metadata, self.tabWidget)
path = metadata['path']
contents = sorter.BookSorter(
[path], 'reading', self.database_path).initiate_threads()
tab_ref = Tab(contents, self.tabWidget)
self.tabWidget.setCurrentWidget(tab_ref)
print(tab_ref.book_metadata) # Metadata upon tab creation
# print(tab_ref.book_metadata) # Metadata upon tab creation
def close_tab(self, tab_index):
print(self.tabWidget.widget(tab_index).book_metadata) # Metadata upon tab deletion
# print(self.tabWidget.widget(tab_index).metadata) # Metadata upon tab deletion
self.tabWidget.removeTab(tab_index)
def closeEvent(self, event=None):

View File

@@ -3,6 +3,7 @@
import sqlite3
import os
class DatabaseInit:
def __init__(self, location_prefix):
os.makedirs(location_prefix, exist_ok=True)
@@ -16,7 +17,7 @@ class DatabaseInit:
self.database.execute(
"CREATE TABLE books \
(id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, \
Path TEXT, ISBN TEXT, Tags TEXT, Hash TEXT, CoverImage BLOB)")
Path TEXT, Position TEXT, ISBN TEXT, Tags TEXT, Hash TEXT, CoverImage BLOB)")
self.database.execute(
"CREATE TABLE cache \
(id INTEGER PRIMARY KEY, Name TEXT, Path TEXT, CachedDict BLOB)")
@@ -26,28 +27,29 @@ class DatabaseInit:
self.database.commit()
self.database.close()
class DatabaseFunctions:
def __init__(self, location_prefix):
database_path = os.path.join(location_prefix, 'Lector.db')
self.database = sqlite3.connect(database_path)
def add_to_database(self, book_data):
# book_data is expected to be a dictionary
def add_to_database(self, data):
# data is expected to be a dictionary
# with keys corresponding to the book hash
# and corresponding items containing
# whatever else needs insertion
# Haha I said insertion
for i in book_data.items():
for i in data.items():
book_hash = i[0]
book_title = i[1]['title']
book_author = i[1]['author']
book_year = i[1]['year']
if not book_year:
book_year = 9999
book_path = i[1]['path']
book_cover = i[1]['cover_image']
book_isbn = i[1]['isbn']
title = i[1]['title']
author = i[1]['author']
year = i[1]['year']
if not year:
year = 9999
path = i[1]['path']
cover = i[1]['cover_image']
isbn = i[1]['isbn']
sql_command_add = (
"INSERT INTO books (Title,Author,Year,Path,ISBN,Hash,CoverImage) VALUES(?, ?, ?, ?, ?, ?, ?)")
@@ -55,11 +57,11 @@ class DatabaseFunctions:
# TODO
# This is a placeholder. You will need to generate book covers
# in case none are found
if book_cover:
if cover:
self.database.execute(
sql_command_add,
[book_title, book_author, book_year,
book_path, book_isbn, book_hash, sqlite3.Binary(book_cover)])
[title, author, year,
path, isbn, book_hash, sqlite3.Binary(cover)])
self.database.commit()
@@ -95,15 +97,15 @@ class DatabaseFunctions:
sql_command_fetch = sql_command_fetch[:-3] # Truncate the last OR
# book data is returned as a list of tuples
book_data = self.database.execute(sql_command_fetch).fetchall()
data = self.database.execute(sql_command_fetch).fetchall()
if book_data:
if data:
# Because this is the result of a fetchall(), we need an
# ugly hack (tm) to get correct results
if fetch_one:
return book_data[0][0]
return data[0][0]
return book_data
return data
else:
return None

View File

@@ -10,6 +10,7 @@
import os
import re
import collections
import ebooklib.epub
@@ -24,7 +25,7 @@ class ParseEPUB:
def read_book(self):
try:
self.book = ebooklib.epub.read_epub(self.filename)
except (KeyError, AttributeError):
except (KeyError, AttributeError, FileNotFoundError):
print('Cannot parse ' + self.filename)
return
@@ -100,3 +101,40 @@ class ParseEPUB:
return isbn
except KeyError:
return
def get_contents(self):
contents = collections.OrderedDict()
def flatten_chapter(toc_element):
output_list = []
for i in toc_element:
if isinstance(i, (tuple, list)):
output_list.extend(flatten_chapter(i))
else:
output_list.append(i)
return output_list
for i in self.book.toc:
if isinstance(i, (tuple, list)):
title = i[0].title
contents[title] = 'Composite Chapter'
# composite_chapter = flatten_chapter(i)
# composite_chapter_content = []
# for j in composite_chapter:
# href = j.href
# composite_chapter_content.append(
# self.book.get_item_with_href(href).get_content())
# contents[title] = composite_chapter_content
else:
title = i.title
href = i.href
try:
content = self.book.get_item_with_href(href).get_content()
if content:
contents[title] = content.decode()
else:
raise AttributeError
except AttributeError:
contents[title] = ''
return contents

View File

@@ -1,16 +1,20 @@
#!/usr/bin/env python3
# TODO
# Methods that return None must be quantified here if needed
# Methods that return None must be quantified within the parsing module
# See if tags can be generated from book content
# See if you want to include a hash of the book's name and author
import os
import hashlib
from multiprocessing.dummy import Pool
import database
from parsers.epub import ParseEPUB
class BookSorter:
def __init__(self, file_list, database_path):
def __init__(self, file_list, mode, database_path):
# Have the GUI pass a list of files straight to here
# Then, on the basis of what is needed, pass the
# filenames to the requisite functions
@@ -21,6 +25,8 @@ class BookSorter:
self.all_books = {}
self.database_path = database_path
self.hashes = []
self.mode = mode
if database_path:
self.database_hashes()
def database_hashes(self):
@@ -34,6 +40,16 @@ class BookSorter:
if all_hashes:
self.hashes = [i[0] for i in all_hashes]
def database_position(self, file_hash):
position = database.DatabaseFunctions(
self.database_path).fetch_data(
('Position',),
'books',
{'Hash': file_hash},
'EQUALS',
True)
return position
def read_book(self, filename):
# filename is expected as a string containg the
# full path of the ebook file
@@ -41,17 +57,21 @@ class BookSorter:
with open(filename, 'rb') as current_book:
file_md5 = hashlib.md5(current_book.read()).hexdigest()
# IF the file is NOT being loaded into the reader,
# Do not allow addition in case the file is dupicated in the directory
# OR is already in the database
# TODO
# See if you want to include a hash of the book's name and author
if file_md5 in self.all_books.items() or file_md5 in self.hashes:
# This should not get triggered in reading mode
if (self.mode == 'addition'
and (file_md5 in self.all_books.items() or file_md5 in self.hashes)):
return
# TODO
# See if tags can be generated from book content
# Sort according to to file extension here
# Select sorter by file extension
try:
file_extension = os.path.splitext(filename)[1][1:]
if file_extension == 'epub':
book_ref = ParseEPUB(filename)
except IndexError:
return
# Everything following this is standard
# Some of the None returns will have to have
@@ -61,9 +81,11 @@ class BookSorter:
title = book_ref.get_title()
author = book_ref.get_author()
year = book_ref.get_year()
cover_image = book_ref.get_cover_image()
isbn = book_ref.get_isbn()
# Different modes require different values
if self.mode == 'addition':
cover_image = book_ref.get_cover_image()
self.all_books[file_md5] = {
'title': title,
'author': author,
@@ -72,6 +94,20 @@ class BookSorter:
'path': filename,
'cover_image': cover_image}
if self.mode == 'reading':
content = book_ref.get_contents()
position = self.database_position(file_md5)
self.all_books = {
'title': title,
'author': author,
'year': year,
'isbn': isbn,
'hash': file_md5,
'path': filename,
'position': position,
'content': content}
def initiate_threads(self):
_pool = Pool(5)
_pool.map(self.read_book, self.file_list)

View File

@@ -31,51 +31,52 @@ class Library:
for i in books:
# The database query returns a tuple with the following indices
# Index 0 is the key ID is ignored
book_title = i[1]
book_author = i[2]
book_year = i[3]
book_cover = i[8]
book_tags = i[6]
book_path = i[4]
book_progress = None # TODO
title = i[1]
author = i[2]
year = i[3]
path = i[4]
tags = i[6]
cover = i[9]
progress = None # TODO
# Leave at None for an untouched book
# 'completed' for a completed book
# whatever else is here can be used
# to remember position
# Maybe get from the position param
all_metadata = {
'book_title': i[1],
'book_author': i[2],
'book_year': i[3],
'book_path': i[4],
'book_isbn': i[5],
'book_tags': i[6],
'book_hash': i[7]}
'title': i[1],
'author': i[2],
'year': i[3],
'path': i[4],
'position': i[5],
'isbn': i[6],
'tags': i[7],
'hash': i[8]}
tooltip_string = book_title + '\nAuthor: ' + book_author + '\nYear: ' + str(book_year)
if book_tags:
tooltip_string += ('\nTags: ' + book_tags)
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year)
if tags:
tooltip_string += ('\nTags: ' + tags)
# This remarkably ugly hack is because the QSortFilterProxyModel
# doesn't easily allow searching through multiple item roles
search_workaround = book_title + ' ' + book_author
if book_tags:
search_workaround += book_tags
search_workaround = title + ' ' + author
if tags:
search_workaround += tags
# Generate book state for passing onto the QStyledItemDelegate
def generate_book_state(book_path, book_progress):
if not os.path.exists(book_path):
def generate_book_state(path, progress):
if not os.path.exists(path):
return 'deleted'
if book_progress:
if book_progress == 'completed':
if progress:
if progress == 'completed':
return 'completed'
else:
return 'inprogress'
else:
return None
book_state = generate_book_state(book_path, book_progress)
state = generate_book_state(path, progress)
# Generate image pixmap and then pass it to the widget
# as a QIcon
@@ -84,17 +85,17 @@ class Library:
# QtCore.Qt.DisplayRole is the same as item.setText()
# The model is a single row and has no columns
img_pixmap = QtGui.QPixmap()
img_pixmap.loadFromData(book_cover)
img_pixmap.loadFromData(cover)
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
item = QtGui.QStandardItem()
item.setToolTip(tooltip_string)
# The following order is needed to keep sorting working
item.setData(book_title, QtCore.Qt.UserRole)
item.setData(book_author, QtCore.Qt.UserRole + 1)
item.setData(book_year, QtCore.Qt.UserRole + 2)
item.setData(title, QtCore.Qt.UserRole)
item.setData(author, QtCore.Qt.UserRole + 1)
item.setData(year, QtCore.Qt.UserRole + 2)
item.setData(all_metadata, QtCore.Qt.UserRole + 3)
item.setData(search_workaround, QtCore.Qt.UserRole + 4)
item.setData(book_state, QtCore.Qt.UserRole + 5)
item.setData(state, QtCore.Qt.UserRole + 5)
item.setIcon(QtGui.QIcon(img_pixmap))
self.parent_window.viewModel.appendRow(item)
@@ -108,7 +109,8 @@ class Library:
def update_proxymodel(self):
self.proxy_model.setFilterRole(QtCore.Qt.UserRole + 4)
self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.proxy_model.setFilterWildcard(self.parent_window.libraryToolBar.filterEdit.text())
self.proxy_model.setFilterWildcard(
self.parent_window.libraryToolBar.filterEdit.text())
self.parent_window.statusMessage.setText(
str(self.proxy_model.rowCount()) + ' books')

View File

@@ -97,9 +97,7 @@ class BookToolBar(QtWidgets.QToolBar):
self.searchBar.setObjectName('searchBar')
# Sorter
sorting_choices = ['Chapter ' + str(i) for i in range(1, 11)]
self.tocBox = QtWidgets.QComboBox()
self.tocBox.addItems(sorting_choices)
self.tocBox.setObjectName('sortingBox')
self.tocBox.setSizePolicy(sizePolicy)
self.tocBox.setMinimumContentsLength(10)
@@ -216,27 +214,31 @@ class LibraryToolBar(QtWidgets.QToolBar):
class Tab(QtWidgets.QWidget):
def __init__(self, book_metadata, parent=None):
def __init__(self, metadata, parent=None):
# TODO
# The display widget will probably have to be shifted to something else
# A horizontal slider to control flow
# Keyboard shortcuts
# The content display widget is currently a QTextBrowser
super(Tab, self).__init__(parent)
self.parent = parent
self.book_metadata = book_metadata # Save progress data into this dictionary
self.metadata = metadata # Save progress data into this dictionary
self.setStyleSheet("background-color: black")
book_title = self.book_metadata['book_title']
book_path = self.book_metadata['book_path']
title = self.metadata['title']
path = self.metadata['path']
self.gridLayout = QtWidgets.QGridLayout(self)
self.gridLayout.setObjectName("gridLayout")
self.textEdit = QtWidgets.QTextEdit(self)
self.textEdit.setObjectName("textEdit")
self.textEdit.setFrameShape(QtWidgets.QFrame.NoFrame)
self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1)
self.parent.addTab(self, book_title)
self.textEdit.setText(book_path)
self.contentView = QtWidgets.QTextBrowser(self)
self.contentView.setFrameShape(QtWidgets.QFrame.NoFrame)
self.contentView.setObjectName("contentView")
self.contentView.verticalScrollBar().setSingleStep(7)
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.gridLayout.addWidget(self.contentView, 0, 0, 1, 1)
self.parent.addTab(self, title)
self.contentView.setStyleSheet(
"QTextEdit {font-size:20px; padding-left:100; padding-right:100; background-color:black}")
class LibraryDelegate(QtWidgets.QStyledItemDelegate):
@@ -246,13 +248,13 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
option = option.__class__(option)
book_state = index.data(QtCore.Qt.UserRole + 5)
if book_state:
if book_state == 'deleted':
state = index.data(QtCore.Qt.UserRole + 5)
if state:
if state == 'deleted':
read_icon = QtGui.QIcon.fromTheme('vcs-conflicting').pixmap(36)
if book_state == 'completed':
if state == 'completed':
read_icon = QtGui.QIcon.fromTheme('vcs-normal').pixmap(36)
if book_state == 'inprogress':
if state == 'inprogress':
read_icon = QtGui.QIcon.fromTheme('vcs-locally-modified').pixmap(36)
else:
return