Initial file loading
epub content parsing is horribly borked
This commit is contained in:
92
__main__.py
92
__main__.py
@@ -5,6 +5,7 @@
|
|||||||
Check files (hashes) upon restart
|
Check files (hashes) upon restart
|
||||||
Recursive file addition
|
Recursive file addition
|
||||||
Show what on startup
|
Show what on startup
|
||||||
|
If cache large files
|
||||||
Library:
|
Library:
|
||||||
✓ sqlite3 for cover images cache
|
✓ sqlite3 for cover images cache
|
||||||
✓ sqlite3 for storing metadata
|
✓ sqlite3 for storing metadata
|
||||||
@@ -13,8 +14,9 @@
|
|||||||
✓ Image reflow
|
✓ Image reflow
|
||||||
✓ Search bar in toolbar
|
✓ Search bar in toolbar
|
||||||
✓ Shift focus to the tab that has the book open
|
✓ 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
|
Ignore a / the / numbers for sorting purposes
|
||||||
Maybe create emblem per filetype
|
|
||||||
Put the path in the scope of the search
|
Put the path in the scope of the search
|
||||||
maybe as a type: switch
|
maybe as a type: switch
|
||||||
Mass tagging
|
Mass tagging
|
||||||
@@ -26,17 +28,18 @@
|
|||||||
✓ Override the keypress event of the textedit
|
✓ Override the keypress event of the textedit
|
||||||
✓ Use format* icons for toolbar buttons
|
✓ Use format* icons for toolbar buttons
|
||||||
✓ Implement book view settings with a(nother) toolbar
|
✓ 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
|
All ebooks should first be added to the database and then returned as HTML
|
||||||
Pagination
|
Pagination
|
||||||
Theming
|
Theming
|
||||||
Set context menu for definitions and the like
|
Set context menu for definitions and the like
|
||||||
Keep fontsize and margins consistent - Let page increase in length
|
Keep fontsize and margins consistent - Let page increase in length
|
||||||
Filetypes:
|
Filetypes:
|
||||||
|
? Plugin system for parsers
|
||||||
|
? pdf support
|
||||||
epub support
|
epub support
|
||||||
mobi, azw support
|
mobi, azw support
|
||||||
txt, doc, djvu support
|
txt, doc, djvu support
|
||||||
pdf support?
|
|
||||||
cbz, cbr support
|
cbz, cbr support
|
||||||
Keep font settings enabled but only for background color
|
Keep font settings enabled but only for background color
|
||||||
Internet:
|
Internet:
|
||||||
@@ -44,7 +47,7 @@
|
|||||||
Get ISBN using python-isbnlib
|
Get ISBN using python-isbnlib
|
||||||
Other:
|
Other:
|
||||||
✓ Define every widget in code
|
✓ Define every widget in code
|
||||||
Maybe include icons for emblems
|
? Include icons for emblems
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -91,6 +94,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
|
|
||||||
self.bookToolBar = BookToolBar(self)
|
self.bookToolBar = BookToolBar(self)
|
||||||
self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)
|
self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)
|
||||||
|
self.bookToolBar.tocBox.activated.connect(self.set_toc_position)
|
||||||
self.addToolBar(self.bookToolBar)
|
self.addToolBar(self.bookToolBar)
|
||||||
|
|
||||||
# Make the correct toolbar visible
|
# Make the correct toolbar visible
|
||||||
@@ -99,10 +103,12 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
|
|
||||||
# New tabs and their contents
|
# New tabs and their contents
|
||||||
self.current_tab = None
|
self.current_tab = None
|
||||||
self.current_textEdit = None
|
self.current_contentView = None
|
||||||
|
|
||||||
# Tab closing
|
# Tab closing
|
||||||
self.tabWidget.setTabsClosable(True)
|
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.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, None)
|
||||||
self.tabWidget.tabCloseRequested.connect(self.close_tab)
|
self.tabWidget.tabCloseRequested.connect(self.close_tab)
|
||||||
|
|
||||||
@@ -162,7 +168,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
if my_file[0]:
|
if my_file[0]:
|
||||||
self.listView.setEnabled(False)
|
self.listView.setEnabled(False)
|
||||||
self.last_open_path = os.path.dirname(my_file[0][0])
|
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()
|
parsed_books = books.initiate_threads()
|
||||||
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
|
database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
|
||||||
self.listView.setEnabled(True)
|
self.listView.setEnabled(True)
|
||||||
@@ -176,11 +182,14 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
if box_button.text() == '&Yes':
|
if box_button.text() == '&Yes':
|
||||||
selected_hashes = []
|
selected_hashes = []
|
||||||
for i in selected_books:
|
for i in selected_books:
|
||||||
book_data = i.data(QtCore.Qt.UserRole + 3)
|
data = i.data(QtCore.Qt.UserRole + 3)
|
||||||
selected_hashes.append(book_data['book_hash'])
|
selected_hashes.append(data['hash'])
|
||||||
database.DatabaseFunctions(
|
database.DatabaseFunctions(
|
||||||
self.database_path).delete_from_database(selected_hashes)
|
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()
|
self.reload_listview()
|
||||||
|
|
||||||
selected_number = len(selected_books)
|
selected_number = len(selected_books)
|
||||||
@@ -205,8 +214,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
|
|
||||||
def tab_switch(self):
|
def tab_switch(self):
|
||||||
if self.tabWidget.currentIndex() == 0:
|
if self.tabWidget.currentIndex() == 0:
|
||||||
|
|
||||||
self.bookToolBar.hide()
|
self.bookToolBar.hide()
|
||||||
self.libraryToolBar.show()
|
self.libraryToolBar.show()
|
||||||
|
|
||||||
if self.lib_ref.proxy_model:
|
if self.lib_ref.proxy_model:
|
||||||
# Making the proxy model available doesn't affect
|
# Making the proxy model available doesn't affect
|
||||||
# memory utilization at all. Bleh.
|
# memory utilization at all. Bleh.
|
||||||
@@ -215,51 +226,76 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
else:
|
else:
|
||||||
self.bookToolBar.show()
|
self.bookToolBar.show()
|
||||||
self.libraryToolBar.hide()
|
self.libraryToolBar.hide()
|
||||||
|
|
||||||
current_metadata = self.tabWidget.widget(
|
current_metadata = self.tabWidget.widget(
|
||||||
self.tabWidget.currentIndex()).book_metadata
|
self.tabWidget.currentIndex()).metadata
|
||||||
current_title = current_metadata['book_title']
|
|
||||||
current_author = current_metadata['book_author']
|
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(
|
self.statusMessage.setText(
|
||||||
current_author + ' - ' + current_title)
|
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):
|
def set_fullscreen(self):
|
||||||
self.current_tab = self.tabWidget.currentIndex()
|
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(
|
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.exit_shortcut.activated.connect(self.set_normalsize)
|
||||||
|
|
||||||
self.current_textEdit.setWindowFlags(QtCore.Qt.Window)
|
self.current_contentView.setWindowFlags(QtCore.Qt.Window)
|
||||||
self.current_textEdit.setWindowState(QtCore.Qt.WindowFullScreen)
|
self.current_contentView.setWindowState(QtCore.Qt.WindowFullScreen)
|
||||||
self.hide()
|
self.hide()
|
||||||
self.current_textEdit.show()
|
self.current_contentView.show()
|
||||||
|
|
||||||
def set_normalsize(self):
|
def set_normalsize(self):
|
||||||
self.current_textEdit.setWindowState(QtCore.Qt.WindowNoState)
|
self.current_contentView.setWindowState(QtCore.Qt.WindowNoState)
|
||||||
self.current_textEdit.setWindowFlags(QtCore.Qt.Widget)
|
self.current_contentView.setWindowFlags(QtCore.Qt.Widget)
|
||||||
self.show()
|
self.show()
|
||||||
self.current_textEdit.show()
|
self.current_contentView.show()
|
||||||
|
|
||||||
def list_doubleclick(self, myindex):
|
def list_doubleclick(self, myindex):
|
||||||
# TODO
|
|
||||||
# Load the book.
|
|
||||||
index = self.listView.model().index(myindex.row(), 0)
|
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)
|
# Shift focus to the tab that has the book open (if there is one)
|
||||||
for i in range(1, self.tabWidget.count()):
|
for i in range(1, self.tabWidget.count()):
|
||||||
tab_book_metadata = self.tabWidget.widget(i).book_metadata
|
tab_metadata = self.tabWidget.widget(i).metadata
|
||||||
if tab_book_metadata['book_hash'] == book_metadata['book_hash']:
|
if tab_metadata['hash'] == metadata['hash']:
|
||||||
self.tabWidget.setCurrentIndex(i)
|
self.tabWidget.setCurrentIndex(i)
|
||||||
return
|
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)
|
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):
|
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)
|
self.tabWidget.removeTab(tab_index)
|
||||||
|
|
||||||
def closeEvent(self, event=None):
|
def closeEvent(self, event=None):
|
||||||
|
40
database.py
40
database.py
@@ -3,6 +3,7 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class DatabaseInit:
|
class DatabaseInit:
|
||||||
def __init__(self, location_prefix):
|
def __init__(self, location_prefix):
|
||||||
os.makedirs(location_prefix, exist_ok=True)
|
os.makedirs(location_prefix, exist_ok=True)
|
||||||
@@ -16,7 +17,7 @@ class DatabaseInit:
|
|||||||
self.database.execute(
|
self.database.execute(
|
||||||
"CREATE TABLE books \
|
"CREATE TABLE books \
|
||||||
(id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, \
|
(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(
|
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,28 +27,29 @@ class DatabaseInit:
|
|||||||
self.database.commit()
|
self.database.commit()
|
||||||
self.database.close()
|
self.database.close()
|
||||||
|
|
||||||
|
|
||||||
class DatabaseFunctions:
|
class DatabaseFunctions:
|
||||||
def __init__(self, location_prefix):
|
def __init__(self, location_prefix):
|
||||||
database_path = os.path.join(location_prefix, 'Lector.db')
|
database_path = os.path.join(location_prefix, 'Lector.db')
|
||||||
self.database = sqlite3.connect(database_path)
|
self.database = sqlite3.connect(database_path)
|
||||||
|
|
||||||
def add_to_database(self, book_data):
|
def add_to_database(self, data):
|
||||||
# book_data is expected to be a dictionary
|
# data is expected to be a dictionary
|
||||||
# with keys corresponding to the book hash
|
# with keys corresponding to the book hash
|
||||||
# and corresponding items containing
|
# and corresponding items containing
|
||||||
# whatever else needs insertion
|
# whatever else needs insertion
|
||||||
# Haha I said insertion
|
# Haha I said insertion
|
||||||
|
|
||||||
for i in book_data.items():
|
for i in data.items():
|
||||||
book_hash = i[0]
|
book_hash = i[0]
|
||||||
book_title = i[1]['title']
|
title = i[1]['title']
|
||||||
book_author = i[1]['author']
|
author = i[1]['author']
|
||||||
book_year = i[1]['year']
|
year = i[1]['year']
|
||||||
if not book_year:
|
if not year:
|
||||||
book_year = 9999
|
year = 9999
|
||||||
book_path = i[1]['path']
|
path = i[1]['path']
|
||||||
book_cover = i[1]['cover_image']
|
cover = i[1]['cover_image']
|
||||||
book_isbn = i[1]['isbn']
|
isbn = i[1]['isbn']
|
||||||
|
|
||||||
sql_command_add = (
|
sql_command_add = (
|
||||||
"INSERT INTO books (Title,Author,Year,Path,ISBN,Hash,CoverImage) VALUES(?, ?, ?, ?, ?, ?, ?)")
|
"INSERT INTO books (Title,Author,Year,Path,ISBN,Hash,CoverImage) VALUES(?, ?, ?, ?, ?, ?, ?)")
|
||||||
@@ -55,11 +57,11 @@ class DatabaseFunctions:
|
|||||||
# TODO
|
# TODO
|
||||||
# This is a placeholder. You will need to generate book covers
|
# This is a placeholder. You will need to generate book covers
|
||||||
# in case none are found
|
# in case none are found
|
||||||
if book_cover:
|
if cover:
|
||||||
self.database.execute(
|
self.database.execute(
|
||||||
sql_command_add,
|
sql_command_add,
|
||||||
[book_title, book_author, book_year,
|
[title, author, year,
|
||||||
book_path, book_isbn, book_hash, sqlite3.Binary(book_cover)])
|
path, isbn, book_hash, sqlite3.Binary(cover)])
|
||||||
|
|
||||||
self.database.commit()
|
self.database.commit()
|
||||||
|
|
||||||
@@ -95,15 +97,15 @@ class DatabaseFunctions:
|
|||||||
sql_command_fetch = sql_command_fetch[:-3] # Truncate the last OR
|
sql_command_fetch = sql_command_fetch[:-3] # Truncate the last OR
|
||||||
|
|
||||||
# book data is returned as a list of tuples
|
# 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
|
# Because this is the result of a fetchall(), we need an
|
||||||
# ugly hack (tm) to get correct results
|
# ugly hack (tm) to get correct results
|
||||||
if fetch_one:
|
if fetch_one:
|
||||||
return book_data[0][0]
|
return data[0][0]
|
||||||
|
|
||||||
return book_data
|
return data
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import collections
|
||||||
|
|
||||||
import ebooklib.epub
|
import ebooklib.epub
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ class ParseEPUB:
|
|||||||
def read_book(self):
|
def read_book(self):
|
||||||
try:
|
try:
|
||||||
self.book = ebooklib.epub.read_epub(self.filename)
|
self.book = ebooklib.epub.read_epub(self.filename)
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError, FileNotFoundError):
|
||||||
print('Cannot parse ' + self.filename)
|
print('Cannot parse ' + self.filename)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -100,3 +101,40 @@ class ParseEPUB:
|
|||||||
return isbn
|
return isbn
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
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
|
||||||
|
72
sorter.py
72
sorter.py
@@ -1,16 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# TODO
|
# 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
|
import hashlib
|
||||||
from multiprocessing.dummy import Pool
|
from multiprocessing.dummy import Pool
|
||||||
|
|
||||||
import database
|
import database
|
||||||
from parsers.epub import ParseEPUB
|
from parsers.epub import ParseEPUB
|
||||||
|
|
||||||
|
|
||||||
class BookSorter:
|
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
|
# Have the GUI pass a list of files straight to here
|
||||||
# Then, on the basis of what is needed, pass the
|
# Then, on the basis of what is needed, pass the
|
||||||
# filenames to the requisite functions
|
# filenames to the requisite functions
|
||||||
@@ -21,7 +25,9 @@ class BookSorter:
|
|||||||
self.all_books = {}
|
self.all_books = {}
|
||||||
self.database_path = database_path
|
self.database_path = database_path
|
||||||
self.hashes = []
|
self.hashes = []
|
||||||
self.database_hashes()
|
self.mode = mode
|
||||||
|
if database_path:
|
||||||
|
self.database_hashes()
|
||||||
|
|
||||||
def database_hashes(self):
|
def database_hashes(self):
|
||||||
all_hashes = database.DatabaseFunctions(
|
all_hashes = database.DatabaseFunctions(
|
||||||
@@ -34,6 +40,16 @@ class BookSorter:
|
|||||||
if all_hashes:
|
if all_hashes:
|
||||||
self.hashes = [i[0] for i in 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):
|
def read_book(self, filename):
|
||||||
# filename is expected as a string containg the
|
# filename is expected as a string containg the
|
||||||
# full path of the ebook file
|
# full path of the ebook file
|
||||||
@@ -41,17 +57,21 @@ class BookSorter:
|
|||||||
with open(filename, 'rb') as current_book:
|
with open(filename, 'rb') as current_book:
|
||||||
file_md5 = hashlib.md5(current_book.read()).hexdigest()
|
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
|
# Do not allow addition in case the file is dupicated in the directory
|
||||||
# OR is already in the database
|
# OR is already in the database
|
||||||
# TODO
|
# This should not get triggered in reading mode
|
||||||
# See if you want to include a hash of the book's name and author
|
if (self.mode == 'addition'
|
||||||
if file_md5 in self.all_books.items() or file_md5 in self.hashes:
|
and (file_md5 in self.all_books.items() or file_md5 in self.hashes)):
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO
|
# Select sorter by file extension
|
||||||
# See if tags can be generated from book content
|
try:
|
||||||
# Sort according to to file extension here
|
file_extension = os.path.splitext(filename)[1][1:]
|
||||||
book_ref = ParseEPUB(filename)
|
if file_extension == 'epub':
|
||||||
|
book_ref = ParseEPUB(filename)
|
||||||
|
except IndexError:
|
||||||
|
return
|
||||||
|
|
||||||
# Everything following this is standard
|
# Everything following this is standard
|
||||||
# Some of the None returns will have to have
|
# Some of the None returns will have to have
|
||||||
@@ -61,16 +81,32 @@ class BookSorter:
|
|||||||
title = book_ref.get_title()
|
title = book_ref.get_title()
|
||||||
author = book_ref.get_author()
|
author = book_ref.get_author()
|
||||||
year = book_ref.get_year()
|
year = book_ref.get_year()
|
||||||
cover_image = book_ref.get_cover_image()
|
|
||||||
isbn = book_ref.get_isbn()
|
isbn = book_ref.get_isbn()
|
||||||
|
|
||||||
self.all_books[file_md5] = {
|
# Different modes require different values
|
||||||
'title': title,
|
if self.mode == 'addition':
|
||||||
'author': author,
|
cover_image = book_ref.get_cover_image()
|
||||||
'year': year,
|
self.all_books[file_md5] = {
|
||||||
'isbn': isbn,
|
'title': title,
|
||||||
'path': filename,
|
'author': author,
|
||||||
'cover_image': cover_image}
|
'year': year,
|
||||||
|
'isbn': isbn,
|
||||||
|
'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):
|
def initiate_threads(self):
|
||||||
_pool = Pool(5)
|
_pool = Pool(5)
|
||||||
|
@@ -31,51 +31,52 @@ class Library:
|
|||||||
for i in books:
|
for i in books:
|
||||||
# The database query returns a tuple with the following indices
|
# The database query returns a tuple with the following indices
|
||||||
# Index 0 is the key ID is ignored
|
# Index 0 is the key ID is ignored
|
||||||
book_title = i[1]
|
title = i[1]
|
||||||
book_author = i[2]
|
author = i[2]
|
||||||
book_year = i[3]
|
year = i[3]
|
||||||
book_cover = i[8]
|
path = i[4]
|
||||||
book_tags = i[6]
|
tags = i[6]
|
||||||
book_path = i[4]
|
cover = i[9]
|
||||||
book_progress = None # TODO
|
progress = None # TODO
|
||||||
# Leave at None for an untouched book
|
# Leave at None for an untouched book
|
||||||
# 'completed' for a completed book
|
# 'completed' for a completed book
|
||||||
# whatever else is here can be used
|
# whatever else is here can be used
|
||||||
# to remember position
|
# to remember position
|
||||||
|
# Maybe get from the position param
|
||||||
|
|
||||||
all_metadata = {
|
all_metadata = {
|
||||||
'book_title': i[1],
|
'title': i[1],
|
||||||
'book_author': i[2],
|
'author': i[2],
|
||||||
'book_year': i[3],
|
'year': i[3],
|
||||||
'book_path': i[4],
|
'path': i[4],
|
||||||
'book_isbn': i[5],
|
'position': i[5],
|
||||||
'book_tags': i[6],
|
'isbn': i[6],
|
||||||
'book_hash': i[7]}
|
'tags': i[7],
|
||||||
|
'hash': i[8]}
|
||||||
|
|
||||||
tooltip_string = book_title + '\nAuthor: ' + book_author + '\nYear: ' + str(book_year)
|
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year)
|
||||||
if book_tags:
|
if tags:
|
||||||
tooltip_string += ('\nTags: ' + book_tags)
|
tooltip_string += ('\nTags: ' + tags)
|
||||||
|
|
||||||
# This remarkably ugly hack is because the QSortFilterProxyModel
|
# This remarkably ugly hack is because the QSortFilterProxyModel
|
||||||
# doesn't easily allow searching through multiple item roles
|
# doesn't easily allow searching through multiple item roles
|
||||||
search_workaround = book_title + ' ' + book_author
|
search_workaround = title + ' ' + author
|
||||||
if book_tags:
|
if tags:
|
||||||
search_workaround += book_tags
|
search_workaround += tags
|
||||||
|
|
||||||
# Generate book state for passing onto the QStyledItemDelegate
|
# Generate book state for passing onto the QStyledItemDelegate
|
||||||
def generate_book_state(book_path, book_progress):
|
def generate_book_state(path, progress):
|
||||||
if not os.path.exists(book_path):
|
if not os.path.exists(path):
|
||||||
return 'deleted'
|
return 'deleted'
|
||||||
|
|
||||||
if book_progress:
|
if progress:
|
||||||
if book_progress == 'completed':
|
if progress == 'completed':
|
||||||
return 'completed'
|
return 'completed'
|
||||||
else:
|
else:
|
||||||
return 'inprogress'
|
return 'inprogress'
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
state = generate_book_state(path, progress)
|
||||||
book_state = generate_book_state(book_path, book_progress)
|
|
||||||
|
|
||||||
# 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
|
||||||
@@ -84,17 +85,17 @@ class Library:
|
|||||||
# 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
|
||||||
img_pixmap = QtGui.QPixmap()
|
img_pixmap = QtGui.QPixmap()
|
||||||
img_pixmap.loadFromData(book_cover)
|
img_pixmap.loadFromData(cover)
|
||||||
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
|
img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
|
||||||
item = QtGui.QStandardItem()
|
item = QtGui.QStandardItem()
|
||||||
item.setToolTip(tooltip_string)
|
item.setToolTip(tooltip_string)
|
||||||
# The following order is needed to keep sorting working
|
# The following order is needed to keep sorting working
|
||||||
item.setData(book_title, QtCore.Qt.UserRole)
|
item.setData(title, QtCore.Qt.UserRole)
|
||||||
item.setData(book_author, QtCore.Qt.UserRole + 1)
|
item.setData(author, QtCore.Qt.UserRole + 1)
|
||||||
item.setData(book_year, QtCore.Qt.UserRole + 2)
|
item.setData(year, QtCore.Qt.UserRole + 2)
|
||||||
item.setData(all_metadata, QtCore.Qt.UserRole + 3)
|
item.setData(all_metadata, QtCore.Qt.UserRole + 3)
|
||||||
item.setData(search_workaround, QtCore.Qt.UserRole + 4)
|
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))
|
item.setIcon(QtGui.QIcon(img_pixmap))
|
||||||
self.parent_window.viewModel.appendRow(item)
|
self.parent_window.viewModel.appendRow(item)
|
||||||
|
|
||||||
@@ -108,7 +109,8 @@ class Library:
|
|||||||
def update_proxymodel(self):
|
def update_proxymodel(self):
|
||||||
self.proxy_model.setFilterRole(QtCore.Qt.UserRole + 4)
|
self.proxy_model.setFilterRole(QtCore.Qt.UserRole + 4)
|
||||||
self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
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(
|
self.parent_window.statusMessage.setText(
|
||||||
str(self.proxy_model.rowCount()) + ' books')
|
str(self.proxy_model.rowCount()) + ' books')
|
||||||
|
38
widgets.py
38
widgets.py
@@ -97,9 +97,7 @@ class BookToolBar(QtWidgets.QToolBar):
|
|||||||
self.searchBar.setObjectName('searchBar')
|
self.searchBar.setObjectName('searchBar')
|
||||||
|
|
||||||
# Sorter
|
# Sorter
|
||||||
sorting_choices = ['Chapter ' + str(i) for i in range(1, 11)]
|
|
||||||
self.tocBox = QtWidgets.QComboBox()
|
self.tocBox = QtWidgets.QComboBox()
|
||||||
self.tocBox.addItems(sorting_choices)
|
|
||||||
self.tocBox.setObjectName('sortingBox')
|
self.tocBox.setObjectName('sortingBox')
|
||||||
self.tocBox.setSizePolicy(sizePolicy)
|
self.tocBox.setSizePolicy(sizePolicy)
|
||||||
self.tocBox.setMinimumContentsLength(10)
|
self.tocBox.setMinimumContentsLength(10)
|
||||||
@@ -216,27 +214,31 @@ class LibraryToolBar(QtWidgets.QToolBar):
|
|||||||
|
|
||||||
|
|
||||||
class Tab(QtWidgets.QWidget):
|
class Tab(QtWidgets.QWidget):
|
||||||
def __init__(self, book_metadata, parent=None):
|
def __init__(self, metadata, parent=None):
|
||||||
# TODO
|
# TODO
|
||||||
# The display widget will probably have to be shifted to something else
|
|
||||||
# A horizontal slider to control flow
|
# A horizontal slider to control flow
|
||||||
# Keyboard shortcuts
|
# Keyboard shortcuts
|
||||||
|
|
||||||
|
# The content display widget is currently a QTextBrowser
|
||||||
super(Tab, self).__init__(parent)
|
super(Tab, self).__init__(parent)
|
||||||
self.parent = 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']
|
title = self.metadata['title']
|
||||||
book_path = self.book_metadata['book_path']
|
path = self.metadata['path']
|
||||||
|
|
||||||
self.gridLayout = QtWidgets.QGridLayout(self)
|
self.gridLayout = QtWidgets.QGridLayout(self)
|
||||||
self.gridLayout.setObjectName("gridLayout")
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
self.textEdit = QtWidgets.QTextEdit(self)
|
self.contentView = QtWidgets.QTextBrowser(self)
|
||||||
self.textEdit.setObjectName("textEdit")
|
self.contentView.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||||
self.textEdit.setFrameShape(QtWidgets.QFrame.NoFrame)
|
self.contentView.setObjectName("contentView")
|
||||||
self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1)
|
self.contentView.verticalScrollBar().setSingleStep(7)
|
||||||
self.parent.addTab(self, book_title)
|
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
self.textEdit.setText(book_path)
|
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):
|
class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
@@ -246,13 +248,13 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
|
|||||||
def paint(self, painter, option, index):
|
def paint(self, painter, option, index):
|
||||||
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
|
||||||
option = option.__class__(option)
|
option = option.__class__(option)
|
||||||
book_state = index.data(QtCore.Qt.UserRole + 5)
|
state = index.data(QtCore.Qt.UserRole + 5)
|
||||||
if book_state:
|
if state:
|
||||||
if book_state == 'deleted':
|
if state == 'deleted':
|
||||||
read_icon = QtGui.QIcon.fromTheme('vcs-conflicting').pixmap(36)
|
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)
|
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)
|
read_icon = QtGui.QIcon.fromTheme('vcs-locally-modified').pixmap(36)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
Reference in New Issue
Block a user