Library addition and loading

This commit is contained in:
BasioMeusPuga
2017-11-06 22:04:26 +05:30
parent ea399ff2c5
commit 8082aaf15a
3 changed files with 226 additions and 71 deletions

View File

@@ -24,9 +24,10 @@ import os
import sys import sys
from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5 import QtWidgets, QtGui, QtCore
import book_parser
import mainwindow import mainwindow
import database import database
import parser
class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
@@ -35,8 +36,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.setupUi(self) self.setupUi(self)
# Initialize application # Initialize application
Database(self) Settings(self).read_settings() # This should populate all variables that need
Settings(self).read_settings() # to be remembered across sessions
database.DatabaseInit(self.database_path)
Toolbars(self) Toolbars(self)
# New tabs and their contents # New tabs and their contents
@@ -52,8 +54,15 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.tabWidget.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, None) self.tabWidget.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, None)
self.tabWidget.tabCloseRequested.connect(self.close_tab_class) self.tabWidget.tabCloseRequested.connect(self.close_tab_class)
# ListView
self.listView.setSpacing(10)
self.reload_listview()
self.listView.doubleClicked.connect(self.listclick) self.listView.doubleClicked.connect(self.listclick)
# Keyboard shortcuts
self.exit_all = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'), self)
self.exit_all.activated.connect(QtWidgets.qApp.exit)
def create_tab_class(self): def create_tab_class(self):
# TODO # TODO
# Shift focus to tab if it's already open instead of creating # Shift focus to tab if it's already open instead of creating
@@ -70,9 +79,14 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self, 'Open file', self.last_open_path, "eBooks (*.epub *.mobi *.txt)") self, 'Open file', self.last_open_path, "eBooks (*.epub *.mobi *.txt)")
if my_file[0]: if my_file[0]:
self.last_open_path = os.path.dirname(my_file[0][0]) self.last_open_path = os.path.dirname(my_file[0][0])
print(self.last_open_path) books = book_parser.BookSorter(my_file[0])
books = parser.BookSorter(my_file[0]) parsed_books = books.initiate_threads()
books.add_to_database() database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
self.reload_listview()
def reload_listview(self):
lib_ref = Library(self)
lib_ref.load_listView()
def close_tab_class(self, tab_index): def close_tab_class(self, tab_index):
this_tab = Tabs(self, None) this_tab = Tabs(self, None)
@@ -105,47 +119,71 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.show() self.show()
self.current_textEdit.show() self.current_textEdit.show()
def populatelist(self):
self.listView.setWindowTitle('huh')
# The QlistView widget needs to be populated
# with a model that inherits from QStandardItemModel
model = QtGui.QStandardItemModel()
# Get the list of images from here
# Temp dir this out after getting the images from the
# database
my_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'thumbnails')
image_list = [os.path.join(my_dir, i) for i in os.listdir('./thumbnails')]
# Generate image pixmap and then pass it to the widget
# as a QIcon
# Additional data can be set using an incrementing
# QtCore.Qt.UserRole
# QtCore.Qt.DisplayRole is the same as item.setText()
# The model is a single row and has no columns
for i in image_list:
img_pixmap = QtGui.QPixmap(i)
# item = QtGui.QStandardItem(i.split('/')[-1:][0][:-4])
item = QtGui.QStandardItem()
item.setData('Additional data for ' + i.split('/')[-1:][0], QtCore.Qt.UserRole)
item.setIcon(QtGui.QIcon(img_pixmap))
model.appendRow(item)
s = QtCore.QSize(200, 200) # Set icon sizing here
self.listView.setIconSize(s)
self.listView.setModel(model)
def listclick(self, myindex): def listclick(self, myindex):
# print('selected item index found at %s with data: %s' % (myindex.row(), myindex.data())) # print('selected item index found at %s with data: %s' % (myindex.row(), myindex.data()))
index = self.listView.model().index(myindex.row(), 0) index = self.listView.model().index(myindex.row(), 0)
print(self.listView.model().data(index, QtCore.Qt.UserRole)) print(self.listView.model().data(index, QtCore.Qt.UserRole))
self.listView.setSpacing(10)
def closeEvent(self, event): def closeEvent(self, event):
Settings(self).save_settings() Settings(self).save_settings()
class Library:
def __init__(self, parent):
self.parent_window = parent
def load_listView(self):
# TODO
# Make this take hints from the SortBy dropdown, the FilterBy lineEdit
# and the fetch_data method in the database module
# The rest of it is just refreshing the listview
# The QlistView widget needs to be populated
# with a model that inherits from QStandardItemModel
model = QtGui.QStandardItemModel()
books = database.DatabaseFunctions(
self.parent_window.database_path).fetch_data(
('*',),
'books',
{'Title': ''},
'LIKE')
if not books:
print('Database returned nothing')
return
# The database query returns a tuple with the following indices
# Index 0 is the key ID is is ignored
for i in books:
book_title = i[1]
book_cover = i[6]
additional_data = {
'book_path': i[2],
'book_isbn': i[3],
'book_tags': i[4],
'book_hash': i[5]}
# Generate image pixmap and then pass it to the widget
# as a QIcon
# Additional data can be set using an incrementing
# QtCore.Qt.UserRole
# 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)
item = QtGui.QStandardItem(book_title)
item.setData(additional_data, QtCore.Qt.UserRole)
item.setIcon(QtGui.QIcon(img_pixmap))
model.appendRow(item)
s = QtCore.QSize(200, 200) # Set icon sizing here
self.parent_window.listView.setIconSize(s)
self.parent_window.listView.setModel(model)
class Settings: class Settings:
def __init__(self, parent): def __init__(self, parent):
self.parent_window = parent self.parent_window = parent
@@ -161,10 +199,12 @@ class Settings:
QtCore.QPoint(286, 141))) QtCore.QPoint(286, 141)))
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('path') self.settings.beginGroup('runtimeVariables')
self.parent_window.last_open_path = self.settings.value( self.parent_window.last_open_path = self.settings.value(
'path', os.path.expanduser('~')) 'lastOpenPath', os.path.expanduser('~'))
print(self.parent_window.last_open_path) self.parent_window.database_path = self.settings.value(
'databasePath',
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation))
self.settings.endGroup() self.settings.endGroup()
def save_settings(self): def save_settings(self):
@@ -173,8 +213,9 @@ class Settings:
self.settings.setValue('windowPosition', self.parent_window.pos()) self.settings.setValue('windowPosition', self.parent_window.pos())
self.settings.endGroup() self.settings.endGroup()
self.settings.beginGroup('lastOpen') self.settings.beginGroup('runtimeVariables')
self.settings.setValue('path', self.parent_window.last_open_path) self.settings.setValue('lastOpenPath', self.parent_window.last_open_path)
self.settings.setValue('databasePath', self.parent_window.database_path)
self.settings.endGroup() self.settings.endGroup()
@@ -207,7 +248,7 @@ class Toolbars:
addButton.triggered.connect(self.parent_window.open_file) addButton.triggered.connect(self.parent_window.open_file)
settingsButton.triggered.connect(self.parent_window.create_tab_class) settingsButton.triggered.connect(self.parent_window.create_tab_class)
deleteButton.triggered.connect(self.parent_window.populatelist) deleteButton.triggered.connect(self.parent_window.reload_listview)
self.parent_window.LibraryToolBar.addAction(addButton) self.parent_window.LibraryToolBar.addAction(addButton)
self.parent_window.LibraryToolBar.addAction(deleteButton) self.parent_window.LibraryToolBar.addAction(deleteButton)
@@ -238,15 +279,6 @@ class Tabs:
self.parent_window.tabWidget.removeTab(tab_index) self.parent_window.tabWidget.removeTab(tab_index)
class Database:
# This is maybe, possibly, redundant
def __init__(self, parent):
self.parent_window = parent
self.database_path = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.AppDataLocation)
self.db = database.DatabaseFunctions(self.database_path)
def main(): def main():
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
app.setApplicationName('Lector') # This is needed for QStandardPaths app.setApplicationName('Lector') # This is needed for QStandardPaths

