Library addition and loading
This commit is contained in:
122
__main__.py
122
__main__.py
@@ -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,19 +119,51 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
self.show()
|
self.show()
|
||||||
self.current_textEdit.show()
|
self.current_textEdit.show()
|
||||||
|
|
||||||
def populatelist(self):
|
def listclick(self, myindex):
|
||||||
self.listView.setWindowTitle('huh')
|
# print('selected item index found at %s with data: %s' % (myindex.row(), myindex.data()))
|
||||||
|
index = self.listView.model().index(myindex.row(), 0)
|
||||||
|
print(self.listView.model().data(index, QtCore.Qt.UserRole))
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
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
|
# The QlistView widget needs to be populated
|
||||||
# with a model that inherits from QStandardItemModel
|
# with a model that inherits from QStandardItemModel
|
||||||
model = QtGui.QStandardItemModel()
|
model = QtGui.QStandardItemModel()
|
||||||
|
|
||||||
# Get the list of images from here
|
books = database.DatabaseFunctions(
|
||||||
# Temp dir this out after getting the images from the
|
self.parent_window.database_path).fetch_data(
|
||||||
# database
|
('*',),
|
||||||
my_dir = os.path.join(
|
'books',
|
||||||
os.path.dirname(os.path.realpath(__file__)), 'thumbnails')
|
{'Title': ''},
|
||||||
image_list = [os.path.join(my_dir, i) for i in os.listdir('./thumbnails')]
|
'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
|
# Generate image pixmap and then pass it to the widget
|
||||||
# as a QIcon
|
# as a QIcon
|
||||||
@@ -125,25 +171,17 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
# QtCore.Qt.UserRole
|
# QtCore.Qt.UserRole
|
||||||
# QtCore.Qt.DisplayRole is the same as item.setText()
|
# QtCore.Qt.DisplayRole is the same as item.setText()
|
||||||
# The model is a single row and has no columns
|
# The model is a single row and has no columns
|
||||||
for i in image_list:
|
|
||||||
img_pixmap = QtGui.QPixmap(i)
|
img_pixmap = QtGui.QPixmap()
|
||||||
# item = QtGui.QStandardItem(i.split('/')[-1:][0][:-4])
|
img_pixmap.loadFromData(book_cover)
|
||||||
item = QtGui.QStandardItem()
|
item = QtGui.QStandardItem(book_title)
|
||||||
item.setData('Additional data for ' + i.split('/')[-1:][0], QtCore.Qt.UserRole)
|
item.setData(additional_data, QtCore.Qt.UserRole)
|
||||||
item.setIcon(QtGui.QIcon(img_pixmap))
|
item.setIcon(QtGui.QIcon(img_pixmap))
|
||||||
model.appendRow(item)
|
model.appendRow(item)
|
||||||
|
|
||||||
s = QtCore.QSize(200, 200) # Set icon sizing here
|
s = QtCore.QSize(200, 200) # Set icon sizing here
|
||||||
self.listView.setIconSize(s)
|
self.parent_window.listView.setIconSize(s)
|
||||||
self.listView.setModel(model)
|
self.parent_window.listView.setModel(model)
|
||||||
|
|
||||||
def listclick(self, myindex):
|
|
||||||
# print('selected item index found at %s with data: %s' % (myindex.row(), myindex.data()))
|
|
||||||
index = self.listView.model().index(myindex.row(), 0)
|
|
||||||
print(self.listView.model().data(index, QtCore.Qt.UserRole))
|
|
||||||
self.listView.setSpacing(10)
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
Settings(self).save_settings()
|
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
@@ -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
|
||||||
|
@@ -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
|
105
database.py
105
database.py
@@ -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')
|
||||||
|
Reference in New Issue
Block a user