First version of Reject List Feature (PI only).

This commit is contained in:
Jim Miller 2012-11-17 19:55:12 -06:00
parent 48d0a32b8d
commit 08afa5f38a
4 changed files with 419 additions and 13 deletions

View file

@ -196,7 +196,7 @@ class ImageTitleLayout(QHBoxLayout):
'''
A reusable layout widget displaying an image followed by a title
'''
def __init__(self, parent, icon_name, title):
def __init__(self, parent, icon_name, title, tooltip=None):
QHBoxLayout.__init__(self)
title_image_label = QLabel(parent)
pixmap = get_pixmap(icon_name)
@ -217,6 +217,9 @@ class ImageTitleLayout(QHBoxLayout):
self.addWidget(shelf_label)
self.insertStretch(-1)
if tooltip:
title_image_label.setToolTip(tooltip)
shelf_label.setToolTip(tooltip)
class SizePersistedDialog(QDialog):
'''

View file

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Jim Miller'
__docformat__ = 'restructuredtext en'
import traceback, copy
import traceback, copy, threading
from collections import OrderedDict
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFont, QWidget,
@ -18,7 +18,7 @@ from calibre.utils.config import JSONConfig
from calibre.gui2.ui import get_gui
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order)
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order, RejectListDialog)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getConfigSections
@ -34,6 +34,7 @@ PREFS_KEY_SETTINGS = 'settings'
# take from here.
default_prefs = {}
default_prefs['personal.ini'] = get_resources('plugin-example.ini')
default_prefs['rejecturls'] = ''
default_prefs['updatemeta'] = True
default_prefs['updatecover'] = False
@ -137,8 +138,68 @@ class PrefsFacade():
def save_to_db(self):
set_library_config(self._get_prefs())
prefs = PrefsFacade(default_prefs)
class RejectURLList:
def __init__(self,prefs):
self.prefs = prefs
self.sync_lock = threading.RLock()
self.listcache = None
def _get_listcache(self):
if self.listcache == None:
self.listcache = {}
for line in self.prefs['rejecturls'].splitlines():
if ',' in line:
(rejurl,note) = line.split(',',1)
else:
(rejurl,note) = (line,'')
self.listcache[rejurl] = note
return self.listcache
def _save_list(self,listcache):
rejectlist = []
for url in listcache:
rejectlist.append("%s,%s"%(url,listcache[url]))
self.prefs['rejecturls'] = '\n'.join(rejectlist)
self.prefs.save_to_db()
self.listcache = None
def check(self,url):
with self.sync_lock:
listcache = self._get_listcache()
if url in listcache:
note = listcache[url]
if not note: # in case of URL, but no note.
note = "(no reject note)"
return note
# not found
return None
def remove(self,url):
with self.sync_lock:
listcache = self._get_listcache()
if url in listcache:
del listcache[url]
self._save_list(listcache)
def add(self,rejectlist,clear=False):
# rejectlist=list of (url,note) tuples.
with self.sync_lock:
if clear:
listcache={}
else:
listcache = self._get_listcache()
for (url,note) in rejectlist:
listcache[url]=note
self._save_list(listcache)
def get_list(self):
return copy.deepcopy(self._get_listcache())
rejecturllist = RejectURLList(prefs)
class ConfigWidget(QWidget):
@ -162,6 +223,9 @@ class ConfigWidget(QWidget):
self.personalini_tab = PersonalIniTab(self, plugin_action)
tab_widget.addTab(self.personalini_tab, 'personal.ini')
# self.rejecturls_tab = RejectUrlsTab(self, plugin_action)
# tab_widget.addTab(self.rejecturls_tab, 'Reject URLs')
self.readinglist_tab = ReadingListTab(self, plugin_action)
tab_widget.addTab(self.readinglist_tab, 'Reading Lists')
if 'Reading List' not in plugin_action.gui.iactions:
@ -396,6 +460,13 @@ class BasicTab(QWidget):
self.injectseries.setChecked(prefs['injectseries'])
self.l.addWidget(self.injectseries)
self.l.addSpacing(10)
self.rejectlist = QPushButton('View Reject URL List', self)
self.rejectlist.setToolTip("View list of URLs FFDL will automatically Reject.")
self.rejectlist.clicked.connect(self.show_rejectlist)
self.l.addWidget(self.rejectlist)
self.l.insertStretch(-1)
def set_collisions(self):
@ -411,6 +482,26 @@ class BasicTab(QWidget):
def show_defaults(self):
text = get_resources('plugin-defaults.ini')
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
def show_rejectlist(self):
rejectlist = []
for (url,note) in rejecturllist.get_list().items():
rejectlist.append((None,url,note))
d = RejectListDialog(self,
rejectlist,
header="Edit Reject URLs List",
show_delete=False)
d.exec_()
if d.result() != d.Accepted:
return
rejectlist=[]
for (bookid,url,note) in d.get_reject_list():
rejectlist.append((url,note))
rejecturllist.add(rejectlist,clear=True)
class PersonalIniTab(QWidget):
@ -452,6 +543,34 @@ class PersonalIniTab(QWidget):
text = get_resources('plugin-defaults.ini')
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
# class RejectUrlsTab(QWidget):
# def __init__(self, parent_dialog, plugin_action):
# self.parent_dialog = parent_dialog
# self.plugin_action = plugin_action
# QWidget.__init__(self)
# self.l = QVBoxLayout()
# self.setLayout(self.l)
# label = QLabel("List of story URLs you've previously rejected followed by an optional note. FFDL will stop and ask you if try to download a story on your reject list. The system will put title, author and why you rejected it when added from the FFDL 'Reject Story' option.")
# label.setWordWrap(True)
# self.l.addWidget(label)
# self.l.addSpacing(5)
# self.label = QLabel('Rejected URLs: (URL,Notes)')
# self.l.addWidget(self.label)
# self.rejecturls = QTextEdit(self)
# try:
# self.rejecturls.setFont(QFont("Courier",
# self.plugin_action.gui.font().pointSize()+1));
# except Exception as e:
# print("Couldn't get font: %s"%e)
# self.rejecturls.setLineWrapMode(QTextEdit.NoWrap)
# self.rejecturls.setText(prefs['rejecturls'])
# self.l.addWidget(self.rejecturls)
class ShowDefaultsIniDialog(QDialog):
def __init__(self, icon, text, parent=None):
@ -916,3 +1035,4 @@ class StandardColumnsTab(QWidget):
self.l.addLayout(horz)
self.l.insertStretch(-1)

View file

@ -10,10 +10,12 @@ __docformat__ = 'restructuredtext en'
import traceback
from PyQt4 import QtGui
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QIcon, QTextCursor,
QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, QVariant,
QProgressDialog, QTimer, QDialogButtonBox, QPixmap, Qt, QAbstractItemView )
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout,
QGridLayout, QPushButton, QProgressDialog, QString, QLabel,
QCheckBox, QIcon, QTextCursor, QTextEdit, QLineEdit, QInputDialog,
QComboBox, QClipboard, QVariant, QProgressDialog, QTimer,
QDialogButtonBox, QPixmap, Qt, QAbstractItemView, SIGNAL,
QTableWidgetItem )
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
@ -712,3 +714,209 @@ class StoryListTableWidget(QTableWidget):
self.setItem(dest_row, col, self.takeItem(src_row, col))
self.removeRow(src_row)
self.blockSignals(False)
class RejectListTableWidget(QTableWidget):
def __init__(self, parent):
QTableWidget.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def on_headersection_clicked(self):
self.setSortingEnabled(True)
def populate_table(self, reject_list):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(reject_list))
header_labels = ['URL', 'Note']
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.horizontalHeader().setStretchLastSection(True)
#self.verticalHeader().setDefaultSectionSize(24)
self.verticalHeader().hide()
# need sortingEnbled to sort, but off to up & down.
self.connect(self.horizontalHeader(),
SIGNAL('sectionClicked(int)'),
self.on_headersection_clicked)
# row is just row number.
for row, rejectrow in enumerate(reject_list):
self.populate_table_row(row,rejectrow)
self.resizeColumnsToContents()
self.setMinimumColumnWidth(1, 100)
self.setMinimumColumnWidth(2, 100)
self.setMinimumSize(300, 0)
def setMinimumColumnWidth(self, col, minimum):
if self.columnWidth(col) < minimum:
self.setColumnWidth(col, minimum)
def populate_table_row(self, row, rejectrow):
(bookid,url,note) = rejectrow
url_cell = ReadOnlyTableWidgetItem(url)
url_cell.setData(Qt.UserRole, QVariant(bookid))
url_cell.setToolTip('URL to add to the Reject List.')
self.setItem(row, 0, url_cell)
note_cell = QTableWidgetItem(note)
note_cell.setToolTip('Double-click to edit note.')
self.setItem(row, 1, note_cell)
def get_reject_list(self):
rejectrows = []
for row in range(self.rowCount()):
bookid = self.item(row, 0).data(Qt.UserRole).toPyObject()
url = unicode(self.item(row, 0).text())
note = unicode(self.item(row, 1).text())
rejectrows.append((bookid,url,note))
return rejectrows
def remove_selected_rows(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
message = '<p>Are you sure you want to remove this URL from the list?'
if len(rows) > 1:
message = '<p>Are you sure you want to remove the %d selected URLs from the list?'%len(rows)
if not confirm(message,'ffdl_rejectlist_delete_item_again', self):
return
first_sel_row = self.currentRow()
for selrow in reversed(rows):
self.removeRow(selrow.row())
if first_sel_row < self.rowCount():
self.select_and_scroll_to_row(first_sel_row)
elif self.rowCount() > 0:
self.select_and_scroll_to_row(first_sel_row - 1)
def select_and_scroll_to_row(self, row):
self.selectRow(row)
self.scrollToItem(self.currentItem())
def move_rows_up(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
first_sel_row = rows[0].row()
if first_sel_row <= 0:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in selrows:
self.swap_row_widgets(selrow - 1, selrow + 1)
scroll_to_row = first_sel_row - 1
if scroll_to_row > 0:
scroll_to_row = scroll_to_row - 1
self.scrollToItem(self.item(scroll_to_row, 0))
def move_rows_down(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
last_sel_row = rows[-1].row()
if last_sel_row == self.rowCount() - 1:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in reversed(selrows):
self.swap_row_widgets(selrow + 2, selrow)
scroll_to_row = last_sel_row + 1
if scroll_to_row < self.rowCount() - 1:
scroll_to_row = scroll_to_row + 1
self.scrollToItem(self.item(scroll_to_row, 0))
def swap_row_widgets(self, src_row, dest_row):
self.blockSignals(True)
self.setSortingEnabled(False)
self.insertRow(dest_row)
for col in range(0, self.columnCount()):
self.setItem(dest_row, col, self.takeItem(src_row, col))
self.removeRow(src_row)
self.blockSignals(False)
class RejectListDialog(SizePersistedDialog):
def __init__(self, gui, reject_list,
header="List of Books to Reject",
icon='rotate-right.png',
show_delete=True,
save_size_name='ffdl:reject list dialog'):
SizePersistedDialog.__init__(self, gui, save_size_name)
self.gui = gui
self.setWindowTitle(header)
self.setWindowIcon(get_icon(icon))
layout = QVBoxLayout(self)
self.setLayout(layout)
title_layout = ImageTitleLayout(self, icon, header,
'<i></i>FFDL will remember these URLs and display the note and offer to reject them if you try to download them again later.')
layout.addLayout(title_layout)
rejects_layout = QHBoxLayout()
layout.addLayout(rejects_layout)
self.rejects_table = RejectListTableWidget(self)
rejects_layout.addWidget(self.rejects_table)
button_layout = QVBoxLayout()
rejects_layout.addLayout(button_layout)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
# self.move_up_button = QtGui.QToolButton(self)
# self.move_up_button.setToolTip('Move selected books up the list')
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
# button_layout.addWidget(self.move_up_button)
self.remove_button = QtGui.QToolButton(self)
self.remove_button.setToolTip('Remove selected URL(s) from the list')
self.remove_button.setIcon(get_icon('list_remove.png'))
self.remove_button.clicked.connect(self.remove_from_list)
button_layout.addWidget(self.remove_button)
# self.move_down_button = QtGui.QToolButton(self)
# self.move_down_button.setToolTip('Move selected books down the list')
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
# button_layout.addWidget(self.move_down_button)
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem1)
options_layout = QHBoxLayout()
if show_delete:
self.deletebooks = QCheckBox('Delete Books (including books without FanFiction URLs)?',self)
self.deletebooks.setToolTip("Delete the selected books after adding them to the Rejected URLs list.")
self.deletebooks.setChecked(True)
options_layout.addWidget(self.deletebooks)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
options_layout.addWidget(button_box)
layout.addLayout(options_layout)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.rejects_table.populate_table(reject_list)
def remove_from_list(self):
self.rejects_table.remove_selected_rows()
def get_reject_list(self):
return self.rejects_table.get_reject_list()
def get_deletebooks(self):
return self.deletebooks.isChecked()

View file

@ -41,10 +41,10 @@ from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable i
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource, get_dcsource_chaptercount, get_story_url_from_html
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, permitted_values)
from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, permitted_values, rejecturllist)
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (
AddNewDialog, UpdateExistingDialog, display_story_list, DisplayStoryListDialog,
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog,
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog, RejectListDialog,
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY,
NotGoingToDownload )
@ -190,6 +190,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
self.get_list_url_action = self.create_menu_item_ex(self.menu, 'Get Story URLs from Web Page', image='view.png',
triggered=self.get_urls_from_page)
self.reject_list_action = self.create_menu_item_ex(self.menu, 'Reject Selected Books', image='rotate-right.png',
triggered=self.reject_list_urls)
# print("platform.system():%s"%platform.system())
# print("platform.mac_ver()[0]:%s"%platform.mac_ver()[0])
if not self.check_macmenuhack(): # not platform.mac_ver()[0]: # Some macs crash on these menu items for unknown reasons.
@ -314,6 +317,56 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
show=True,
show_copy_button=False)
def reject_list_urls(self):
if len(self.gui.library_view.get_selected_ids()) > 0:
book_list = map( partial(self._convert_id_to_book, good=False),
self.gui.library_view.get_selected_ids() )
LoopProgressDialog(self.gui,
book_list,
partial(self._reject_story_url_for_list, db=self.gui.current_db),
self._finish_reject_list_urls,
init_label="Collecting URLs for Reject List...",
win_title="Get URLs for Reject List",
status_prefix="URL retrieved")
def _reject_story_url_for_list(self,book,db=None):
self._populate_book_from_calibre_id(book,db)
book['url'] = self._get_story_url(db,book['calibre_id'])
if book['url'] == None:
book['good']=False
else:
book['good']=True
def _finish_reject_list_urls(self, book_list):
# construct reject list of (calibre_id, url, note) tuples.
reject_list = [ (x['calibre_id'],x['url'],
"%s by %s"%(x['title'],
', '.join(x['author']))) for x in book_list if x['good'] ]
if reject_list:
d = RejectListDialog(self.gui,reject_list)
d.exec_()
if d.result() != d.Accepted:
return
bookids=[]
rejectlist=[]
for (bookid,url,note) in d.get_reject_list():
bookids.append(bookid)
rejectlist.append((url,note))
rejecturllist.add(rejectlist)
if d.get_deletebooks():
self.gui.iactions['Remove Books'].delete_books()
else:
message="<p>Rejecting FFDL URLs: None of the books selected have FanFiction URLs.</p><p>Proceed to Remove?</p>"
if confirm(message,'fanfictiondownloader_reject_non_fanfiction', self.gui):
self.gui.iactions['Remove Books'].delete_books()
def add_dialog(self):
#print("add_dialog()")
@ -347,6 +400,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
def update_existing(self):
if len(self.gui.library_view.get_selected_ids()) == 0:
self.gui.status_bar.show_message(_('No Selected Books to Update'), 3000)
return
#print("update_existing()")
@ -431,6 +485,29 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
necessary data. To be called from LoopProgressDialog
'loop'. Also pops dialogs for is adult, user/pass.
'''
url = book['url']
print("url:%s"%url)
rejnote = rejecturllist.check(url)
if rejnote:
if question_dialog(self.gui, 'Reject URL?',
'<p>Reject URL?</p>'+
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
"<p>Click 'No' to download anyway.</p>",
show_copy_button=False):
book['comment'] = "Story on Reject URLs list (%s)."%rejnote
book['good']=False
book['icon']='rotate-right.png'
book['status'] = 'Rejected'
return
else:
if question_dialog(self.gui, 'Remove Reject URL?',
"<p>Remove URL from Reject List?</p>"+
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
"<p>Click 'Yes' to remove it from the list and download,<br /> 'No' to download, but leave it on the Reject list.</p>",
show_copy_button=False):
rejecturllist.remove(url)
# The current database shown in the GUI
# db is an instance of the class LibraryDatabase2 from database.py
@ -447,8 +524,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# book has already been flagged bad for whatever reason.
return
url = book['url']
print("url:%s"%url)
skip_date_update = False
options['personal.ini'] = prefs['personal.ini']
@ -1208,7 +1283,7 @@ make_firstimage_cover:true
book['comment'] = ''
book['url'] = ''
book['added'] = False
return book
def _populate_book_from_calibre_id(self, book, db=None):