View File

@@ -2,16 +2,22 @@
import os import os
import re import re
import collections import hashlib
from multiprocessing.dummy import Pool
import ebooklib.epub import ebooklib.epub
class ParseEPUB: class ParseEPUB:
def __init__(self, filename): def __init__(self, filename):
# TODO
# Maybe also include book description
self.filename = filename self.filename = filename
self.book_title = None self.book = None
def read_epub(self):
try: try:
self.book = ebooklib.epub.read_epub(filename) self.book = ebooklib.epub.read_epub(self.filename)
except (KeyError, AttributeError): except (KeyError, AttributeError):
print('Cannot parse ' + self.filename) print('Cannot parse ' + self.filename)
return return
@@ -20,6 +26,11 @@ class ParseEPUB:
return self.book.title.strip() return self.book.title.strip()
def get_cover_image(self): def get_cover_image(self):
# TODO
# Generate a cover image in case one isn't found
# This has to be done or the database module will
# error out
# Get cover image # Get cover image
# This seems hack-ish, but that's never stopped me before # This seems hack-ish, but that's never stopped me before
image_path = None image_path = None
@@ -59,7 +70,6 @@ class ParseEPUB:
return image_content return image_content
except KeyError: except KeyError:
print('Cannot parse ' + self.filename)
return return
def get_isbn(self): def get_isbn(self):
@@ -83,13 +93,39 @@ class BookSorter:
# Parsing for the reader proper # Parsing for the reader proper
# Caching upon closing # Caching upon closing
self.file_list = file_list self.file_list = file_list
self.all_books = {}
def add_to_database(self): def read_book(self, filename):
# Consider multithreading this # filename is expected as a string containg the
for i in self.file_list: # full path of the ebook file
book_ref = ParseEPUB(i)
# TODO
# See if you want to include a hash of the book's name and author
with open(filename, 'rb') as current_book:
file_md5 = hashlib.md5(current_book.read()).hexdigest()
if file_md5 in self.all_books.items():
return
# TODO
# See if tags can be generated from book content
book_ref = ParseEPUB(filename)
book_ref.read_epub()
if book_ref.book:
title = book_ref.get_title() title = book_ref.get_title()
cover_image = book_ref.get_cover_image() cover_image = book_ref.get_cover_image()
isbn = book_ref.get_isbn() isbn = book_ref.get_isbn()
print(title, isbn) self.all_books[file_md5] = {
'title': title,
'isbn': isbn,
'path': filename,
'cover_image': cover_image}
def initiate_threads(self):
_pool = Pool(5)
_pool.map(self.read_book, self.file_list)
_pool.close()
_pool.join()
return self.all_books

