diff --git a/calibre-plugin/common_utils.py b/calibre-plugin/common_utils.py index 0ce98474..71a74aac 100644 --- a/calibre-plugin/common_utils.py +++ b/calibre-plugin/common_utils.py @@ -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): ''' diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 5bdb6caa..8a3789d1 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -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) + diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index 0345bb19..8c2aee0e 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -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 = '
Are you sure you want to remove this URL from the list?' + if len(rows) > 1: + message = '
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, + '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() + diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 19735993..d849d41f 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -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="
Rejecting FFDL URLs: None of the books selected have FanFiction URLs.
Proceed to Remove?
" + 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?', + 'Reject URL?
'+ + '%s is on the Reject URL list:
"%s"
Click 'No' to download anyway.
", + 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?', + "Remove URL from Reject List?
"+ + '%s is on the Reject URL list:
"%s"
Click 'Yes' to remove it from the list and download,
'No' to download, but leave it on the Reject list.