Library addition and loading
This commit is contained in:
122
__main__.py
122
__main__.py
@@ -24,9 +24,10 @@ import os
|
||||
import sys
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
import book_parser
|
||||
import mainwindow
|
||||
import database
|
||||
import parser
|
||||
|
||||
|
||||
class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
@@ -35,8 +36,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.setupUi(self)
|
||||
|
||||
# Initialize application
|
||||
Database(self)
|
||||
Settings(self).read_settings()
|
||||
Settings(self).read_settings() # This should populate all variables that need
|
||||
# to be remembered across sessions
|
||||
database.DatabaseInit(self.database_path)
|
||||
Toolbars(self)
|
||||
|
||||
# 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.tabCloseRequested.connect(self.close_tab_class)
|
||||
|
||||
# ListView
|
||||
self.listView.setSpacing(10)
|
||||
self.reload_listview()
|
||||
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):
|
||||
# TODO
|
||||
# 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)")
|
||||
if my_file[0]:
|
||||
self.last_open_path = os.path.dirname(my_file[0][0])
|
||||
print(self.last_open_path)
|
||||
books = parser.BookSorter(my_file[0])
|
||||
books.add_to_database()
|
||||
books = book_parser.BookSorter(my_file[0])
|
||||
parsed_books = books.initiate_threads()
|
||||
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):
|
||||
this_tab = Tabs(self, None)
|
||||
@@ -105,19 +119,51 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
self.show()
|
||||
self.current_textEdit.show()
|
||||
|
||||
def populatelist(self):
|
||||
self.listView.setWindowTitle('huh')
|
||||
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))
|
||||
|
||||
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
|
||||
# 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')]
|
||||
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
|
||||
@@ -125,25 +171,17 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||
# 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)
|
||||
|
||||
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.listView.setIconSize(s)
|
||||
self.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()
|
||||
self.parent_window.listView.setIconSize(s)
|
||||
self.parent_window.listView.setModel(model)
|
||||
|
||||
|
||||
class Settings:
|
||||
@@ -161,10 +199,12 @@ class Settings:
|
||||
QtCore.QPoint(286, 141)))
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('path')
|
||||
self.settings.beginGroup('runtimeVariables')
|
||||
self.parent_window.last_open_path = self.settings.value(
|
||||
'path', os.path.expanduser('~'))
|
||||
print(self.parent_window.last_open_path)
|
||||
'lastOpenPath', os.path.expanduser('~'))
|
||||
self.parent_window.database_path = self.settings.value(
|
||||
'databasePath',
|
||||
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation))
|
||||
self.settings.endGroup()
|
||||
|
||||
def save_settings(self):
|
||||
@@ -173,8 +213,9 @@ class Settings:
|
||||
self.settings.setValue('windowPosition', self.parent_window.pos())
|
||||
self.settings.endGroup()
|
||||
|
||||
self.settings.beginGroup('lastOpen')
|
||||
self.settings.setValue('path', self.parent_window.last_open_path)
|
||||
self.settings.beginGroup('runtimeVariables')
|
||||
self.settings.setValue('lastOpenPath', self.parent_window.last_open_path)
|
||||
self.settings.setValue('databasePath', self.parent_window.database_path)
|
||||
self.settings.endGroup()
|
||||
|
||||
|
||||
@@ -207,7 +248,7 @@ class Toolbars:
|
||||
|
||||
addButton.triggered.connect(self.parent_window.open_file)
|
||||
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(deleteButton)
|
||||
@@ -238,15 +279,6 @@ class Tabs:
|
||||
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():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName('Lector') # This is needed for QStandardPaths
|
||||
|
@@ -2,16 +2,22 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import collections
|
||||
import hashlib
|
||||
from multiprocessing.dummy import Pool
|
||||
|
||||
import ebooklib.epub
|
||||
|
||||
|
||||
class ParseEPUB:
|
||||
def __init__(self, filename):
|
||||
# TODO
|
||||
# Maybe also include book description
|
||||
self.filename = filename
|
||||
self.book_title = None
|
||||
self.book = None
|
||||
|
||||
def read_epub(self):
|
||||
try:
|
||||
self.book = ebooklib.epub.read_epub(filename)
|
||||
self.book = ebooklib.epub.read_epub(self.filename)
|
||||
except (KeyError, AttributeError):
|
||||
print('Cannot parse ' + self.filename)
|
||||
return
|
||||
@@ -20,6 +26,11 @@ class ParseEPUB:
|
||||
return self.book.title.strip()
|
||||
|
||||
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
|
||||
# This seems hack-ish, but that's never stopped me before
|
||||
image_path = None
|
||||
@@ -59,7 +70,6 @@ class ParseEPUB:
|
||||
return image_content
|
||||
|
||||
except KeyError:
|
||||
print('Cannot parse ' + self.filename)
|
||||
return
|
||||
|
||||
def get_isbn(self):
|
||||
@@ -83,13 +93,39 @@ class BookSorter:
|
||||
# Parsing for the reader proper
|
||||
# Caching upon closing
|
||||
self.file_list = file_list
|
||||
self.all_books = {}
|
||||
|
||||
def add_to_database(self):
|
||||
# Consider multithreading this
|
||||
for i in self.file_list:
|
||||
book_ref = ParseEPUB(i)
|
||||
def read_book(self, filename):
|
||||
# filename is expected as a string containg the
|
||||
# full path of the ebook file
|
||||
|
||||
# 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()
|
||||
cover_image = book_ref.get_cover_image()
|
||||
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 os
|
||||
|
||||
|
||||
class DatabaseFunctions:
|
||||
class DatabaseInit:
|
||||
def __init__(self, location_prefix):
|
||||
os.makedirs(location_prefix, exist_ok=True)
|
||||
self.database_path = os.path.join(
|
||||
location_prefix, 'Lector.db')
|
||||
database_path = os.path.join(location_prefix, 'Lector.db')
|
||||
|
||||
self.database = sqlite3.connect(self.database_path)
|
||||
if not os.path.exists(self.database_path):
|
||||
if not os.path.exists(database_path):
|
||||
self.database = sqlite3.connect(database_path)
|
||||
self.create_database()
|
||||
else:
|
||||
self.database = sqlite3.connect(database_path)
|
||||
|
||||
def create_database(self):
|
||||
self.database.execute(
|
||||
"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(
|
||||
"CREATE TABLE cache \
|
||||
(id INTEGER PRIMARY KEY, Name TEXT, Path TEXT, CachedDict BLOB)")
|
||||
@@ -26,5 +27,91 @@ class DatabaseFunctions:
|
||||
|
||||
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