diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index c66c7f4..66899cb 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -5,7 +5,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' -__copyright__ = '2011, Fanficdownloader team' +__copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' # The class that all Interface Action plugin wrappers must inherit from @@ -22,7 +22,7 @@ class InterfacePluginDemo(InterfaceActionBase): calibre utilities to run without needing to load the GUI libraries. ''' name = 'aa FanFictionDownLoader Plugin' - description = 'UI plugin to download and maintain FanFiction from various sites.' + description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' version = (1, 0, 0) @@ -31,7 +31,7 @@ class InterfacePluginDemo(InterfaceActionBase): #: This field defines the GUI plugin class that contains all the code #: that actually does something. Its format is module_path:class_name #: The specified class must be defined in the specified module. - actual_plugin = 'calibre_plugins.fanfictiondownloader_plugin.ui:InterfacePlugin' + actual_plugin = 'calibre_plugins.fanfictiondownloader_plugin.ui:FanFictionDownLoaderPlugin' def is_customizable(self): ''' @@ -78,11 +78,9 @@ class InterfacePluginDemo(InterfaceActionBase): if ac is not None: ac.apply_settings() - - - # For testing, run from command line with this: # calibre-debug -e __init__.py +# if __name__ == '__main__': from PyQt4.Qt import QApplication from calibre.gui2.preferences import test_widget diff --git a/calibre-plugin/about.txt b/calibre-plugin/about.txt index cb9e328..df43d96 100644 --- a/calibre-plugin/about.txt +++ b/calibre-plugin/about.txt @@ -1,9 +1,11 @@ -FanFictionDownLoader Plugin Demo +FanFictionDownLoader Plugin =========================== http://code.google.com/p/fanficdownloader/ -Created by Jim Miller, borrowing heavily from Kovid Goyal's 'The InterfacePlugin Demo' +Created by Jim Miller, borrowing heavily from +Kovid Goyal's 'The InterfacePlugin Demo' and +Grant Drake's 'Count Pages' plugins. Requires calibre >= 0.7.53 diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 3b7a50c..aae3a56 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' -__copyright__ = '2011, Fanficdownloader team' +__copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit, QTextEdit diff --git a/calibre-plugin/jobs.py b/calibre-plugin/jobs.py deleted file mode 100644 index 30c92ae..0000000 --- a/calibre-plugin/jobs.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2011, Jim Miller' -__docformat__ = 'restructuredtext en' - -import os, traceback, time - -import ConfigParser - -from calibre.ebooks import DRMError -from calibre.ptempfile import PersistentTemporaryFile -from calibre.ebooks.metadata import MetaInformation - -from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions - -def do_story_downloads(adaptertuple_list, fileform, db, - abort=None, log=None, notifications=[]): # lambda x,y:x lambda makes small anonymous function. - ''' - Master job, to launch child jobs to download this list of stories - ''' - print("do_story_downloads") - notifications.put((0.01, 'Start Downloading Stories')) - count = 0 - total = len(adaptertuple_list) - # Queue all the jobs - for (url,adapter) in adaptertuple_list: - do_story_download(adapter,fileform,db) - count = count + 1 - notifications.put((float(count)/total, 'Downloading Stories')) - # return the map as the job result - # return book_pages_map, book_words_map - return {},{} - -def do_story_download(adapter,fileform,db): - print("do_story_download") - -# ffdlconfig = ConfigParser.SafeConfigParser() -# adapter = adapters.getAdapter(ffdlconfig,url) - - story = adapter.getStoryMetadataOnly() - - mi = MetaInformation(story.getMetadata("title"), - (story.getMetadata("author"),)) # author is a list. - - writer = writers.getWriter(fileform,adapter.config,adapter) - tmp = PersistentTemporaryFile("."+fileform) - print("tmp: "+tmp.name) - - writer.writeStory(tmp) - - print("post write tmp: "+tmp.name) - - mi.set_identifiers({'url':story.getMetadata("storyUrl")}) - mi.publisher = story.getMetadata("site") - - mi.tags = writer.getTags() - mi.languages = ['en'] - mi.pubdate = story.getMetadataRaw('datePublished').strftime("%Y-%m-%d") - mi.timestamp = story.getMetadataRaw('dateCreated').strftime("%Y-%m-%d") - mi.comments = story.getMetadata("description") - - (notadded,addedcount)=db.add_books([tmp],[fileform],[mi], add_duplicates=True) - # Otherwise list of books doesn't update right away. - #self.gui.library_view.model().books_added(addedcount) - - del adapter - del writer - diff --git a/calibre-plugin/plugin.py b/calibre-plugin/plugin.py index b171af1..2e956a2 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/plugin.py @@ -4,11 +4,13 @@ from __future__ import (unicode_literals, division, print_function) __license__ = 'GPL v3' -__copyright__ = '2011, Fanficdownloader team' +__copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QGridLayout, QPushButton, QProgressDialog, QString, - QLabel, QLineEdit, QInputDialog, QComboBox, QProgressDialog, QTimer ) +from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QGridLayout, + QPushButton, QProgressDialog, QString, QLabel, + QTextEdit, QLineEdit, QInputDialog, QComboBox, + QProgressDialog, QTimer ) from calibre.gui2 import error_dialog, warning_dialog, question_dialog @@ -16,22 +18,28 @@ from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapter class DownloadDialog(QDialog): - def __init__(self, gui, icon, do_user_config, pluginaction): + def __init__(self, gui, icon, do_user_config, start_downloads): QDialog.__init__(self, gui) self.gui = gui self.do_user_config = do_user_config - self.pluginaction = pluginaction + self.start_downloads = start_downloads + self.setMinimumWidth(300) self.l = QVBoxLayout() self.setLayout(self.l) self.setWindowTitle('FanFictionDownLoader') self.setWindowIcon(icon) - self.l.addWidget(QLabel('Story URL:')) - self.url = QLineEdit(self) + self.l.addWidget(QLabel('Story URL(s), one per line:')) + self.url = QTextEdit(self) + self.url.setLineWrapMode(QTextEdit.NoWrap) self.url.setText('http://test1.com?sid=12345') self.l.addWidget(self.url) + + # self.url = QLineEdit(self) + # self.url.setText('http://test1.com?sid=12345') + # self.l.addWidget(self.url) self.l.addWidget(QLabel('Output Format:')) self.fileform = QComboBox(self) @@ -42,7 +50,7 @@ class DownloadDialog(QDialog): self.l.addWidget(self.fileform) self.ffdl_button = QPushButton( - 'Download Story', self) + 'Download Stories', self) self.ffdl_button.clicked.connect(self.ffdl) self.l.addWidget(self.ffdl_button) @@ -68,21 +76,22 @@ class DownloadDialog(QDialog): # get_resources will return a dictionary mapping names to bytes. Names that # are not found in the zip file will not be in the returned dictionary. text = get_resources('about.txt') - QMessageBox.about(self, 'About the Interface Plugin Demo', + QMessageBox.about(self, 'About the FanFictionDownLoader Plugin', text.decode('utf-8')) def ffdl(self): - self.pluginaction.start_downloads(unicode(self.url.text()), - unicode(self.fileform.currentText())) + self.start_downloads(unicode(self.url.toPlainText()), + unicode(self.fileform.currentText())) self.hide() def config(self): self.do_user_config(parent=self) - # Apply the changes - #self.label.setText(prefs['hello_world_msg']) + class UserPassDialog(QDialog): - + ''' + Need to collect User/Pass for some sites. + ''' def __init__(self, gui, site): QDialog.__init__(self, gui) self.gui = gui @@ -120,9 +129,11 @@ class UserPassDialog(QDialog): self.status=False self.hide() -class QueueProgressDialog(QProgressDialog): - - def __init__(self, gui, title, loop_list, fileform, loop_function, enqueue_function, db): +class MetadataProgressDialog(QProgressDialog): + ''' + ProgressDialog displayed while fetching metadata for each story. + ''' + def __init__(self, gui, title, loop_list, fileform, getadapter_function, download_list_function, db): QProgressDialog.__init__(self, title, QString(), 0, len(loop_list), gui) self.setWindowTitle(title) self.setMinimumWidth(500) @@ -130,41 +141,39 @@ class QueueProgressDialog(QProgressDialog): self.db = db self.loop_list = loop_list self.fileform = fileform - self.loop_function = loop_function - self.enqueue_function = enqueue_function - # self.book_ids, self.tdir, self.format_order, self.queue, self.db = \ - # book_ids, tdir, format_order, queue, db - # self.pages_algorithm, self.pages_custom_column = pages_algorithm, pages_col - # self.words_algorithm, self.words_custom_column = words_algorithm, words_col + self.getadapter_function = getadapter_function + self.download_list_function = download_list_function self.i, self.loop_bad, self.loop_good = 0, [], [] - self.setValue(0) - self.setLabelText("Fetching metadata for %d of %d"%(0,len(self.loop_list))) QTimer.singleShot(0, self.do_loop) self.exec_() + def bump(self): + self.i += 1 + self.setValue(self.i) + self.setLabelText("Fetching metadata for %d of %d"%(0,len(self.loop_list))) + + def do_loop(self): current = self.loop_list[self.i] - self.i += 1 + self.bump() try: - retval = self.loop_function(current,self.fileform) + retval = self.getadapter_function(current,self.fileform) self.loop_good.append((current,retval)) except Exception as e: - self.loop_bad.append(current) + self.loop_bad.append((current,e)) - self.setValue(self.i) - self.setLabelText("Fetching metadata for %d of %d"%(self.i,len(self.loop_list))) if self.i >= len(self.loop_list): - return self.do_queue() + return self.do_when_finished() else: QTimer.singleShot(0, self.do_loop) - def do_queue(self): + def do_when_finished(self): self.hide() if self.loop_bad != []: res = [] - for current in self.loop_bad: - res.append('%s'%current) + for j in self.loop_bad: + res.append('%s : %s'%j) msg = '%s' % '\n'.join(res) warning_dialog(self.gui, _('Could not get metadata for some stories'), _('Could not get metadata for %d of %d stories.') % @@ -172,4 +181,4 @@ class QueueProgressDialog(QProgressDialog): msg).exec_() self.gui = None # Queue a job to process these ePub/Mobi books - self.enqueue_function(self.loop_good,self.fileform) + self.download_list_function(self.loop_good,self.fileform) diff --git a/calibre-plugin/ui.py b/calibre-plugin/ui.py index c5dca7c..478e6de 100644 --- a/calibre-plugin/ui.py +++ b/calibre-plugin/ui.py @@ -4,28 +4,26 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' -__copyright__ = '2011, Fanficdownloader team' +__copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' from StringIO import StringIO import ConfigParser -from PyQt4.Qt import (QMessageBox) - # The class that all interface action plugins must inherit from -from calibre.gui2.actions import InterfaceAction +from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata import MetaInformation from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog -from calibre.gui2.threaded_jobs import ThreadedJobServer, ThreadedJob - -from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions +from calibre.gui2.actions import InterfaceAction +from calibre.gui2.threaded_jobs import ThreadedJob +from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions from calibre_plugins.fanfictiondownloader_plugin.config import prefs -from calibre_plugins.fanfictiondownloader_plugin.plugin import (DownloadDialog, QueueProgressDialog,UserPassDialog) +from calibre_plugins.fanfictiondownloader_plugin.plugin import DownloadDialog, MetadataProgressDialog, UserPassDialog -from calibre_plugins.fanfictiondownloader_plugin.jobs import do_story_downloads +#from calibre_plugins.fanfictiondownloader_plugin.jobs import do_story_downloads -class InterfacePlugin(InterfaceAction): +class FanFictionDownLoaderPlugin(InterfaceAction): name = 'FanFictionDownLoader' @@ -33,8 +31,10 @@ class InterfacePlugin(InterfaceAction): # The keyboard shortcut can be None if you dont want to use a keyboard # shortcut. Remember that currently calibre has no central management for # keyboard shortcuts, so try to use an unusual/unused shortcut. + # (text, icon_path, tooltip, keyboard shortcut) + # icon_path isn't in the zip--icon loaded below. action_spec = ('FanFictionDownLoader', None, - 'Download FanFiction stories from various web sites', None) + 'Download FanFiction stories from various web sites', None) action_type = 'current' @@ -56,6 +56,7 @@ class InterfacePlugin(InterfaceAction): # The qaction is automatically created from the action_spec defined # above self.qaction.setIcon(icon) + # Call function when plugin triggered. self.qaction.triggered.connect(self.show_dialog) def show_dialog(self): @@ -76,45 +77,49 @@ class InterfacePlugin(InterfaceAction): # self.gui is the main calibre GUI. It acts as the gateway to access # all the elements of the calibre user interface, it should also be the # parent of the dialog - d = DownloadDialog(self.gui, self.qaction.icon(), do_user_config, self) + # DownloadDialog just collects URLs, format and presents buttons. + d = DownloadDialog(self.gui, + self.qaction.icon(), + do_user_config, # method for config button + self.start_downloads, # method to start downloads + ) d.show() def apply_settings(self): - # In an actual non trivial plugin, you would probably need to - # do something based on the settings in prefs + # No need to do anything with perfs here, but we could. prefs - - def start_downloads(self,url,fileform): + def start_downloads(self,urls,fileform): self.ffdlconfig = ConfigParser.SafeConfigParser() self.ffdlconfig.readfp(StringIO(get_resources("defaults.ini"))) self.ffdlconfig.readfp(StringIO(prefs['personal.ini'])) ## XXX including code for Things to Come, namely, a list of ## URLs rather than just one. - url_list = [url, - #"http://test1.com?sid=6700", - #"http://test1.com?sid=6701", - # "http://test1.com?sid=6702", - # "http://test1.com?sid=6703", - # "http://test1.com?sid=6704", - # "http://test1.com?sid=6705", - # "http://test1.com?sid=6706" - ] + url_list = urls.splitlines() + ''' + http://test1.com?sid=6700 + http://test1.com?sid=6701 + http://test1.com?sid=6702 + http://test1.com?sid=6703 + http://test1.com?sid=6704 + http://test1.com?sid=6705 + http://test1.com?sid=6706 + ''' self.fetchmeta_qpd = \ - QueueProgressDialog(self.gui, + MetadataProgressDialog(self.gui, "Getting Metadata for Stories", url_list, fileform, self.get_adapter_for_story, - self.enqueue_story_list_for_download, + self.download_list, self.db) def get_adapter_for_story(self,url,fileform): ''' Returns adapter object for story at URL. To be called from - QueueProgressDialog 'loop' to build up list of adapters. Also + MetadataProgressDialog 'loop' to build up list of adapters. Also pops dialogs for is adult, user/pass, duplicate ''' @@ -162,9 +167,9 @@ class InterfacePlugin(InterfaceAction): else: return None - def enqueue_story_list_for_download(self,adaptertuple_list,fileform): + def download_list(self,adaptertuple_list,fileform): ''' - Called by QueueProgressDialog to enqueue story downloads for BG processing. + Called by MetadataProgressDialog to enqueue story downloads for BG processing. adapter_list is a list of tuples of (url,adapter) ''' print("enqueue_story_list_for_download") @@ -219,3 +224,58 @@ class InterfacePlugin(InterfaceAction): # show_copy_button=False, parent=self.gui) # p.show() #info_dialog(self.gui,'FFDL Complete','It worked?') + +def do_story_downloads(adaptertuple_list, fileform, db, + abort=None, log=None, notifications=[]): # lambda x,y:x lambda makes small anonymous function. + ''' + Master job, to launch child jobs to download this list of stories + ''' + print("do_story_downloads") + notifications.put((0.01, 'Start Downloading Stories')) + count = 0 + total = len(adaptertuple_list) + # Queue all the jobs + for (url,adapter) in adaptertuple_list: + do_story_download(adapter,fileform,db) + count = count + 1 + notifications.put((float(count)/total, 'Downloading Stories')) + # return the map as the job result + # return book_pages_map, book_words_map + return {},{} + +def do_story_download(adapter,fileform,db): + print("do_story_download") + +# ffdlconfig = ConfigParser.SafeConfigParser() +# adapter = adapters.getAdapter(ffdlconfig,url) + + story = adapter.getStoryMetadataOnly() + + mi = MetaInformation(story.getMetadata("title"), + (story.getMetadata("author"),)) # author is a list. + + writer = writers.getWriter(fileform,adapter.config,adapter) + tmp = PersistentTemporaryFile("."+fileform) + print("tmp: "+tmp.name) + + writer.writeStory(tmp) + + print("post write tmp: "+tmp.name) + + mi.set_identifiers({'url':story.getMetadata("storyUrl")}) + mi.publisher = story.getMetadata("site") + + mi.tags = writer.getTags() + mi.languages = ['en'] + mi.pubdate = story.getMetadataRaw('datePublished').strftime("%Y-%m-%d") + mi.timestamp = story.getMetadataRaw('dateCreated').strftime("%Y-%m-%d") + mi.comments = story.getMetadata("description") + + (notadded,addedcount)=db.add_books([tmp],[fileform],[mi], add_duplicates=True) + # Otherwise list of books doesn't update right away. + #self.gui.library_view.model().books_added(addedcount) + + del adapter + del writer + +