View File

@@ -3,21 +3,22 @@
import sqlite3 import sqlite3
import os import os
class DatabaseInit:
class DatabaseFunctions:
def __init__(self, location_prefix): def __init__(self, location_prefix):
os.makedirs(location_prefix, exist_ok=True) os.makedirs(location_prefix, exist_ok=True)
self.database_path = os.path.join( database_path = os.path.join(location_prefix, 'Lector.db')
location_prefix, 'Lector.db')
self.database = sqlite3.connect(self.database_path) if not os.path.exists(database_path):
if not os.path.exists(self.database_path): self.database = sqlite3.connect(database_path)
self.create_database() self.create_database()
else:
self.database = sqlite3.connect(database_path)
def create_database(self): def create_database(self):
self.database.execute( self.database.execute(
"CREATE TABLE books \ "CREATE TABLE books \
(id INTEGER PRIMARY KEY, Name TEXT, Path TEXT, ISBN TEXT, Tags TEXT, CoverImage BLOB)") (id INTEGER PRIMARY KEY, Title TEXT, Path TEXT, \
ISBN TEXT, Tags TEXT, Hash TEXT, CoverImage BLOB)")
self.database.execute( self.database.execute(
"CREATE TABLE cache \ "CREATE TABLE cache \
(id INTEGER PRIMARY KEY, Name TEXT, Path TEXT, CachedDict BLOB)") (id INTEGER PRIMARY KEY, Name TEXT, Path TEXT, CachedDict BLOB)")
@@ -26,5 +27,91 @@ class DatabaseFunctions:
self.database.commit() self.database.commit()
def add_to_database(self, book_data, image_data):
pass 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
# 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():
book_hash = i[0]
book_title = i[1]['title'].replace("'", "")
book_path = i[1]['path']
book_cover = i[1]['cover_image']
book_isbn = i[1]['isbn']
# Check if the file might not already be in the database
hash_from_database = self.fetch_data(
('Title',),
'books',
{'Hash': book_hash},
'EQUALS',
True)
sql_command_add = (
"INSERT INTO books (Title,Path,ISBN,Hash,CoverImage) VALUES(?, ?, ?, ?, ?)")
# TODO
# This is a placeholder. You will need to generate book covers
# in case none are found
if not hash_from_database and book_cover:
self.database.execute(
sql_command_add,
[book_title, book_path, book_isbn, book_hash, sqlite3.Binary(book_cover)])
self.database.commit()
def fetch_data(self, columns, table, selection_criteria, equivalence, fetch_one=False):
# columns is a tuple that will be passed as a comma separated list
# table is a string that will be used as is
# selection_criteria is a dictionary which contains the name of a column linked
# to a corresponding value for selection
# Example:
# Name and AltName are expected to be the same
# sel_dict = {
# 'Name': 'sav',
# 'AltName': 'sav'
# }
# data = DatabaseFunctions().fetch_data(('Name',), 'books', sel_dict)
try:
column_list = ','.join(columns)
sql_command_fetch = f"SELECT {column_list} FROM {table}"
if selection_criteria:
sql_command_fetch += " WHERE"
if equivalence == 'EQUALS':
for i in selection_criteria.keys():
search_parameter = selection_criteria[i]
sql_command_fetch += f" {i} = '{search_parameter}' OR"
elif equivalence == 'LIKE':
for i in selection_criteria.keys():
search_parameter = "'%" + selection_criteria[i] + "%'"
sql_command_fetch += f" {i} LIKE {search_parameter} OR"
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()
if book_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 book_data
else:
return None
# except sqlite3.OperationalError:
except KeyError:
print('SQLite is in rebellion, Commander')