#########################################################
## @file : kmanapp.py
## @desc : kindle note managerment tool GUI
## @create : 2020/06/03
## @author : Chengan
## @email : douboer@gmail.com
#########################################################
import sys
import os
from time import sleep
import pandas as pd
import threading
from PySide2.QtWidgets import *
from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject,
QAbstractTableModel, QObject, QPoint, QRect, QSize, QTime,
QUrl, Qt, QThread, Signal, QTimer)
from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont,
QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter,
QPixmap, QRadialGradient, QStandardItem, QStandardItemModel)
from mainwindow import Ui_MainWindow
from kman import *
from parseweb import *
# import binary resource file(kmanapp_rc.py)
import kmanapp_rc
notes_temp = """
《{bookname}》
{author}
({time})
【{note}】
{content}
【P{position}】
"""
infos_temp = """
| |
{bookname}
作者 : {author}
评论数 : {ratenum}
评分 : {score}
出版社 : {publisher}
出版时间 : {publishing}
|
|
|
|
"""
words_temp = """
{usage}
{bookname}
{author}
{category}
{timestamp}
{position}
"""
ONLY_TEST = 1
class kmanWindow(QMainWindow):
"""
def __init__(self, *args, **kwargs):
super(kmanWindow, self).__init__(*args, **kwargs)
"""
flag = True
def __init__(self, parent=None):
super(kmanWindow, self).__init__(parent)
# create ui and initial it
ui = Ui_MainWindow()
ui.setupUi(self)
self.ui = ui
self.add_ui_component()
# initial tree selected
self.tree_selected = 'note_root'
self.km = kMan()
self.spide = bookInfoSpide()
# initial check order:
# 1. backup file bk.data ->
# 2. kindle(My Clippings.txt) ->
# 3. local file(config) ->
flg = 0
if os.path.exists(BACKUPNOTEFN) and os.path.exists(BACKUPWORDFN):
self.books_data = self.km.json2dict(BACKUPNOTEFN)
self.words_data = self.km.json2dict(BACKUPWORDFN)
if (len(self.books_data)*len(self.words_data[0]))>=1:
self.books_data = self.km.json2dict(BACKUPNOTEFN)
self.words_data = self.km.json2dict(BACKUPWORDFN)
flg = 1
if self.km.get_kindle_path() and (not flg):
self.import_kindle()
else:
self.books_data = self.km.import_clips()
self.words_data = self.km.import_words()
[self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data)
#self.filter_list = self.km.filter_words(self.words_data)
# initial books information which grab from douban or amazon
# if the information exist in backup file, initial with this file,
# and grap new book's information from douban
# else grap all book information from douban
self.books_info = defaultdict(dict)
try:
if os.path.exists(BACKUPINFOFN):
self.books_info = self.km.json2dict(BACKUPINFOFN)
increase_book_list = self.check_increase_books(self.books_data, self.books_info)
if len(increase_book_list) > 0:
trd = threading.Thread(target=self.grab_books_info, args=(increase_book_list,))
trd.setDaemon(True)
trd.start()
else:
booklist = list(self.books_data.keys())
trd = threading.Thread(target=self.grab_books_info, args=(book_list,))
trd.setDaemon(True)
trd.start()
except Exception as e:
print(e)
finally:
pass
self.fill_treeview()
self.refresh_ui_component(comp=1)
# timer to check status of kindle
self.timer = QTimer(self)
self.timer.timeout.connect(self.show_status_info)
self.timer.start(1000)
# connect action/toolbutton to slot functions
ui.actionimportkindle.triggered.connect(lambda: self.import_kindle())
ui.actionimportlocal.triggered.connect(lambda: self.import_local())
ui.actionconfig.triggered.connect(lambda: self.config())
ui.actionwords.triggered.connect(lambda: self.words())
ui.actionstatistic.triggered.connect(lambda: self.statistic())
ui.actionhomepage.triggered.connect(lambda: self.homepage())
ui.actionabout.triggered.connect(lambda: self.about())
ui.actionflush.triggered.connect(lambda: self.refresh())
ui.actionexport.triggered.connect(lambda: self.export())
#ui.searchLineEdit.returnPressed.connect(self.search_return_press())
ui.searchComboBox.currentIndexChanged.connect(self.search_scope_change)
ui.searchToolButton.clicked.connect(self.search_button_clicked)
ui.treeView.clicked.connect(self.tree_item_clicked)
ui.tableView.clicked.connect(self.table_item_clicked)
ui.tableView.horizontalHeader().setStretchLastSection(True)
#ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
ui.tableView.verticalHeader().hide()
ui.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
ui.tableView.setColumnWidth(0, 40) # type
ui.tableView.setColumnWidth(2, 50) # author
ui.tableView.selectRow(0)
# initial tableView
data = self.convert_to_panda(self.filter_list)
ui.tablemodel = nTableModel(data)
ui.tableView.verticalHeader().hide()
ui.tableView.setModel(self.ui.tablemodel)
def check_increase_books(self, bks, bksinfo):
new_list = list(bks.keys()) # kindle's books with note
new_list = [re.split(r'[\((\-\::_\s]',nn.strip())[0] for nn in new_list]
last_list = list(bksinfo.keys()) # grab book information last time
increase_list = []
for nn in new_list:
flag = 0
for gg in last_list:
if nn in gg: flag = 1
if flag == 0:
increase_list.append(nn)
return increase_list
def add_ui_component(self):
self.ui.searchComboBox.addItems([u'ALL',u'BOOKNAME',u'CONTENT',u'AUTHOR'])
self.ui.treeView.resize(200,200)
# status bar
self.ui.status_label = QLabel()
self.ui.pe = QPalette()
def refresh_ui_component(self, comp=0):
""" refresh treeView, tableview, textedit information
after import or open clips file
Args: comp 0 - treeview + tablevew + textedit , initial, fill_treeview()
move to __init__()
1 - tablevew + textedit , note tree view clicked
2 - textedit , note table view clicked
3 - tablevew + textedit , word tree view clicked
4 - textedit , word table view clicked
5 - textedit , info tree view clicked
"""
# refresh tableview click note tree
if comp in [1,2]:
data = self.convert_to_panda(self.filter_list,0)
# refresh tableview click word tree
elif comp in [3,4]:
data = self.convert_to_panda(self.filter_list,1)
# refresh tableview content
if hasattr(self.ui, 'tablemodel'):
del self.ui.tablemodel
self.ui.tablemodel = nTableModel(data)
self.ui.tableView.verticalHeader().hide()
self.ui.tableView.setModel(self.ui.tablemodel)
#self.ui.tablemodel.tabledata_update.connect(self.tabledata_update_slot)
# refresh textedit content
if comp in [1,2]:
if len(self.filter_list)>0:
[stype,sbookname,sauthor,sposition,stime,scontent] = self.filter_list[0]
tt = notes_temp.format( bookname=sbookname, author=sauthor, time=stime,
note=stype, content=scontent, position=sposition)
print(re.split(r'[\((\-\::_\s]',sbookname.strip())[0])
self.ui.textEdit.setHtml(tt)
elif comp in [3,4]:
self.render_textedit_words(self.words_data)
elif comp == 5:
self.render_textedit_infos(self.books_info)
self.show_status_info()
def render_textedit_words(self, wdata, mrow=100):
[bookinfo, words, lookups] = wdata
index = self.ui.tableView.currentIndex()
if index.row() == -1:
word = self.filter_list[0][0]
else:
word = index.sibling(index.row(), 0).data()
txt = ""
for row in lookups:
if words[row[1]]['word'] == word:
[susage, stimestamp, sbookname, sauthor, scategory, sposition] = \
[row[4],row[5],bookinfo[row[2]]['bookname'], \
bookinfo[row[2]]['author'], words[row[1]]['category'],row[3]]
txt += words_temp.format(
usage=susage,bookname=sbookname,
author=sauthor,category=scategory,
timestamp=stimestamp,position=sposition)
self.ui.textEdit.setHtml(txt)
'''
{
"金融的本质": {
"link": "https://book.douban.com/subject/25843334",
"bookname": "金融的本质",
"img": "https://img9.doubanio.com/view/subject/s/public/s27246465.jpg",
"score": "8.2",
"ratenum": "893",
"publisher": "中信出版社",
"publishing": "2014",
"author": "【美】伯南克/巴曙松"
}
'''
def render_textedit_infos(self, idata, selectitem):
""" render textedit for book information
Args:
idata - books_info
selectitem - text of treeitem selected
"""
if selectitem in list(idata.keys()):
vv = idata[selectitem]
self.ui.textEdit.setHtml(infos_temp.format(link=vv['link'],bookname=vv['bookname'],
author=vv['author'], ratenum=vv['ratenum'],
score=vv['score'], publisher=vv['publisher'],
publishing=vv['publishing'],img=vv['img'].split('/')[-1]))
else: pass
def convert_to_panda(self, mlist, tp=0):
if tp==0:
pdframe = pd.DataFrame(mlist, \
columns = ['Type','Bookname','Author','Position','Date','Content'])
else:
pdframe = pd.DataFrame(mlist, \
columns = ['Word','Bookname','Author','Category'])
return pdframe
def tabledata_update_slot(self, s):
print('call tabledata_update_slot() {}'.format(s))
def tree_item_clicked(self, modelidx):
model_index_list = self.ui.treeView.selectedIndexes() # QModelIndexList
model_index = model_index_list[0] # QModelIndex
itemmodel = model_index.model() #QAbstractItemModel/QStandardItemModel
item = itemmodel.itemFromIndex(modelidx) #QStandardItem
self.tree_selected = item.accessibleDescription()
print(self.tree_selected, item.text())
# filter clips
self.filter_list = None
info = re.split(r'\s+',item.text())[0]
comp = 0
if self.tree_selected.split('_')[0] == 'info':
self.ui.tableView.setVisible(False)
self.render_textedit_infos(self.books_info, item.text())
comp = 5
else:
self.ui.tableView.setVisible(True)
# refresh filter clips(tableview) after tree item clicked
if self.tree_selected in ['note_root', 'note_bookname', 'note_author']:
[self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data)
comp = 1
elif self.tree_selected == 'note_bleaf': # bookname leaf
comp = 1
[self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data, info, 1)
elif self.tree_selected == 'note_aleaf': # author leaf
comp = 1
[self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data, info, 2)
elif self.tree_selected in ['word_root', 'word_bookname']:
self.filter_list = self.km.filter_words(self.words_data)
comp = 3
elif self.tree_selected == 'word_leaf': # word bookname leaf
comp = 3
self.filter_list = self.km.filter_words(self.words_data, info)
else: return
if comp == 3:
self.ui.tableView.setColumnWidth(1, 50) # author
self.ui.tableView.setColumnWidth(3, 50) # category
# QHeaderView::Interactive 0
# QHeaderView::Stretch 1
# QHeaderView::Fixed 2
# QHeaderView::ResizeToContents 3
self.ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
if comp == 1:
self.ui.tableView.setColumnWidth(0, 40) # type
self.ui.tableView.setColumnWidth(2, 50) # author
self.ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.refresh_ui_component(comp)
def table_item_clicked(self, index):
"""
print('tableView.currentIndex().row() {} tableView.currentIndex().column() {}'
.format(self.ui.tableView.currentIndex().row(),
self.ui.tableView.currentIndex().column()))
"""
if not index.isValid(): return
ss = self.tree_selected.split('_')[0]
if ss=='note':
row = index.row()
stype = index.sibling(row, 0).data()
sbookname = index.sibling(row, 1).data()
sauthor = index.sibling(row, 2).data()
sposition = index.sibling(row, 3).data()
stime = index.sibling(row, 4).data()
scontent = index.sibling(row, 5).data()
self.ui.textEdit.setHtml(notes_temp.format(
bookname=sbookname, author=sauthor, time=stime,
note=stype, content=scontent, position=sposition))
elif ss=='note':
self.render_textedit_words(self.words_data)
def search_button_clicked(self):
search_word = self.ui.searchLineEdit.text()
if search_word.strip() == '':
self.messagebox(ico=2, info=u'\n\n search content is empty!')
return 0
content_type = self.ui.searchComboBox.currentText()
#content_idx = self.ui.searchComboBox.currentIndex()
[nu, sbks] = self.km.search_clip(self.books_data, search_word, 'ALL', content_type)
[self.filter_books, self.filter_list] = self.km.filter_clips(sbks)
self.refresh_ui_component(1)
print( 'call search_button_clicked()' )
def keyPressEvent(self, event):
#print('hasfocus {} key {} {} {}'.format(self.ui.searchLineEdit.hasFocus(),\
# event.key(), Qt.Key_Return, event.key()==Qt.Key_Return))
if (self.ui.searchLineEdit.hasFocus() and event.key() == Qt.Key_Return):
print('call keyPressEvent() {} '.format(event.key()))
self.search_button_clicked()
def search_return_press(self):
self.search_button_clicked()
print('call search_return_press()')
# XXX
def search_scope_change(self,t):
p = {0:'ALL',1:'TITLE',2:'AUTHOR',3:'CONTENT'}
s = self.ui.searchLineEdit.text()
#print(self.books_data)
#print(search_clip(self.books_data,s,'ALL',p[t]))
print('call search_scope_change()')
## XXX
'''
def check_kindle_status(self):
while self.flag:
self.show_status_info()
sleep(2)
'''
def show_status_info(self):
""" show status information on statusbar
Args:
conn: 1 if kindle is connected else 0
Return:
conn
"""
status = self.km.status_info()
self.ui.statusbar.showMessage(status[0],0)
self.ui.status_label.setText(status[1])
if not self.km.status:
self.ui.pe.setColor(QPalette.WindowText,Qt.red)
#self.ui.status_label.setAutoFillBackground(True)
self.ui.status_label.setPalette(pe)
self.ui.statusbar.addPermanentWidget(self.ui.status_label, stretch=0)
# define slot functions
def import_kindle(self):
fp = self.km.get_kindle_path()
if not fp:
self.messagebox(ico=2, info='\n\n kindle is not connected')
return 0
self.books_data = self.km.import_clips(fp+'documents/'+CLIPFN)
self.words_data = self.km.import_words(fp+'system/vocabulary/'+WORDFN)
[self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data)
self.filter_wordlist = self.km.filter_words(self.words_data)
self.fill_treeview()
self.refresh_ui_component(1)
def import_local(self):
fn, ft = QFileDialog.getOpenFileName(self,
"choose file to import",
'./', # start path
"All Files (*);;Text Files (*.txt)") # filter file type
self.fn = fn
self.books_data = self.km.import_clips(fn)
[self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data)
self.fill_treeview()
self.refresh_ui_component(1)
#print('filename ', fn, 'filetype ', ft)
if fn == "": return False
def fill_treeview(self):
self.ui.model = QStandardItemModel()
rootItem = self.ui.model.invisibleRootItem()
item = QStandardItem('All Notes ({})'.format(self.km.get_totalnum_nt(self.books_data)))
icon = QIcon()
icon.addFile(u":/icons/emblem_library.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('note_root')
rootItem.appendRow(item)
parent_item = item
# add bookname tree
[numbooks, booknum] = self.km.get_bookname_num(self.books_data)
bookname_item = QStandardItem('Bookname ({})'.format(numbooks))
icon = QIcon()
icon.addFile(u":/icons/book_open.png", QSize(), QIcon.Normal, QIcon.Off)
bookname_item.setIcon(icon)
bookname_item.setAccessibleDescription('note_bookname')
parent_item.appendRow(bookname_item)
if numbooks > 0:
for k, v in booknum.items():
item = QStandardItem('{} ({})'.format(k, v))
icon = QIcon()
icon.addFile(u":/icons/book_open_bookmark.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('note_bleaf')
bookname_item.appendRow(item)
# add author tree
[numauthor, authnum] = self.km.get_author_num(self.books_data)
author_item = QStandardItem('Author ({})'.format(numauthor))
icon = QIcon()
icon.addFile(u":/icons/person.png", QSize(), QIcon.Normal, QIcon.Off)
author_item.setIcon(icon)
author_item.setAccessibleDescription('note_author')
parent_item.appendRow(author_item)
parent_item = author_item
if numauthor > 0:
for k, v in authnum.items():
item = QStandardItem('{} ({})'.format(k, v))
icon = QIcon()
icon.addFile(u":/icons/user.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('note_aleaf')
author_item.appendRow(item)
# add words root
word_rootItem = self.ui.model.invisibleRootItem()
[numwords, wordnum] = self.km.get_book_word_num(self.words_data)
item = QStandardItem('All Words({})'.format(numwords))
icon = QIcon()
icon.addFile(u":/icons/emblem_library.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('word_root')
word_rootItem.appendRow(item)
word_parent_item = item
# add word bookname tree
word_bookname_item = QStandardItem('Bookname ({})'.format(numbooks))
icon = QIcon()
icon.addFile(u":/icons/book_open.png", QSize(), QIcon.Normal, QIcon.Off)
word_bookname_item.setIcon(icon)
word_bookname_item.setAccessibleDescription('word_bookname')
word_parent_item.appendRow(word_bookname_item)
if numwords > 0:
for k, v in wordnum.items():
item = QStandardItem('{} ({})'.format(k, v))
icon = QIcon()
icon.addFile(u":/icons/book_open_bookmark.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('word_leaf')
word_bookname_item.appendRow(item)
# add infos root
self.ui.tableView.setVisible(True)
info_rootItem = self.ui.model.invisibleRootItem()
item = QStandardItem('All Books({})'.format(len(self.books_info.keys())))
icon = QIcon()
icon.addFile(u":/icons/amazon.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('info_root')
info_rootItem.appendRow(item)
info_parent_item = item
# add book info tree
info_bookname_item = QStandardItem('Douban ({})'.format(numbooks))
icon = QIcon()
icon.addFile(u":/icons/book_open.png", QSize(), QIcon.Normal, QIcon.Off)
info_bookname_item.setIcon(icon)
info_bookname_item.setAccessibleDescription('info_bookname')
info_parent_item.appendRow(info_bookname_item)
for k in self.books_info.keys():
item = QStandardItem('{}'.format(k))
icon = QIcon()
icon.addFile(u":/icons/list.png", QSize(), QIcon.Normal, QIcon.Off)
item.setIcon(icon)
item.setAccessibleDescription('info_leaf')
info_bookname_item.appendRow(item)
self.ui.treeView.setModel(self.ui.model)
self.ui.treeView.expandAll()
def config(self):
print("call slot config()")
pass
def words(self):
print("call slot words()")
pass
def statistic(self):
print("call slot statistic()")
pass
def homepage(self):
import webbrowser
webbrowser.open('https://gitee.com/douboer/kman')
print("call slot homepage()")
pass
def about(self):
self.messagebox(ico=1, info='\n'+ \
' kindle management tool \n\n' + \
' v1.0.4\n\n' + \
' Author: chengan\n\n' + \
' douboer@gmail.com')
print("call slot about()")
pass
def refresh(self):
self.import_kindle()
print("call slot refresh()")
pass
def export(self):
if self.tree_selected.split('_')[0]=='note':
self.export_filter_notes()
else:
self.export_filter_words()
print("call export()")
def export_filter_notes(self):
self.km.export_notes(self.filter_books, 'export', ft='MD')
pass
def export_filter_words(self):
self.km.export_words(self.words_data, self.filter_list, 'export', ft='MD')
pass
def messagebox(self, ico=1, info=''):
""" unify messagebox
Args: ico - QMessageBox.NoIcon 0
QMessageBox.Information 1
QMessageBox.Warning 2
QMessageBox.Critical 3
QMessageBox.Question 4
"""
icons = {0:QMessageBox.NoIcon, \
1:QMessageBox.Information, \
2:QMessageBox.Warning, \
3:QMessageBox.Critical, \
4:QMessageBox.Question }
msgBox = QMessageBox()
msgBox.setText(info)
msgBox.setInformativeText("")
msgBox.setIcon(icons[ico])
msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
msgBox.setBaseSize(QSize(600, 300))
r = msgBox.exec()
# backup file when kman closed
# so we can read backup file when kman start
def closeEvent(self, e):
with open(BACKUPNOTEFN, 'w', encoding='utf8', errors='ignore') as fw:
fw.write(self.km.dict2json(self.books_data))
with open(BACKUPWORDFN, 'w', encoding='utf8', errors='ignore') as fw:
fw.write(self.km.dict2json(self.words_data))
with open(BACKUPINFOFN, 'w', encoding='utf8', errors='ignore') as fw:
fw.write(self.km.dict2json(self.books_info))
# stop check thread
self.flag = False
def grab_books_info(self, booklist):
bks_info = {}
for bkname in booklist:
bkname = re.split(r'[\((\-\::_\s]',bkname.strip())[0]
print(bkname)
bkinfo = self.spide.grab_book_info(bkname)
filter_bkinfo = self.spide.filter_spide_book(bkinfo)
bks_info.update(filter_bkinfo)
if filter_bkinfo:
self.spide.down_book_img(filter_bkinfo)
return self.books_info.update(bks_info)
# thanks Martin Fitzpatrick ^_^
# https://www.learnpyqt.com/courses/model-views/qtableview-modelviews-numpy-pandas/
class nTableModel(QAbstractTableModel):
tabledata_update = Signal(str)
def __init__(self, data):
super(nTableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole:
value = self._data.iloc[index.row(), index.column()]
self.tabledata_update[str].emit('{} {}'.format(index.row(),index.column()))
return str(value)
def rowCount(self, index):
return self._data.shape[0]
def columnCount(self, index):
return self._data.shape[1]
def headerData(self, section, orientation, role):
# section is the index of the column/row.
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
if __name__ == "__main__":
app = QApplication(sys.argv)
kmw = kmanWindow()
icon = QIcon()
icon.addFile(u":/icons/Cbb20.png", QSize(), QIcon.Normal, QIcon.Off)
kmw.setWindowIcon(icon)
kmw.setWindowTitle("kindle management")
kmw.resize(1200, 800)
#kmw.showFullScreen()
kmw.show()
"""
# move to __init__()
try:
booklist = self.books_data.keys()
trd = threading.Thread(target=kmw.grab_books_info, args=(booklist,))
trd.setDaemon(True)
trd.start()
except Exception as e:
print(e)
pass
finally:
pass
"""
# loop check kindle is connected or not
# BUG to be implement XXXX
"""
try:
t = threading.Thread(target=kmw.check_kindle_status)
t.start()
except:
print ("Error: can not start thread")
"""
app.exec_()