From 97e257d22abdd10a61f102cea2b9a21680cf885b Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 16 Dec 2011 10:55:08 -0600 Subject: [PATCH 01/30] Begin on a calibre plugin of FFDL while still sharing code with CLI & web. --- app.yaml | 2 +- fanficdownloader/adapters/__init__.py | 53 ++++++++++++------- .../adapters/adapter_adastrafanficcom.py | 6 +-- .../adapters/adapter_fanficcastletvnet.py | 6 +-- .../adapters/adapter_fanfictionnet.py | 4 +- .../adapters/adapter_fictionalleyorg.py | 6 +-- .../adapters/adapter_ficwadcom.py | 6 +-- .../adapters/adapter_fimfictionnet.py | 11 ++-- .../adapter_harrypotterfanfictioncom.py | 6 +-- .../adapters/adapter_mediaminerorg.py | 6 +-- .../adapters/adapter_potionsandsnitchesnet.py | 6 +-- .../adapters/adapter_tenhawkpresentscom.py | 6 +-- fanficdownloader/adapters/adapter_test1.py | 4 +- .../adapter_thewriterscoffeeshopcom.py | 6 +-- .../adapters/adapter_tthfanficorg.py | 6 +-- .../adapters/adapter_twilightednet.py | 6 +-- .../adapters/adapter_twiwritenet.py | 6 +-- .../adapters/adapter_whoficcom.py | 4 +- fanficdownloader/adapters/base_adapter.py | 10 ++-- fanficdownloader/writers/__init__.py | 2 +- fanficdownloader/writers/base_writer.py | 4 +- fanficdownloader/writers/writer_epub.py | 2 +- fanficdownloader/writers/writer_mobi.py | 4 +- fanficdownloader/writers/writer_txt.py | 2 +- 24 files changed, 95 insertions(+), 79 deletions(-) diff --git a/app.yaml b/app.yaml index 852b064..d0c0668 100644 --- a/app.yaml +++ b/app.yaml @@ -1,5 +1,5 @@ # ffd-retief-hrd fanfictiondownloader -application: fanfictiondownloader +application: ffd-retief-hrd version: 4-1-1 runtime: python27 api_version: 1 diff --git a/fanficdownloader/adapters/__init__.py b/fanficdownloader/adapters/__init__.py index 0800ca4..70f5006 100644 --- a/fanficdownloader/adapters/__init__.py +++ b/fanficdownloader/adapters/__init__.py @@ -15,21 +15,51 @@ # limitations under the License. # -import os, re, sys, glob +import os, re, sys, glob, types from os.path import dirname, basename, normpath import logging import urlparse as up -import fanficdownloader.exceptions as exceptions +from .. import exceptions as exceptions + +## must import each adapter here. + +import adapter_test1 +import adapter_fanfictionnet +import adapter_fanficcastletvnet +import adapter_fanfictionnet +import adapter_fictionalleyorg +import adapter_fictionpresscom +import adapter_ficwadcom +import adapter_fimfictionnet +import adapter_harrypotterfanfictioncom +import adapter_mediaminerorg +import adapter_potionsandsnitchesnet +import adapter_tenhawkpresentscom +import adapter_adastrafanficcom +import adapter_thewriterscoffeeshopcom +import adapter_tthfanficorg +import adapter_twilightednet +import adapter_twiwritenet +import adapter_whoficcom ## This bit of complexity allows adapters to be added by just adding -## the source file. It eliminates the long if/else clauses we used to -## need to pick out the adapter. +## importing. It eliminates the long if/else clauses we used to need +## to pick out the adapter. ## List of registered site adapters. - __class_list = [] +def imports(): + for name, val in globals().items(): + if isinstance(val, types.ModuleType): + yield val.__name__ + +for x in imports(): + if "fanficdownloader.adapters.adapter_" in x: + #print x + __class_list.append(sys.modules[x].getClass()) + def getAdapter(config,url): ## fix up leading protocol. fixedurl = re.sub(r"(?i)^[htp]+[:/]+","http://",url.strip()) @@ -63,16 +93,3 @@ def getClassFor(domain): for cls in __class_list: if cls.matchesSite(domain): return cls - -## Automatically import each adapter_*.py file. -## Each implement getClass() to their class - -filelist = glob.glob(dirname(__file__)+'/adapter_*.py') -sys.path.insert(0,normpath(dirname(__file__))) - -for file in filelist: - #print "file: "+basename(file)[:-3] - module = __import__(basename(file)[:-3]) - __class_list.append(module.getClass()) - -del sys.path[0] diff --git a/fanficdownloader/adapters/adapter_adastrafanficcom.py b/fanficdownloader/adapters/adapter_adastrafanficcom.py index be600c6..bc908a2 100644 --- a/fanficdownloader/adapters/adapter_adastrafanficcom.py +++ b/fanficdownloader/adapters/adapter_adastrafanficcom.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_fanficcastletvnet.py b/fanficdownloader/adapters/adapter_fanficcastletvnet.py index 96d5d1b..ee31b47 100644 --- a/fanficdownloader/adapters/adapter_fanficcastletvnet.py +++ b/fanficdownloader/adapters/adapter_fanficcastletvnet.py @@ -20,9 +20,9 @@ import logging import re import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_fanfictionnet.py b/fanficdownloader/adapters/adapter_fanfictionnet.py index 42e2b88..4afa74e 100644 --- a/fanficdownloader/adapters/adapter_fanfictionnet.py +++ b/fanficdownloader/adapters/adapter_fanfictionnet.py @@ -21,8 +21,8 @@ import re import urllib2 import time -import fanficdownloader.BeautifulSoup as bs -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_fictionalleyorg.py b/fanficdownloader/adapters/adapter_fictionalleyorg.py index 14a03ec..7eb9e37 100644 --- a/fanficdownloader/adapters/adapter_fictionalleyorg.py +++ b/fanficdownloader/adapters/adapter_fictionalleyorg.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_ficwadcom.py b/fanficdownloader/adapters/adapter_ficwadcom.py index 2782199..8d63aba 100644 --- a/fanficdownloader/adapters/adapter_ficwadcom.py +++ b/fanficdownloader/adapters/adapter_ficwadcom.py @@ -22,9 +22,9 @@ import urllib2 import time import httplib, urllib -import fanficdownloader.BeautifulSoup as bs -import fanficdownloader.exceptions as exceptions -from fanficdownloader.htmlcleanup import stripHTML +from .. import BeautifulSoup as bs +from .. import exceptions as exceptions +from ..htmlcleanup import stripHTML from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_fimfictionnet.py b/fanficdownloader/adapters/adapter_fimfictionnet.py index 8332203..cddcb8f 100644 --- a/fanficdownloader/adapters/adapter_fimfictionnet.py +++ b/fanficdownloader/adapters/adapter_fimfictionnet.py @@ -21,9 +21,9 @@ import re import urllib2 import cookielib as cl -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate @@ -88,8 +88,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter): soup = bs.BeautifulSoup(data).find("div", {"class":"content_box post_content_box"}) - title = soup.find("h2").find("a").text # first a link in first h2 is title. - author = soup.find("h2").find("span",{'class':'author'}).find("a").text + title, author = [link.text for link in soup.find("h2").findAll("a")] self.story.setMetadata("title", title) self.story.setMetadata("author", author) self.story.setMetadata("authorId", author) # The author's name will be unique @@ -159,4 +158,4 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter): if soup == None: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) return utf8FromSoup(soup) - + \ No newline at end of file diff --git a/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py b/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py index 155ebf7..2d44f9b 100644 --- a/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py +++ b/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_mediaminerorg.py b/fanficdownloader/adapters/adapter_mediaminerorg.py index 62aa50c..23c72e3 100644 --- a/fanficdownloader/adapters/adapter_mediaminerorg.py +++ b/fanficdownloader/adapters/adapter_mediaminerorg.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py b/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py index 6cb4b41..de9a722 100644 --- a/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py +++ b/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_tenhawkpresentscom.py b/fanficdownloader/adapters/adapter_tenhawkpresentscom.py index 6ec8a24..a4922d2 100644 --- a/fanficdownloader/adapters/adapter_tenhawkpresentscom.py +++ b/fanficdownloader/adapters/adapter_tenhawkpresentscom.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index 1debdfc..05f6ee5 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -19,8 +19,8 @@ import datetime import time import logging -import fanficdownloader.BeautifulSoup as bs -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from .. import exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_thewriterscoffeeshopcom.py b/fanficdownloader/adapters/adapter_thewriterscoffeeshopcom.py index 5a399a6..fecc2bd 100644 --- a/fanficdownloader/adapters/adapter_thewriterscoffeeshopcom.py +++ b/fanficdownloader/adapters/adapter_thewriterscoffeeshopcom.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_tthfanficorg.py b/fanficdownloader/adapters/adapter_tthfanficorg.py index 1906aaa..4e9b0a3 100644 --- a/fanficdownloader/adapters/adapter_tthfanficorg.py +++ b/fanficdownloader/adapters/adapter_tthfanficorg.py @@ -21,9 +21,9 @@ import re import urllib2 import time -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_twilightednet.py b/fanficdownloader/adapters/adapter_twilightednet.py index 2a58adf..675f305 100644 --- a/fanficdownloader/adapters/adapter_twilightednet.py +++ b/fanficdownloader/adapters/adapter_twilightednet.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_twiwritenet.py b/fanficdownloader/adapters/adapter_twiwritenet.py index 5fe9c3d..95da354 100644 --- a/fanficdownloader/adapters/adapter_twiwritenet.py +++ b/fanficdownloader/adapters/adapter_twiwritenet.py @@ -21,9 +21,9 @@ import re import urllib import urllib2 -import fanficdownloader.BeautifulSoup as bs -from fanficdownloader.htmlcleanup import stripHTML -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/adapter_whoficcom.py b/fanficdownloader/adapters/adapter_whoficcom.py index fdc3fb2..40baa6a 100644 --- a/fanficdownloader/adapters/adapter_whoficcom.py +++ b/fanficdownloader/adapters/adapter_whoficcom.py @@ -20,8 +20,8 @@ import logging import re import urllib2 -import fanficdownloader.BeautifulSoup as bs -import fanficdownloader.exceptions as exceptions +from .. import BeautifulSoup as bs +from .. import exceptions as exceptions from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate diff --git a/fanficdownloader/adapters/base_adapter.py b/fanficdownloader/adapters/base_adapter.py index cd76abc..6c9d3a1 100644 --- a/fanficdownloader/adapters/base_adapter.py +++ b/fanficdownloader/adapters/base_adapter.py @@ -39,13 +39,13 @@ except: pass #logging.info("Hook to make default deadline 10.0 NOT installed--not using appengine") -from fanficdownloader.story import Story -from fanficdownloader.configurable import Configurable -from fanficdownloader.htmlcleanup import removeEntities, removeAllEntities, stripHTML -from fanficdownloader.exceptions import InvalidStoryURL +from ..story import Story +from ..configurable import Configurable +from ..htmlcleanup import removeEntities, removeAllEntities, stripHTML +from ..exceptions import InvalidStoryURL try: - import fanficdownloader.chardet as chardet + from .. import chardet as chardet except ImportError: chardet = None diff --git a/fanficdownloader/writers/__init__.py b/fanficdownloader/writers/__init__.py index 7a3f703..7d9faf6 100644 --- a/fanficdownloader/writers/__init__.py +++ b/fanficdownloader/writers/__init__.py @@ -18,7 +18,7 @@ ## This could (should?) use a dynamic loader like adapters, but for ## now, it's static, since there's so few of them. -from fanficdownloader.exceptions import FailedToDownload +from ..exceptions import FailedToDownload from writer_html import HTMLWriter from writer_txt import TextWriter diff --git a/fanficdownloader/writers/base_writer.py b/fanficdownloader/writers/base_writer.py index 411c2e4..ad476ce 100644 --- a/fanficdownloader/writers/base_writer.py +++ b/fanficdownloader/writers/base_writer.py @@ -24,8 +24,8 @@ import zipfile from zipfile import ZipFile, ZIP_DEFLATED import logging -from fanficdownloader.configurable import Configurable -from fanficdownloader.htmlcleanup import removeEntities, removeAllEntities, stripHTML +from ..configurable import Configurable +from ..htmlcleanup import removeEntities, removeAllEntities, stripHTML class BaseStoryWriter(Configurable): diff --git a/fanficdownloader/writers/writer_epub.py b/fanficdownloader/writers/writer_epub.py index 7b3fac2..57424c1 100644 --- a/fanficdownloader/writers/writer_epub.py +++ b/fanficdownloader/writers/writer_epub.py @@ -26,7 +26,7 @@ from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED from xml.dom.minidom import parse, parseString, getDOMImplementation from base_writer import * -from fanficdownloader.htmlcleanup import stripHTML +from ..htmlcleanup import stripHTML class EpubWriter(BaseStoryWriter): diff --git a/fanficdownloader/writers/writer_mobi.py b/fanficdownloader/writers/writer_mobi.py index 73b48af..87dd30f 100644 --- a/fanficdownloader/writers/writer_mobi.py +++ b/fanficdownloader/writers/writer_mobi.py @@ -20,8 +20,8 @@ import string import StringIO from base_writer import * -from fanficdownloader.htmlcleanup import stripHTML -from fanficdownloader.mobi import Converter +from ..htmlcleanup import stripHTML +from ..mobi import Converter class MobiWriter(BaseStoryWriter): diff --git a/fanficdownloader/writers/writer_txt.py b/fanficdownloader/writers/writer_txt.py index ccf4637..55cc771 100644 --- a/fanficdownloader/writers/writer_txt.py +++ b/fanficdownloader/writers/writer_txt.py @@ -21,7 +21,7 @@ from textwrap import wrap from base_writer import * -from fanficdownloader.html2text import html2text, BODY_WIDTH +from ..html2text import html2text, BODY_WIDTH ## In BaseStoryWriter, we define _write to encode objects ## back into for true output. But txt needs to write the From d5c38927fb951c5e06f743cda3305ad90e4d1527 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 16 Dec 2011 10:57:40 -0600 Subject: [PATCH 02/30] Add initial version of plugin code--proof of concept stage. --- calibre-plugin/__init__.py | 81 ++++++++++ calibre-plugin/about.txt | 8 + calibre-plugin/config.py | 41 +++++ calibre-plugin/images/icon.png | Bin 0 -> 5785 bytes ...mport-name-fanfictiondownloader_plugin.txt | 0 calibre-plugin/plugin.py | 145 ++++++++++++++++++ calibre-plugin/ui.py | 71 +++++++++ 7 files changed, 346 insertions(+) create mode 100644 calibre-plugin/__init__.py create mode 100644 calibre-plugin/about.txt create mode 100644 calibre-plugin/config.py create mode 100644 calibre-plugin/images/icon.png create mode 100644 calibre-plugin/plugin-import-name-fanfictiondownloader_plugin.txt create mode 100644 calibre-plugin/plugin.py create mode 100644 calibre-plugin/ui.py diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py new file mode 100644 index 0000000..544fa3a --- /dev/null +++ b/calibre-plugin/__init__.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# -*- coding: utf-8 -*- +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'Apache License' +__copyright__ = '2011, Fanficdownloader team' +__docformat__ = 'restructuredtext en' + +# The class that all Interface Action plugin wrappers must inherit from +from calibre.customize import InterfaceActionBase + +class InterfacePluginDemo(InterfaceActionBase): + ''' + This class is a simple wrapper that provides information about the actual + plugin class. The actual interface plugin class is called InterfacePlugin + and is defined in the ui.py file, as specified in the actual_plugin field + below. + + The reason for having two classes is that it allows the command line + 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.' + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Jim Miller' + version = (1, 0, 0) + minimum_calibre_version = (0, 7, 53) + + #: 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' + + def is_customizable(self): + ''' + This method must return True to enable customization via + Preferences->Plugins + ''' + return True + + def config_widget(self): + ''' + Implement this method and :meth:`save_settings` in your plugin to + use a custom configuration dialog. + + This method, if implemented, must return a QWidget. The widget can have + an optional method validate() that takes no arguments and is called + immediately after the user clicks OK. Changes are applied if and only + if the method returns True. + + If for some reason you cannot perform the configuration at this time, + return a tuple of two strings (message, details), these will be + displayed as a warning dialog to the user and the process will be + aborted. + + The base class implementation of this method raises NotImplementedError + so by default no user configuration is possible. + ''' + # It is important to put this import statement here rather than at the + # top of the module as importing the config class will also cause the + # GUI libraries to be loaded, which we do not want when using calibre + # from the command line + from calibre_plugins.fanfictiondownloader_plugin.config import ConfigWidget + return ConfigWidget() + + def save_settings(self, config_widget): + ''' + Save the settings specified by the user with config_widget. + + :param config_widget: The widget returned by :meth:`config_widget`. + ''' + config_widget.save_settings() + + # Apply the changes + ac = self.actual_plugin_ + if ac is not None: + ac.apply_settings() + + diff --git a/calibre-plugin/about.txt b/calibre-plugin/about.txt new file mode 100644 index 0000000..ae4d602 --- /dev/null +++ b/calibre-plugin/about.txt @@ -0,0 +1,8 @@ +FanFictionDownLoader Plugin Demo +=========================== + +Created by Jim Miller, borrowing heavily from Kovid Goyal's 'The +InterfacePlugin Demo' + +Requires calibre >= 0.7.53 + diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py new file mode 100644 index 0000000..f36cb71 --- /dev/null +++ b/calibre-plugin/config.py @@ -0,0 +1,41 @@ +#!/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__ = 'Apache License' +__copyright__ = '2011, Fanficdownloader team' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit + +from calibre.utils.config import JSONConfig + +# This is where all preferences for this plugin will be stored +# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also +# in a global namespace, so make it as unique as possible. +# You should always prefix your config file name with plugins/, +# so as to ensure you dont accidentally clobber a calibre config file +prefs = JSONConfig('plugins/fanfictiondownloader_plugin') + +# Set defaults +prefs.defaults['hello_world_msg'] = 'Hello, World!' + +class ConfigWidget(QWidget): + + def __init__(self): + QWidget.__init__(self) + self.l = QHBoxLayout() + self.setLayout(self.l) + + self.label = QLabel('Hello world &message:') + self.l.addWidget(self.label) + + self.msg = QLineEdit(self) + self.msg.setText(prefs['hello_world_msg']) + self.l.addWidget(self.msg) + self.label.setBuddy(self.msg) + + def save_settings(self): + prefs['hello_world_msg'] = unicode(self.msg.text()) + diff --git a/calibre-plugin/images/icon.png b/calibre-plugin/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ad823e2ff4d53c376632ccb0e2fe60d96cc063ea GIT binary patch literal 5785 zcmV;K7G~**P)g@Il=sYFBw5pBmc%b4Il=J;xC0t2o4nd|H;U}JMAu57Yj`AYqjEkMlJ>n z@c%0#5(jAV=RC#!ug&uRK^O%7Jvs@de=f-f(?I$EC~p4ICl6-)eZvHFEF+5P+#UD8 z0!%OgMvngw%noF8{zt*z8WsPeC}z)k#Q5(s6G9ULH*(eAXN0>M8GoNBrvRn@V+-f= zKY#u*`1buf!`H8089sgf%<%2&HwG3K76x8k9tK_>9tJKhP6kd+P6mJg!WMJ$am6Y; zG(P&PtOrs7OyUqehx2zZ%^Lm;9yIVsVUqqIz{rk>z@H~Lpo$Se2oDV=!G8>lT>nup zDE2<5DgQ@N{H;adKPWu8TknH0n{f=o|1XU2aQ>37)Q^&o;4%1$86$nbVi4@+Z>1|q=!*G!IltL_}YYw+hx<^N0~|A6ts$UqWc`_J(26%zyR z?7NH@sR#skr{85{1d6~?5qI}JjFbf~dA>2C;t!~XVi^3j5>&`Q4B%?K56Q%?d0_TG zU{Yl@jD=VRO^Liy?=o;FwLwy432I4(5)@x+6%Ck0|DA{N|9xg;_)mPWC~`O7e}Fgg zfGIlxNX;kGz(io;UVt^#V+eBozW~G_4U9mV0UL(O6$0@AVv-xE8mRyK_b8Itff&eVW(H^OMyP%igBySb)c^VOm*LOfKMa3< z|6u?b{s)M`^q)T{t^pYY!mPkD6GXGH0%;~7A4mf&1X}ahP7 zXy6?_djjk7qb_I8lB(p0Qd)nG{k+4Vc=?hI0H;ZfuWqsII~l1Jnb8T!rezJd-jyByO^kh+Rf$~61wlSA60#345aGf%Omseieq zjeaI+Xd-XwnyAb}dOhdLWxDK&{WjgX?_G+2-$s-<}+Zjrwq z0cf$B&?bT?{AM@Vwl>&+P!yZCNku^@coGVNsi3H!n434N*NR3Dfg@D6vg8ogLqs{pohQY0_jJ7&1GP-Tmh6y!XEERn1XV zbJ+f#ZvDwtp+Daua6I8zo1pp9qYg2DpeNesyQ=0shteL|7R4V54d>cke4TVc#w8S2 z9J9+$JRjRRgcG`Rk3Q7|d={;`87-(Yh}_IF*YlD@H?XJhdHK3sz_*1V-L$WqmWHEo_RO~GthD+(xbc*uR4gq*F=)O=de(FvR#aCL|&2s_)i$X1@ zwjm$-tb+a_yu!C@ZKe0xO22M3=(&>(g+ZwmPW1@2(*V*7N(8el$ItpP3_&+-XBjgE zr8fLZDERqzlmw5zvGrQEUo5gQKC2>5)v{vXSfTtS45CZ}kms7-yQXCIYq+~{<2Osm zROr(&pMo0O0hQKQb(`k-bmE&fPO=IrS129t?U=K|FFNo2z;Z3g;6eO1n75 z)YY3Hpo`bK<+$Y{(IqB<+H=HrjX#Wv22dUyg3{Y(5V@75XPWd0$6DV}bci`94EH&6 zK7@N3@5$iW$(kHY#?T81Q45tdeo-YXm^0jZBg^UwU15>O&=jq-%qf0y;(Y=@4Fqex zuF$n`Ic}CYd?lSp$o^MDYR}dM)&YGbzKhCytXT{z8x5I_#1ab{ABpMF4MWqDby?Qi z(20^5=*EINo9=jy&N?0}HweMmwIg%gjR;LmU~9H)rB=M%HBW^_kCUVvpPuQj)q>T)+FUhB?A@(B3}G>PyygZs z=j9xgyzrXhti}2(=5*32qy|X4NUg$TrHZtRts-QJj457C1J(w~2zdfamB9gb&5b}w z0Nk+T2M4~y<=8Hpal3@Z%{6nAT;Ah|DqE^hS&ih1RbF#2-*DW{X90;d8?|e4KT}0u`KC(T>c3_ ztQCdEQH0@tX7?_4*R*WKwo(LhNuVOsT&0Ni#T1m5zNDcFLZRfr7iqPPP<)U^t1m4T z4EDhXAEJF0IekzcD$Sd3UZ5ZdX>u)C#42}bN%oq%-Esbzox9o1Zf^H(lP>)1-puaI ze>3yX{Qvj;eXrU2>!T&EXT|z;k%ZJ)yspOeJY?NmTopN_)fpwnEf^|@J0*lp9@f!ScArI`P#~fQQ;w(2i>{ct2Wp-^WvR;uOtyI z?hsWTeNJm-w|1Oa8@GRvfnol*?LX=9V7caamN4&-VEVh-zLgKGc4BjY%))M71pczxV8ENt8v~@4= zT>>x7OtL?u?fkJuirdq}(<3D=mF$>|-_w+VBkS1yE$pKwW8!<_C&qsEvGI&F0x|Q8 z+<@$K#}p|W^I1oRKmQsMz_?GQcip%%8*l0p?Vsa?x>pNBPFX=`h zg+Y;Q5mpx^s2pMi7~u-oC*&RMGqIwN&W@DGPA1l6n5Z?onc#lL5SPNpwn@6~0@!CV zyz0H-3b9xzi2N01mi@=-r$v9+gFj*ItK%Eq)7pjX_-V+V`pj1rxaJ41FCFbVqL^Pr z_qipJr9(TCePxPQ{)4$TV1Is40xn~x{2%W*AdLdsIX?`|?=MMQia+B$Gj>!cxpw)8 zC_|tv|JH}WB`~gYi;>18SkI8NTRH4lr>343b_(U~>o0qIAudLu45Kp)M%zF=D}Uto z4~!m8rXAOoi1QzPPlH z5B85&gCknybtLvax_WVViazbmbv~%!U9y84%$%W_pbm7u+ zIUSJq+7QF7h@}kjh9yw+7c;qA<2-K-Qu2h9OdN$i$t!mE#9&n}WiG}1#VSGJ&kM*1 z#*}&OyQ0@tb=#ow#_WbX9Th=d&$^3-8RY;=OlN!N9U6zq-gxPr!2nM#Uk`zl(_ zZ4eq)Itf36;sE zF(igyK_pYsW=Iq}B9e&PL<2T~cx_{yTn~%NOYxTnfIKEW6ri7QBhNm9t$SrmDI>G#x|dNM_VR+PCZd%nM$YY zOg-)($O$ZLwvY1oSn^DWwFiyKXHX$LlWtHkjH&8nAtKlGO_(zO3sBCr$3{_v@o)C_ zp1rn53@R$L*ET9Das&gYF|-J%G3pT|h+-&ei1B^#8H4l=iAqdp0gd62w8Tez#I%@b zlw#V7h~T3o6>Xpou@FK0<66qK+q>O$9=m&c+g`6n_j;{PGP&Kk*`1x=eDlpW-+bSP z#$(u0rSOuHTH8&c-&zud{(3t|f1wuKw=&P(Y9gB%D4><~Ln$+XP#RUBMgpH3sS46LG8J-#19Ur%~=_N*eNRvwlD$2=8FM;d6em7S}C|7_h7*D zWw83<#-s_)+zn>@gn7*k(mRiqN+IFdG8XK}8lU zug6dY0cwa9D={{XB6Xjeao#4Ab*z8*_PAUu1OCI2?=>A%Q*{l!4-|WVlPfLLB&z+* zuqtp&nG7yJQzXYg6AM0lJGSpd;r50s@Yr=hL$$PJ(E@Pnft}briy;3P(FpP6nB9&?~l7{@iIox1b^qi_&HCOf!h}#v=-Py3;I+3@j zeNPqz@go&n9*d|m;)q?f7_4)b1G;i4BcARAH8=ye~2v{^_#%DSV|!ts?l}q$KVv@FR)53@YdZ$D=Oj2EXMT0DuY)jncsKhfyBi!UchLb5zugQ% z(>8Dg4$*oPu=ifXg8g}Lt$l|9i2)KmdIh4b4PcvhH`rD&3H^6U#A1gl~LCG32JK07i}RKyCox1K#rUKGyJa`Eg@UCRipzWX$a<8KRs>(wrroiDa(6pf}{!;~k=;ETt<#{)!7 z?CG^8`W$Uw)<-{mPC=&6=Hy`t^;1AeS!CA}n(!~|{{ZB!A6VPIC8;+I2+QOMDlr}n zW8YD6Fh%M`Rn*D|Gk$8*l_HDXKeA3up9jtrJE`p#DYs0!E}>CxRh?yHUZ~ugI|A?plZx#{KN_$C9y~qPaPz|QouI%VX(}93>*Pw zuO^>79Pcjy_3Ur7p7q-0V41yKt4I0b&mU7tgWl6am|WcOE`?GC5pUSjfWI4m%ROLS zu!4EJl5hFD4WfJ2gYez^sTQ*u(s%b;o#BivRBAf^Qc*RQRm;aLtBQ)P-NX}lt8zAV z2F6*WlEY+rU8uH++60CXGsr=*`JN18mW$xTxGJg7uk%(TE0lAd3$US*8D+{bnD_}n zC}(3=Fyo+}U+-<;oN_60r#~1(SuCBKJjxaY*t(nzUCS~CY9#2b#amN9QShb(rlgqv zij!8i5^{w`PqFFR_^puz)FcWPa)?%_WEQ{5LIc3iBa;L`2U%FeY-|_DnEp`m9@qM= zs*Fzm5h;12*Mnhf##`7z0`4T4?kyOGGS|AUuxT8_?9m zE29e{k$I6Sx%R45U6dxtboZG3Li3G`(WJ{hvmwX?r4}URT{7%uJd7$Q$SI~TgA~|^ z+lyPMAZnrF8xPXYsx8$(WkETNKB90sX_+H%qApwg%Bqh6?syH+{EVO#B3t1(xE@k7 zxu@mHp9y6X(5zFcMzPxpnhjHvu!=Ir(G(bRaRC~o#Uw!%d*`cvp_A> zb*@zT%{Kr=u06?O5y^l3f;S4ifMzUAfBXAq6QJBH0HzctxEN^Dky+x06@IRhPoYQS zzlj2WMnF{^HV6<4jew}ns3zcQnb8bfUk3CV62cM1>jUU7(ivzCM9ri;UXDO-X0ES; zhWUSJ*dzV>4Fl7p{5HZcaq;QF&#C5OV@h%M=@X>Lft^*7=U8 z4g~slNyLNCq&{1E>Rt+^d1D5X7T!safxM0Q6odr%t8HJ;d$&QNp{?NDejX zr;+V!i29`@15+RDiUE}fr0P|nzt#h&d17c;d5>f$SG+QqLXM8`N_{53cI-q7DQC#j8ViZ2(v+(}AlFvH@4jNBRCW(pr_^LXj*?OdJD>p`M_Q;plKN most_recent: + most_recent = timestamp + most_recent_id = record[0] + + if most_recent_id is not None: + # Get the row number of the id as shown in the GUI + row_number = self.db.row(most_recent_id) + # Get a reference to the View plugin + view_plugin = self.gui.iactions['View'] + # Ask the view plugin to launch the viewer for row_number + view_plugin._view_books([row_number]) + + def config(self): + self.do_user_config(parent=self) + # Apply the changes + self.label.setText(prefs['hello_world_msg']) + diff --git a/calibre-plugin/ui.py b/calibre-plugin/ui.py new file mode 100644 index 0000000..d978581 --- /dev/null +++ b/calibre-plugin/ui.py @@ -0,0 +1,71 @@ +#!/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__ = 'Apache License' +__copyright__ = '2011, Fanficdownloader team' +__docformat__ = 'restructuredtext en' + +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + # You do not need this code in your plugins + get_icons = get_resources = None + +# The class that all interface action plugins must inherit from +from calibre.gui2.actions import InterfaceAction +from calibre_plugins.fanfictiondownloader_plugin.plugin import DemoDialog + +class InterfacePlugin(InterfaceAction): + + name = 'FanFictionDownLoader' + + # Declare the main action associated with this plugin + # 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. + action_spec = ('FanFictionDownLoader', None, + 'Run the FanFictionDownLoader Plugin', None) + + def genesis(self): + # This method is called once per plugin, do initial setup here + + # Set the icon for this interface action + # The get_icons function is a builtin function defined for all your + # plugin code. It loads icons from the plugin zip file. It returns + # QIcon objects, if you want the actual data, use the analogous + # get_resources builtin function. + # + # Note that if you are loading more than one icon, for performance, you + # should pass a list of names to get_icons. In this case, get_icons + # will return a dictionary mapping names to QIcons. Names that + # are not found in the zip file will result in null QIcons. + icon = get_icons('images/icon.png') + + # The qaction is automatically created from the action_spec defined + # above + self.qaction.setIcon(icon) + self.qaction.triggered.connect(self.show_dialog) + + def show_dialog(self): + # The base plugin object defined in __init__.py + base_plugin_object = self.interface_action_base_plugin + # Show the config dialog + # The config dialog can also be shown from within + # Preferences->Plugins, which is why the do_user_config + # method is defined on the base plugin class + do_user_config = base_plugin_object.do_user_config + + # 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 = DemoDialog(self.gui, self.qaction.icon(), do_user_config) + d.show() + + def apply_settings(self): + from calibre_plugins.fanfictiondownloader_plugin.config import prefs + # In an actual non trivial plugin, you would probably need to + # do something based on the settings in prefs + prefs + From a9fa06fd4d3f3c1676c15a47501d87328a3362ae Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 16 Dec 2011 11:29:36 -0600 Subject: [PATCH 03/30] Collect URL, first 'useful' version. Borrowing from GPL v3 code, so not Apache License. --- calibre-plugin/__init__.py | 2 +- calibre-plugin/config.py | 2 +- calibre-plugin/plugin.py | 95 +++++++++++++++++++++----------------- calibre-plugin/ui.py | 2 +- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 544fa3a..73485ed 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -__license__ = 'Apache License' +__license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index f36cb71..95374a8 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -__license__ = 'Apache License' +__license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' diff --git a/calibre-plugin/plugin.py b/calibre-plugin/plugin.py index 52ddd7a..d7395c5 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/plugin.py @@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -__license__ = 'Apache License' +__license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' @@ -15,7 +15,7 @@ if False: from StringIO import StringIO -from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel +from PyQt4.Qt import (QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel, QLineEdit) from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata.epub import get_metadata @@ -52,18 +52,26 @@ class DemoDialog(QDialog): self.about_button.clicked.connect(self.about) self.l.addWidget(self.about_button) - self.marked_button = QPushButton( - 'Show books with only one format in the calibre GUI', self) - self.marked_button.clicked.connect(self.marked) - self.l.addWidget(self.marked_button) + # self.marked_button = QPushButton( + # 'Show books with only one format in the calibre GUI', self) + # self.marked_button.clicked.connect(self.marked) + # self.l.addWidget(self.marked_button) - self.view_button = QPushButton( - 'View the most recently added book', self) - self.view_button.clicked.connect(self.view) - self.l.addWidget(self.view_button) + # self.view_button = QPushButton( + # 'View the most recently added book', self) + # self.view_button.clicked.connect(self.view) + # self.l.addWidget(self.view_button) + + self.label = QLabel('Story URL:') + self.l.addWidget(self.label) + + self.msg = QLineEdit(self) +# self.msg.setText(prefs['hello_world_msg']) + self.l.addWidget(self.msg) + self.label.setBuddy(self.msg) self.ffdl_button = QPushButton( - 'Attempt FFDL', self) + 'Download Story', self) self.ffdl_button.clicked.connect(self.ffdl) self.l.addWidget(self.ffdl_button) @@ -88,27 +96,29 @@ class DemoDialog(QDialog): QMessageBox.about(self, 'About the Interface Plugin Demo', text.decode('utf-8')) - def marked(self): - fmt_idx = self.db.FIELD_MAP['formats'] - matched_ids = set() - for record in self.db.data.iterall(): - # Iterate over all records - fmts = record[fmt_idx] - # fmts is either None or a comma separated list of formats - if fmts and ',' not in fmts: - matched_ids.add(record[0]) - # Mark the records with the matching ids - self.db.set_marked_ids(matched_ids) + # def marked(self): + # fmt_idx = self.db.FIELD_MAP['formats'] + # matched_ids = set() + # for record in self.db.data.iterall(): + # # Iterate over all records + # fmts = record[fmt_idx] + # # fmts is either None or a comma separated list of formats + # if fmts and ',' not in fmts: + # matched_ids.add(record[0]) + # # Mark the records with the matching ids + # self.db.set_marked_ids(matched_ids) - # Tell the GUI to search for all marked records - self.gui.search.setEditText('marked:true') - self.gui.search.do_search() + # # Tell the GUI to search for all marked records + # self.gui.search.setEditText('marked:true') + # self.gui.search.do_search() def ffdl(self): config = ConfigParser.SafeConfigParser() config.readfp(StringIO(get_resources("defaults.ini"))) - adapter = adapters.getAdapter(config,"http://test1.com?sid=6646") # http://www.fanfiction.net/s/6439390/1/All_Hallows_Eve") # + print("URL:"+unicode(self.msg.text())) + adapter = adapters.getAdapter(config,unicode(self.msg.text())) + # "http://test1.com?sid=6646") # http://www.fanfiction.net/s/6439390/1/All_Hallows_Eve") # writer = writers.getWriter("epub",config,adapter) tmp = PersistentTemporaryFile(".epub") @@ -116,27 +126,28 @@ class DemoDialog(QDialog): print("tmp: "+tmp.name) mi = get_metadata(tmp,extract_cover=False) self.db.add_books([tmp],["EPUB"],[mi]) + self.hide() QMessageBox.about(self, 'FFDL Metadata', str(adapter.getStoryMetadataOnly()).decode('utf-8')) - def view(self): - most_recent = most_recent_id = None - timestamp_idx = self.db.FIELD_MAP['timestamp'] + # def view(self): + # most_recent = most_recent_id = None + # timestamp_idx = self.db.FIELD_MAP['timestamp'] - for record in self.db.data: - # Iterate over all currently showing records - timestamp = record[timestamp_idx] - if most_recent is None or timestamp > most_recent: - most_recent = timestamp - most_recent_id = record[0] + # for record in self.db.data: + # # Iterate over all currently showing records + # timestamp = record[timestamp_idx] + # if most_recent is None or timestamp > most_recent: + # most_recent = timestamp + # most_recent_id = record[0] - if most_recent_id is not None: - # Get the row number of the id as shown in the GUI - row_number = self.db.row(most_recent_id) - # Get a reference to the View plugin - view_plugin = self.gui.iactions['View'] - # Ask the view plugin to launch the viewer for row_number - view_plugin._view_books([row_number]) + # if most_recent_id is not None: + # # Get the row number of the id as shown in the GUI + # row_number = self.db.row(most_recent_id) + # # Get a reference to the View plugin + # view_plugin = self.gui.iactions['View'] + # # Ask the view plugin to launch the viewer for row_number + # view_plugin._view_books([row_number]) def config(self): self.do_user_config(parent=self) diff --git a/calibre-plugin/ui.py b/calibre-plugin/ui.py index d978581..e2a8899 100644 --- a/calibre-plugin/ui.py +++ b/calibre-plugin/ui.py @@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -__license__ = 'Apache License' +__license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' From 5511b4fd28161690746fbec45866e30f1814f92b Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 16 Dec 2011 23:01:18 -0600 Subject: [PATCH 04/30] Store/use personal.ini, is_adult and user/pass dialogs when needed. --- calibre-plugin/about.txt | 5 +- calibre-plugin/config.py | 11 ++- calibre-plugin/plugin.py | 80 +++++++++++++++++++--- fanficdownloader/adapters/adapter_test1.py | 2 +- 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/calibre-plugin/about.txt b/calibre-plugin/about.txt index ae4d602..cb9e328 100644 --- a/calibre-plugin/about.txt +++ b/calibre-plugin/about.txt @@ -1,8 +1,9 @@ FanFictionDownLoader Plugin Demo =========================== -Created by Jim Miller, borrowing heavily from Kovid Goyal's 'The -InterfacePlugin Demo' +http://code.google.com/p/fanficdownloader/ + +Created by Jim Miller, borrowing heavily from Kovid Goyal's 'The InterfacePlugin Demo' Requires calibre >= 0.7.53 diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 95374a8..ce30df1 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit +from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit, QTextEdit from calibre.utils.config import JSONConfig @@ -20,12 +20,13 @@ prefs = JSONConfig('plugins/fanfictiondownloader_plugin') # Set defaults prefs.defaults['hello_world_msg'] = 'Hello, World!' +prefs.defaults['personal.ini'] = get_resources('example.ini') class ConfigWidget(QWidget): def __init__(self): QWidget.__init__(self) - self.l = QHBoxLayout() + self.l = QVBoxLayout() self.setLayout(self.l) self.label = QLabel('Hello world &message:') @@ -36,6 +37,12 @@ class ConfigWidget(QWidget): self.l.addWidget(self.msg) self.label.setBuddy(self.msg) + self.ini = QTextEdit(self) + self.ini.setLineWrapMode(QTextEdit.NoWrap) + self.ini.setText(prefs['personal.ini']) + self.l.addWidget(self.ini) + def save_settings(self): prefs['hello_world_msg'] = unicode(self.msg.text()) + prefs['personal.ini'] = unicode(self.ini.toPlainText()) diff --git a/calibre-plugin/plugin.py b/calibre-plugin/plugin.py index d7395c5..3ff5d0e 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/plugin.py @@ -15,7 +15,8 @@ if False: from StringIO import StringIO -from PyQt4.Qt import (QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel, QLineEdit) +from PyQt4.Qt import (QDialog, QVBoxLayout, QGridLayout, QPushButton, QMessageBox, + QLabel, QLineEdit, QInputDialog ) from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata.epub import get_metadata @@ -62,13 +63,12 @@ class DemoDialog(QDialog): # self.view_button.clicked.connect(self.view) # self.l.addWidget(self.view_button) - self.label = QLabel('Story URL:') - self.l.addWidget(self.label) + self.l.addWidget(QLabel('Story &URL:')) - self.msg = QLineEdit(self) -# self.msg.setText(prefs['hello_world_msg']) - self.l.addWidget(self.msg) - self.label.setBuddy(self.msg) + self.url = QLineEdit(self) + #self.url.setText('http://test1.com?sid=668') + self.l.addWidget(self.url) + self.label.setBuddy(self.url) self.ffdl_button = QPushButton( 'Download Story', self) @@ -116,14 +116,36 @@ class DemoDialog(QDialog): config = ConfigParser.SafeConfigParser() config.readfp(StringIO(get_resources("defaults.ini"))) - print("URL:"+unicode(self.msg.text())) - adapter = adapters.getAdapter(config,unicode(self.msg.text())) + config.readfp(StringIO(prefs['personal.ini'])) + print("URL:"+unicode(self.url.text())) + adapter = adapters.getAdapter(config,unicode(self.url.text())) # "http://test1.com?sid=6646") # http://www.fanfiction.net/s/6439390/1/All_Hallows_Eve") # + try: + adapter.getStoryMetadataOnly() + except exceptions.FailedToLogin: + print("Login Failed, Need Username/Password.") + userpass = UserPassDialog(self.gui) + userpass.exec_() # exec_ will make it act modal + if userpass.status: + adapter.username = userpass.user.text() + adapter.password = userpass.passwd.text() + except exceptions.AdultCheckRequired: + adult = QMessageBox.warning(self, 'Are You Adult?', + "This story requires that you be an adult. Please confirm you are an adult in your locale:", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + + if adult == QMessageBox.Yes: + adapter.is_adult=True + + adapter.getStoryMetadataOnly() + writer = writers.getWriter("epub",config,adapter) tmp = PersistentTemporaryFile(".epub") - writer.writeStory(tmp) print("tmp: "+tmp.name) + + writer.writeStory(tmp) mi = get_metadata(tmp,extract_cover=False) self.db.add_books([tmp],["EPUB"],[mi]) self.hide() @@ -154,3 +176,41 @@ class DemoDialog(QDialog): # Apply the changes self.label.setText(prefs['hello_world_msg']) +class UserPassDialog(QDialog): + + def __init__(self, gui): + QDialog.__init__(self, gui) + self.gui = gui + self.status=False + self.setWindowTitle('User/Password') + + self.l = QGridLayout() + self.setLayout(self.l) + + self.l.addWidget(QLabel("This site/story requires you to login."),0,0,1,2) + + self.l.addWidget(QLabel("User:"),1,0) + self.user = QLineEdit(self) + self.l.addWidget(self.user,1,1) + + self.l.addWidget(QLabel("Password:"),2,0) + self.passwd = QLineEdit(self) + self.l.addWidget(self.passwd,2,1) + + self.ok_button = QPushButton('OK', self) + self.ok_button.clicked.connect(self.ok) + self.l.addWidget(self.ok_button,3,0) + + self.cancel_button = QPushButton('Cancel', self) + self.cancel_button.clicked.connect(self.cancel) + self.l.addWidget(self.cancel_button,3,1) + + self.resize(self.sizeHint()) + + def ok(self): + self.status=True + self.hide() + + def cancel(self): + self.status=False + self.hide() diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index 05f6ee5..699d690 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -63,7 +63,7 @@ class TestSiteAdapter(BaseSiteAdapter): if self.story.getMetadata('storyId') == '664': self.story.setMetadata(u'title',"Test Story Title "+self.crazystring) else: - self.story.setMetadata(u'title',"Test Story Title") + self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId')) self.story.setMetadata('storyUrl',self.url) self.story.setMetadata('description',u'Description '+self.crazystring+u''' Done From c0ea637aeffa4563466c1f0e5f454fa90e35e78f Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sat, 17 Dec 2011 14:56:46 -0600 Subject: [PATCH 05/30] Fill calibre metadata directly(vs read from epub), allow other output formats. --- calibre-plugin/config.py | 17 ++-- calibre-plugin/plugin.py | 123 ++++++++++-------------- defaults.ini | 8 +- fanficdownloader/writers/base_writer.py | 19 ++++ fanficdownloader/writers/writer_epub.py | 17 +--- fanficdownloader/writers/writer_mobi.py | 3 - 6 files changed, 81 insertions(+), 106 deletions(-) diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index ce30df1..3b7a50c 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -19,7 +19,6 @@ from calibre.utils.config import JSONConfig prefs = JSONConfig('plugins/fanfictiondownloader_plugin') # Set defaults -prefs.defaults['hello_world_msg'] = 'Hello, World!' prefs.defaults['personal.ini'] = get_resources('example.ini') class ConfigWidget(QWidget): @@ -29,20 +28,20 @@ class ConfigWidget(QWidget): self.l = QVBoxLayout() self.setLayout(self.l) - self.label = QLabel('Hello world &message:') + self.label = QLabel('personal.ini:') self.l.addWidget(self.label) - self.msg = QLineEdit(self) - self.msg.setText(prefs['hello_world_msg']) - self.l.addWidget(self.msg) - self.label.setBuddy(self.msg) - self.ini = QTextEdit(self) self.ini.setLineWrapMode(QTextEdit.NoWrap) self.ini.setText(prefs['personal.ini']) self.l.addWidget(self.ini) def save_settings(self): - prefs['hello_world_msg'] = unicode(self.msg.text()) - prefs['personal.ini'] = unicode(self.ini.toPlainText()) + ini = unicode(self.ini.toPlainText()) + if ini: + prefs['personal.ini'] = ini + else: + # if they've removed everything, clear it so they get the + # default next time. + del prefs['personal.ini'] diff --git a/calibre-plugin/plugin.py b/calibre-plugin/plugin.py index 3ff5d0e..8cfa984 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/plugin.py @@ -7,22 +7,15 @@ __license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' -if False: - # This is here to keep my python error checker from complaining about - # the builtin functions that will be defined by the plugin loading system - # You do not need this code in your plugins - get_icons = get_resources = None - from StringIO import StringIO from PyQt4.Qt import (QDialog, QVBoxLayout, QGridLayout, QPushButton, QMessageBox, - QLabel, QLineEdit, QInputDialog ) -from calibre.ptempfile import PersistentTemporaryFile + QLabel, QLineEdit, QInputDialog, QComboBox ) -from calibre.ebooks.metadata.epub import get_metadata +from calibre.ptempfile import PersistentTemporaryFile +from calibre.ebooks.metadata import MetaInformation from calibre_plugins.fanfictiondownloader_plugin.config import prefs - from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions import ConfigParser @@ -43,32 +36,21 @@ class DemoDialog(QDialog): self.l = QVBoxLayout() self.setLayout(self.l) - self.label = QLabel(prefs['hello_world_msg']) - self.l.addWidget(self.label) - self.setWindowTitle('FanFictionDownLoader') self.setWindowIcon(icon) - self.about_button = QPushButton('About', self) - self.about_button.clicked.connect(self.about) - self.l.addWidget(self.about_button) - - # self.marked_button = QPushButton( - # 'Show books with only one format in the calibre GUI', self) - # self.marked_button.clicked.connect(self.marked) - # self.l.addWidget(self.marked_button) - - # self.view_button = QPushButton( - # 'View the most recently added book', self) - # self.view_button.clicked.connect(self.view) - # self.l.addWidget(self.view_button) - - self.l.addWidget(QLabel('Story &URL:')) - + self.l.addWidget(QLabel('Story URL:')) self.url = QLineEdit(self) - #self.url.setText('http://test1.com?sid=668') + self.url.setText('http://test1.com?sid=12345') self.l.addWidget(self.url) - self.label.setBuddy(self.url) + + self.l.addWidget(QLabel('Output Format:')) + self.format = QComboBox(self) + self.format.addItem('epub') + self.format.addItem('mobi') + self.format.addItem('html') + self.format.addItem('txt') + self.l.addWidget(self.format) self.ffdl_button = QPushButton( 'Download Story', self) @@ -80,6 +62,10 @@ class DemoDialog(QDialog): self.conf_button.clicked.connect(self.config) self.l.addWidget(self.conf_button) + self.about_button = QPushButton('About', self) + self.about_button.clicked.connect(self.about) + self.l.addWidget(self.about_button) + self.resize(self.sizeHint()) def about(self): @@ -96,22 +82,6 @@ class DemoDialog(QDialog): QMessageBox.about(self, 'About the Interface Plugin Demo', text.decode('utf-8')) - # def marked(self): - # fmt_idx = self.db.FIELD_MAP['formats'] - # matched_ids = set() - # for record in self.db.data.iterall(): - # # Iterate over all records - # fmts = record[fmt_idx] - # # fmts is either None or a comma separated list of formats - # if fmts and ',' not in fmts: - # matched_ids.add(record[0]) - # # Mark the records with the matching ids - # self.db.set_marked_ids(matched_ids) - - # # Tell the GUI to search for all marked records - # self.gui.search.setEditText('marked:true') - # self.gui.search.do_search() - def ffdl(self): config = ConfigParser.SafeConfigParser() @@ -119,7 +89,6 @@ class DemoDialog(QDialog): config.readfp(StringIO(prefs['personal.ini'])) print("URL:"+unicode(self.url.text())) adapter = adapters.getAdapter(config,unicode(self.url.text())) - # "http://test1.com?sid=6646") # http://www.fanfiction.net/s/6439390/1/All_Hallows_Eve") # try: adapter.getStoryMetadataOnly() @@ -130,6 +99,9 @@ class DemoDialog(QDialog): if userpass.status: adapter.username = userpass.user.text() adapter.password = userpass.passwd.text() + else: + del adapter + return except exceptions.AdultCheckRequired: adult = QMessageBox.warning(self, 'Are You Adult?', "This story requires that you be an adult. Please confirm you are an adult in your locale:", @@ -138,43 +110,46 @@ class DemoDialog(QDialog): if adult == QMessageBox.Yes: adapter.is_adult=True + else: + del adapter + return +# except exceptions.StoryDoesNotExist - adapter.getStoryMetadataOnly() - - writer = writers.getWriter("epub",config,adapter) - tmp = PersistentTemporaryFile(".epub") + story = adapter.getStoryMetadataOnly() + fileform = unicode(self.format.currentText()) + writer = writers.getWriter(fileform,config,adapter) + tmp = PersistentTemporaryFile("."+fileform) print("tmp: "+tmp.name) writer.writeStory(tmp) - mi = get_metadata(tmp,extract_cover=False) - self.db.add_books([tmp],["EPUB"],[mi]) + + mi = MetaInformation(story.getMetadata("title"), + (story.getMetadata("author"),)) # author is a list. + 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") + + self.db.add_books([tmp],[fileform],[mi]) self.hide() - QMessageBox.about(self, 'FFDL Metadata', - str(adapter.getStoryMetadataOnly()).decode('utf-8')) - # def view(self): - # most_recent = most_recent_id = None - # timestamp_idx = self.db.FIELD_MAP['timestamp'] + # Otherwise list of books doesn't update right away. + self.gui.library_view.model().books_added(1) + + # QMessageBox.about(self, 'FFDL Metadata', + # str(adapter.getStoryMetadataOnly()).decode('utf-8')) + del adapter + del writer - # for record in self.db.data: - # # Iterate over all currently showing records - # timestamp = record[timestamp_idx] - # if most_recent is None or timestamp > most_recent: - # most_recent = timestamp - # most_recent_id = record[0] - - # if most_recent_id is not None: - # # Get the row number of the id as shown in the GUI - # row_number = self.db.row(most_recent_id) - # # Get a reference to the View plugin - # view_plugin = self.gui.iactions['View'] - # # Ask the view plugin to launch the viewer for row_number - # view_plugin._view_books([row_number]) def config(self): self.do_user_config(parent=self) # Apply the changes - self.label.setText(prefs['hello_world_msg']) + #self.label.setText(prefs['hello_world_msg']) class UserPassDialog(QDialog): diff --git a/defaults.ini b/defaults.ini index 6da575a..f791b51 100644 --- a/defaults.ini +++ b/defaults.ini @@ -105,6 +105,10 @@ zip_filename: ${title}-${siteabbrev}_${storyId}${formatext}.zip ## zip_filename. allow_unsafe_filename: false +## entries to make epub subjects and calibre tags +## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d" +include_subject_tags: extratags, genre, category, characters, lastupdate, status + ## extra tags (comma separated) to include, primarily for epub. extratags: FanFiction @@ -137,10 +141,6 @@ windows_eol: true ## epub is already a zip file. zip_output: false -## entries to make epub subject tags -## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d" -include_subject_tags: extratags, genre, category, characters, lastupdate, status - ## epub carries the TOC in metadata. ## mobi generated from epub will have a TOC at the end. include_tocpage: false diff --git a/fanficdownloader/writers/base_writer.py b/fanficdownloader/writers/base_writer.py index ad476ce..e01cadf 100644 --- a/fanficdownloader/writers/base_writer.py +++ b/fanficdownloader/writers/base_writer.py @@ -102,6 +102,9 @@ class BaseStoryWriter(Configurable): self.story.setMetadata('formatname',self.getFormatName()) self.story.setMetadata('formatext',self.getFormatExt()) + def getMetadata(self,key): + return stripHTML(self.story.getMetadata(key)) + def getOutputFileName(self): if self.getConfig('zip_output'): return self.getZipFileName() @@ -242,6 +245,22 @@ class BaseStoryWriter(Configurable): if close: outstream.close() + def getTags(self): + # set to avoid duplicates subject tags. + subjectset = set() + for entry in self.validEntries: + if entry in self.getConfigList("include_subject_tags") and \ + entry not in self.story.getLists() and \ + self.story.getMetadata(entry): + subjectset.add(self.getMetadata(entry)) + # listables all go into dc:subject tags, but only if they are configured. + for (name,lst) in self.story.getLists().iteritems(): + if name in self.getConfigList("include_subject_tags"): + for tag in lst: + subjectset.add(tag) + + return subjectset + def writeStoryImpl(self, out): "Must be overriden by sub classes." pass diff --git a/fanficdownloader/writers/writer_epub.py b/fanficdownloader/writers/writer_epub.py index 57424c1..bc16393 100644 --- a/fanficdownloader/writers/writer_epub.py +++ b/fanficdownloader/writers/writer_epub.py @@ -156,9 +156,6 @@ h6 { text-align: center; } ''') - def getMetadata(self,key): - return stripHTML(self.story.getMetadata(key)) - def writeStoryImpl(self, out): ## Python 2.5 ZipFile is rather more primative than later @@ -260,19 +257,7 @@ h6 { text-align: center; } metadata.appendChild(newTag(contentdom,"dc:description",text= self.getMetadata('description'))) - # set to avoid duplicates subject tags. - subjectset = set() - for entry in self.validEntries: - if entry in self.getConfigList("include_subject_tags") and \ - entry not in self.story.getLists() and \ - self.story.getMetadata(entry): - subjectset.add(self.getMetadata(entry)) - # listables all go into dc:subject tags, but only if they are configured. - for (name,lst) in self.story.getLists().iteritems(): - if name in self.getConfigList("include_subject_tags"): - for tag in lst: - subjectset.add(tag) - for subject in subjectset: + for subject in self.getTags(): metadata.appendChild(newTag(contentdom,"dc:subject",text=subject)) diff --git a/fanficdownloader/writers/writer_mobi.py b/fanficdownloader/writers/writer_mobi.py index 87dd30f..bb141c6 100644 --- a/fanficdownloader/writers/writer_mobi.py +++ b/fanficdownloader/writers/writer_mobi.py @@ -124,9 +124,6 @@ class MobiWriter(BaseStoryWriter): ''') - def getMetadata(self,key): - return stripHTML(self.story.getMetadata(key)) - def writeStoryImpl(self, out): files = [] From f634629b98f1fd4468b142b87560e3edc999e92e Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sun, 18 Dec 2011 19:09:08 -0600 Subject: [PATCH 06/30] Check for existing duplicate, other minor improvements. --- calibre-plugin/plugin.py | 70 +++++++++++++--------- calibre-plugin/ui.py | 4 +- fanficdownloader/adapters/adapter_test1.py | 3 + 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/calibre-plugin/plugin.py b/calibre-plugin/plugin.py index 8cfa984..78d2154 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/plugin.py @@ -9,11 +9,12 @@ __docformat__ = 'restructuredtext en' from StringIO import StringIO -from PyQt4.Qt import (QDialog, QVBoxLayout, QGridLayout, QPushButton, QMessageBox, +from PyQt4.Qt import (QDialog, QVBoxLayout, QGridLayout, QPushButton, QLabel, QLineEdit, QInputDialog, QComboBox ) from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata import MetaInformation +from calibre.gui2 import question_dialog from calibre_plugins.fanfictiondownloader_plugin.config import prefs from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions @@ -88,13 +89,14 @@ class DemoDialog(QDialog): config.readfp(StringIO(get_resources("defaults.ini"))) config.readfp(StringIO(prefs['personal.ini'])) print("URL:"+unicode(self.url.text())) + adapter = adapters.getAdapter(config,unicode(self.url.text())) try: adapter.getStoryMetadataOnly() except exceptions.FailedToLogin: print("Login Failed, Need Username/Password.") - userpass = UserPassDialog(self.gui) + userpass = UserPassDialog(self.gui,adapter.getSiteDomain()) userpass.exec_() # exec_ will make it act modal if userpass.status: adapter.username = userpass.user.text() @@ -103,12 +105,9 @@ class DemoDialog(QDialog): del adapter return except exceptions.AdultCheckRequired: - adult = QMessageBox.warning(self, 'Are You Adult?', - "This story requires that you be an adult. Please confirm you are an adult in your locale:", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No) - - if adult == QMessageBox.Yes: + if question_dialog(self.gui, 'Are You Adult?', '

'+ + "This story requires that you be an adult. Please confirm you are an adult in your locale:", + show_copy_button=False): adapter.is_adult=True else: del adapter @@ -117,33 +116,48 @@ class DemoDialog(QDialog): story = adapter.getStoryMetadataOnly() fileform = unicode(self.format.currentText()) - writer = writers.getWriter(fileform,config,adapter) - tmp = PersistentTemporaryFile("."+fileform) - print("tmp: "+tmp.name) - - writer.writeStory(tmp) - + mi = MetaInformation(story.getMetadata("title"), (story.getMetadata("author"),)) # author is a list. - 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") + add=True + identicalbooks = self.db.find_identical_books(mi) + if identicalbooks: + add=False + if question_dialog(self.gui, 'Add Duplicate?', '

'+ + "That story is already in your library. Create a new one?", + show_copy_button=False): + add=True + + if add: + writer = writers.getWriter(fileform,config,adapter) + tmp = PersistentTemporaryFile("."+fileform) + print("tmp: "+tmp.name) + + writer.writeStory(tmp) - self.db.add_books([tmp],[fileform],[mi]) - self.hide() + mi.set_identifiers({'url':story.getMetadata("storyUrl")}) + mi.publisher = story.getMetadata("site") - # Otherwise list of books doesn't update right away. - self.gui.library_view.model().books_added(1) + 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)=self.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) + + self.hide() # QMessageBox.about(self, 'FFDL Metadata', # str(adapter.getStoryMetadataOnly()).decode('utf-8')) del adapter - del writer + try: + del writer + except: + pass def config(self): @@ -153,7 +167,7 @@ class DemoDialog(QDialog): class UserPassDialog(QDialog): - def __init__(self, gui): + def __init__(self, gui, site): QDialog.__init__(self, gui) self.gui = gui self.status=False @@ -162,7 +176,7 @@ class UserPassDialog(QDialog): self.l = QGridLayout() self.setLayout(self.l) - self.l.addWidget(QLabel("This site/story requires you to login."),0,0,1,2) + self.l.addWidget(QLabel("%s requires you to login to download this story."%site),0,0,1,2) self.l.addWidget(QLabel("User:"),1,0) self.user = QLineEdit(self) diff --git a/calibre-plugin/ui.py b/calibre-plugin/ui.py index e2a8899..8b5f94c 100644 --- a/calibre-plugin/ui.py +++ b/calibre-plugin/ui.py @@ -26,8 +26,10 @@ class InterfacePlugin(InterfaceAction): # shortcut. Remember that currently calibre has no central management for # keyboard shortcuts, so try to use an unusual/unused shortcut. action_spec = ('FanFictionDownLoader', None, - 'Run the FanFictionDownLoader Plugin', None) + 'Download FanFiction stories from various web sites', None) + action_type = 'current' + def genesis(self): # This method is called once per plugin, do initial setup here diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index 699d690..2bc8668 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -54,6 +54,9 @@ class TestSiteAdapter(BaseSiteAdapter): if self.story.getMetadata('storyId') == '666': raise exceptions.StoryDoesNotExist(self.url) + if self.story.getMetadata('storyId') == '670': + time.sleep(2.0) + if self.getConfig("username"): self.username = self.getConfig("username") From cbc8cadd91bc41a848a6310d24ab933766d60095 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Mon, 19 Dec 2011 14:59:20 -0600 Subject: [PATCH 07/30] First calibre-plugin version with working background downloading. --- calibre-plugin/__init__.py | 9 ++ calibre-plugin/jobs.py | 72 +++++++++ calibre-plugin/plugin.py | 168 +++++++++------------ calibre-plugin/ui.py | 164 +++++++++++++++++++- fanficdownloader/adapters/adapter_test1.py | 4 +- fanficdownloader/adapters/base_adapter.py | 1 + 6 files changed, 309 insertions(+), 109 deletions(-) create mode 100644 calibre-plugin/jobs.py diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 73485ed..c66c7f4 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -79,3 +79,12 @@ class InterfacePluginDemo(InterfaceActionBase): 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 + app = QApplication([]) + test_widget('Advanced', 'Plugins') diff --git a/calibre-plugin/jobs.py b/calibre-plugin/jobs.py new file mode 100644 index 0000000..30c92ae --- /dev/null +++ b/calibre-plugin/jobs.py @@ -0,0 +1,72 @@ +#!/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 78d2154..b171af1 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/plugin.py @@ -1,38 +1,26 @@ #!/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, +from __future__ import (unicode_literals, division, print_function) __license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' -from StringIO import StringIO +from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QGridLayout, QPushButton, QProgressDialog, QString, + QLabel, QLineEdit, QInputDialog, QComboBox, QProgressDialog, QTimer ) -from PyQt4.Qt import (QDialog, QVBoxLayout, QGridLayout, QPushButton, - QLabel, QLineEdit, QInputDialog, QComboBox ) +from calibre.gui2 import error_dialog, warning_dialog, question_dialog -from calibre.ptempfile import PersistentTemporaryFile -from calibre.ebooks.metadata import MetaInformation -from calibre.gui2 import question_dialog - -from calibre_plugins.fanfictiondownloader_plugin.config import prefs from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions -import ConfigParser +class DownloadDialog(QDialog): -class DemoDialog(QDialog): - - def __init__(self, gui, icon, do_user_config): + def __init__(self, gui, icon, do_user_config, pluginaction): QDialog.__init__(self, gui) self.gui = gui self.do_user_config = do_user_config - - # The current database shown in the GUI - # db is an instance of the class LibraryDatabase2 from database.py - # This class has many, many methods that allow you to do a lot of - # things. - self.db = gui.current_db + self.pluginaction = pluginaction self.l = QVBoxLayout() self.setLayout(self.l) @@ -46,12 +34,12 @@ class DemoDialog(QDialog): self.l.addWidget(self.url) self.l.addWidget(QLabel('Output Format:')) - self.format = QComboBox(self) - self.format.addItem('epub') - self.format.addItem('mobi') - self.format.addItem('html') - self.format.addItem('txt') - self.l.addWidget(self.format) + self.fileform = QComboBox(self) + self.fileform.addItem('epub') + self.fileform.addItem('mobi') + self.fileform.addItem('html') + self.fileform.addItem('txt') + self.l.addWidget(self.fileform) self.ffdl_button = QPushButton( 'Download Story', self) @@ -84,81 +72,9 @@ class DemoDialog(QDialog): text.decode('utf-8')) def ffdl(self): - - config = ConfigParser.SafeConfigParser() - config.readfp(StringIO(get_resources("defaults.ini"))) - config.readfp(StringIO(prefs['personal.ini'])) - print("URL:"+unicode(self.url.text())) - - adapter = adapters.getAdapter(config,unicode(self.url.text())) - - try: - adapter.getStoryMetadataOnly() - except exceptions.FailedToLogin: - print("Login Failed, Need Username/Password.") - userpass = UserPassDialog(self.gui,adapter.getSiteDomain()) - userpass.exec_() # exec_ will make it act modal - if userpass.status: - adapter.username = userpass.user.text() - adapter.password = userpass.passwd.text() - else: - del adapter - return - except exceptions.AdultCheckRequired: - if question_dialog(self.gui, 'Are You Adult?', '

'+ - "This story requires that you be an adult. Please confirm you are an adult in your locale:", - show_copy_button=False): - adapter.is_adult=True - else: - del adapter - return -# except exceptions.StoryDoesNotExist - - story = adapter.getStoryMetadataOnly() - fileform = unicode(self.format.currentText()) - - mi = MetaInformation(story.getMetadata("title"), - (story.getMetadata("author"),)) # author is a list. - - add=True - identicalbooks = self.db.find_identical_books(mi) - if identicalbooks: - add=False - if question_dialog(self.gui, 'Add Duplicate?', '

'+ - "That story is already in your library. Create a new one?", - show_copy_button=False): - add=True - - if add: - writer = writers.getWriter(fileform,config,adapter) - tmp = PersistentTemporaryFile("."+fileform) - print("tmp: "+tmp.name) - - writer.writeStory(tmp) - - 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)=self.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) - + self.pluginaction.start_downloads(unicode(self.url.text()), + unicode(self.fileform.currentText())) self.hide() - - # QMessageBox.about(self, 'FFDL Metadata', - # str(adapter.getStoryMetadataOnly()).decode('utf-8')) - del adapter - try: - del writer - except: - pass - def config(self): self.do_user_config(parent=self) @@ -203,3 +119,57 @@ class UserPassDialog(QDialog): def cancel(self): self.status=False self.hide() + +class QueueProgressDialog(QProgressDialog): + + def __init__(self, gui, title, loop_list, fileform, loop_function, enqueue_function, db): + QProgressDialog.__init__(self, title, QString(), 0, len(loop_list), gui) + self.setWindowTitle(title) + self.setMinimumWidth(500) + self.gui = gui + 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.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 do_loop(self): + current = self.loop_list[self.i] + self.i += 1 + + try: + retval = self.loop_function(current,self.fileform) + self.loop_good.append((current,retval)) + except Exception as e: + self.loop_bad.append(current) + + 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() + else: + QTimer.singleShot(0, self.do_loop) + + def do_queue(self): + self.hide() + if self.loop_bad != []: + res = [] + for current in self.loop_bad: + res.append('%s'%current) + 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.') % + (len(self.loop_bad), len(self.loop_list)), + msg).exec_() + self.gui = None + # Queue a job to process these ePub/Mobi books + self.enqueue_function(self.loop_good,self.fileform) diff --git a/calibre-plugin/ui.py b/calibre-plugin/ui.py index 8b5f94c..c5dca7c 100644 --- a/calibre-plugin/ui.py +++ b/calibre-plugin/ui.py @@ -7,15 +7,23 @@ __license__ = 'GPL v3' __copyright__ = '2011, Fanficdownloader team' __docformat__ = 'restructuredtext en' -if False: - # This is here to keep my python error checker from complaining about - # the builtin functions that will be defined by the plugin loading system - # You do not need this code in your plugins - get_icons = get_resources = None +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_plugins.fanfictiondownloader_plugin.plugin import DemoDialog +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_plugins.fanfictiondownloader_plugin.config import prefs +from calibre_plugins.fanfictiondownloader_plugin.plugin import (DownloadDialog, QueueProgressDialog,UserPassDialog) + +from calibre_plugins.fanfictiondownloader_plugin.jobs import do_story_downloads class InterfacePlugin(InterfaceAction): @@ -59,15 +67,155 @@ class InterfacePlugin(InterfaceAction): # method is defined on the base plugin class do_user_config = base_plugin_object.do_user_config + # The current database shown in the GUI + # db is an instance of the class LibraryDatabase2 from database.py + # This class has many, many methods that allow you to do a lot of + # things. + self.db = self.gui.current_db + # 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 = DemoDialog(self.gui, self.qaction.icon(), do_user_config) + d = DownloadDialog(self.gui, self.qaction.icon(), do_user_config, self) d.show() def apply_settings(self): - from calibre_plugins.fanfictiondownloader_plugin.config import prefs # In an actual non trivial plugin, you would probably need to # do something based on the settings in prefs prefs + + def start_downloads(self,url,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" + ] + + self.fetchmeta_qpd = \ + QueueProgressDialog(self.gui, + "Getting Metadata for Stories", + url_list, + fileform, + self.get_adapter_for_story, + self.enqueue_story_list_for_download, + 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 + pops dialogs for is adult, user/pass, duplicate + ''' + + print("URL:"+url) + adapter = adapters.getAdapter(self.ffdlconfig,url) + + try: + adapter.getStoryMetadataOnly() + except exceptions.FailedToLogin: + print("Login Failed, Need Username/Password.") + userpass = UserPassDialog(self.gui,url) + userpass.exec_() # exec_ will make it act modal + if userpass.status: + adapter.username = userpass.user.text() + adapter.password = userpass.passwd.text() + # else: + # del adapter + # return + except exceptions.AdultCheckRequired: + if question_dialog(self.gui, 'Are You Adult?', '

'+ + "%s requires that you be an adult. Please confirm you are an adult in your locale:"%url, + show_copy_button=False): + adapter.is_adult=True + # else: + # del adapter + # return + + # let exceptions percolate up. + story = adapter.getStoryMetadataOnly() + + mi = MetaInformation(story.getMetadata("title"), + (story.getMetadata("author"),)) # author is a list. + + add=True + identicalbooks = self.db.find_identical_books(mi) + if identicalbooks: + add=False + if question_dialog(self.gui, 'Add Duplicate?', '

'+ + "That story is already in your library. Create a new one?", + show_copy_button=False): + add=True + + if add: + return adapter + else: + return None + + def enqueue_story_list_for_download(self,adaptertuple_list,fileform): + ''' + Called by QueueProgressDialog to enqueue story downloads for BG processing. + adapter_list is a list of tuples of (url,adapter) + ''' + print("enqueue_story_list_for_download") + print(adaptertuple_list) + + func = 'arbitrary_n' + args = ['calibre_plugins.fanfictiondownloader_plugin.jobs', 'do_story_downloads', + (adaptertuple_list, fileform)] # adaptertuple_list + desc = 'Download FanFiction Stories' + + print("pre self.gui.job_manager.run_job") + # job = self.gui.job_manager.run_job( + # self.Dispatcher(self._get_stories_completed), func, args=args, + # description=desc) + + job = ThreadedJob('FanFictionDownload', + 'Downloading FanFiction Stories', + func=do_story_downloads, + args=(adaptertuple_list, fileform, self.db), + kwargs={}, + callback=self._get_stories_completed) + + self.gui.job_manager.run_threaded_job(job) + print("post self.gui.job_manager.run_job") + # job.tdir = tdir + # job.pages_custom_column = pages_custom_column + # job.words_custom_column = words_custom_column + self.gui.status_bar.show_message('Downloading %d stories'%len(adaptertuple_list)) + + def _get_stories_completed(self, job): + print("_get_stories_completed") + # remove_dir(job.tdir) + if job.failed: + return self.gui.job_exception(job, dialog_title='Failed to download stories') +# self.gui.status_bar.show_message('Download Stories completed', 3000) + # not really, but leave it for now. + book_pages_map, book_words_map = job.result + + # if len(book_pages_map) + len(book_words_map) == 0: + # # Must have been some sort of error in processing this book + # msg = 'Failed to generate any statistics. View Log for details' + # p = ErrorNotification(job.details, 'Count log', 'Count Pages failed', msg, + # show_copy_button=False, parent=self.gui) + # else: + # payload = (book_pages_map, job.pages_custom_column, book_words_map, job.words_custom_column) + # all_ids = set(book_pages_map.keys()) | set(book_words_map.keys()) + # msg = '

Count Pages plugin found %d statistics(s). ' % len(all_ids) + \ + # 'Proceed with updating your library?' + # p = ProceedNotification(self._update_database_columns, + # payload, job.details, + # 'Count log', 'Count complete', msg, + # show_copy_button=False, parent=self.gui) + # p.show() + #info_dialog(self.gui,'FFDL Complete','It worked?') diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index 2bc8668..f498cc0 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -54,7 +54,7 @@ class TestSiteAdapter(BaseSiteAdapter): if self.story.getMetadata('storyId') == '666': raise exceptions.StoryDoesNotExist(self.url) - if self.story.getMetadata('storyId') == '670': + if self.story.getMetadata('storyId').startswith('670'): time.sleep(2.0) if self.getConfig("username"): @@ -131,7 +131,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!" if self.story.getMetadata('storyId') == '667': raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url) - if self.story.getMetadata('storyId') == '670': + if self.story.getMetadata('storyId').startswith('670'): time.sleep(2.0) if "chapter=1" in url : diff --git a/fanficdownloader/adapters/base_adapter.py b/fanficdownloader/adapters/base_adapter.py index 6c9d3a1..4a4642e 100644 --- a/fanficdownloader/adapters/base_adapter.py +++ b/fanficdownloader/adapters/base_adapter.py @@ -63,6 +63,7 @@ class BaseSiteAdapter(Configurable): return re.match(self.getSiteURLPattern(), self.url) def __init__(self, config, url): + self.config = config Configurable.__init__(self, config) self.addConfigSection(self.getSiteDomain()) self.addConfigSection("overrides") From b75c9d49462148a5d737920cff0c9860b3c3a7df Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Mon, 19 Dec 2011 18:04:47 -0600 Subject: [PATCH 08/30] Refactoring and cleaning up. --- calibre-plugin/__init__.py | 10 ++-- calibre-plugin/about.txt | 6 +- calibre-plugin/config.py | 2 +- calibre-plugin/jobs.py | 72 ---------------------- calibre-plugin/plugin.py | 79 +++++++++++++----------- calibre-plugin/ui.py | 120 +++++++++++++++++++++++++++---------- 6 files changed, 143 insertions(+), 146 deletions(-) delete mode 100644 calibre-plugin/jobs.py 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 + + From 56ac9127063cb0be91d7c76d448a7b3ad17646e1 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Mon, 19 Dec 2011 21:18:55 -0600 Subject: [PATCH 09/30] Rename some files, further improvements--just about usable for new stories only. --- calibre-plugin/__init__.py | 10 +- calibre-plugin/{plugin.py => dialogs.py} | 66 +++++++--- calibre-plugin/{ui.py => ffdl_plugin.py} | 138 +++++++++------------ fanficdownloader/adapters/adapter_test1.py | 12 +- 4 files changed, 122 insertions(+), 104 deletions(-) rename calibre-plugin/{plugin.py => dialogs.py} (75%) rename calibre-plugin/{ui.py => ffdl_plugin.py} (72%) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 66899cb..5ae0bde 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -13,10 +13,10 @@ from calibre.customize import InterfaceActionBase class InterfacePluginDemo(InterfaceActionBase): ''' - This class is a simple wrapper that provides information about the actual - plugin class. The actual interface plugin class is called InterfacePlugin - and is defined in the ui.py file, as specified in the actual_plugin field - below. + This class is a simple wrapper that provides information about the + actual plugin class. The actual interface plugin class is called + InterfacePlugin and is defined in the ffdl_plugin.py file, as + specified in the actual_plugin field below. The reason for having two classes is that it allows the command line calibre utilities to run without needing to load the GUI libraries. @@ -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:FanFictionDownLoaderPlugin' + actual_plugin = 'calibre_plugins.fanfictiondownloader_plugin.ffdl_plugin:FanFictionDownLoaderPlugin' def is_customizable(self): ''' diff --git a/calibre-plugin/plugin.py b/calibre-plugin/dialogs.py similarity index 75% rename from calibre-plugin/plugin.py rename to calibre-plugin/dialogs.py index 2e956a2..b68f5b7 100644 --- a/calibre-plugin/plugin.py +++ b/calibre-plugin/dialogs.py @@ -12,7 +12,7 @@ from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QGridLayout, QTextEdit, QLineEdit, QInputDialog, QComboBox, QProgressDialog, QTimer ) -from calibre.gui2 import error_dialog, warning_dialog, question_dialog +from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions @@ -34,7 +34,17 @@ class DownloadDialog(QDialog): 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.url.setText('''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 +# http://test1.com?sid=6707 +# http://test1.com?sid=6708 +# http://test1.com?sid=6709 +# ''') self.l.addWidget(self.url) # self.url = QLineEdit(self) @@ -133,9 +143,11 @@ 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) + def __init__(self, gui, loop_list, fileform, getadapter_function, download_list_function, db): + QProgressDialog.__init__(self, + "Fetching metadata for stories...", + QString(), 0, len(loop_list), gui) + self.setWindowTitle("Downloading metadata for stories") self.setMinimumWidth(500) self.gui = gui self.db = db @@ -144,32 +156,44 @@ class MetadataProgressDialog(QProgressDialog): self.getadapter_function = getadapter_function self.download_list_function = download_list_function self.i, self.loop_bad, self.loop_good = 0, [], [] + + ## self.do_loop does QTimer.singleShot on self.do_loop also. + ## A weird way to do a loop, but that was the example I had. 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 updateStatus(self): + self.setLabelText("Fetched metadata for %d of %d"%(self.i+1,len(self.loop_list))) + self.setValue(self.i+1) + print(self.labelText()) def do_loop(self): - current = self.loop_list[self.i] + print("self.i:%d"%self.i) - self.bump() - try: - retval = self.getadapter_function(current,self.fileform) - self.loop_good.append((current,retval)) - except Exception as e: - self.loop_bad.append((current,e)) + if self.i == 0: + self.setValue(0) - if self.i >= len(self.loop_list): + if self.i >= len(self.loop_list) or self.wasCanceled(): return self.do_when_finished() + else: + current = self.loop_list[self.i] + try: + retval = self.getadapter_function(current,self.fileform) + self.loop_good.append((current,retval)) + except Exception as e: + self.loop_bad.append((current,e)) + + self.updateStatus() + self.i += 1 QTimer.singleShot(0, self.do_loop) def do_when_finished(self): self.hide() + + # Queues a job to process these ePub/Mobi books in the background. + self.download_list_function(self.loop_good,self.fileform) + if self.loop_bad != []: res = [] for j in self.loop_bad: @@ -179,6 +203,8 @@ class MetadataProgressDialog(QProgressDialog): _('Could not get metadata for %d of %d stories.') % (len(self.loop_bad), len(self.loop_list)), msg).exec_() + else: + info_dialog(self.gui, "Starting Downloads", + "Got metadata and started download for %d stories."%len(self.loop_good), + show_copy_button=False).exec_() self.gui = None - # Queue a job to process these ePub/Mobi books - self.download_list_function(self.loop_good,self.fileform) diff --git a/calibre-plugin/ui.py b/calibre-plugin/ffdl_plugin.py similarity index 72% rename from calibre-plugin/ui.py rename to calibre-plugin/ffdl_plugin.py index 478e6de..e14fa63 100644 --- a/calibre-plugin/ui.py +++ b/calibre-plugin/ffdl_plugin.py @@ -19,9 +19,7 @@ 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, MetadataProgressDialog, UserPassDialog - -#from calibre_plugins.fanfictiondownloader_plugin.jobs import do_story_downloads +from calibre_plugins.fanfictiondownloader_plugin.dialogs import DownloadDialog, MetadataProgressDialog, UserPassDialog class FanFictionDownLoaderPlugin(InterfaceAction): @@ -94,9 +92,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): 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 = urls.splitlines() + url_list = get_url_list(urls) ''' http://test1.com?sid=6700 http://test1.com?sid=6701 @@ -109,7 +105,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction): self.fetchmeta_qpd = \ MetadataProgressDialog(self.gui, - "Getting Metadata for Stories", url_list, fileform, self.get_adapter_for_story, @@ -158,7 +153,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if identicalbooks: add=False if question_dialog(self.gui, 'Add Duplicate?', '

'+ - "That story is already in your library. Create a new one?", + "%s by %s is already in your library. Create a new one?"% + (story.getMetadata("title"),story.getMetadata("author")), show_copy_button=False): add=True @@ -169,34 +165,20 @@ class FanFictionDownLoaderPlugin(InterfaceAction): def download_list(self,adaptertuple_list,fileform): ''' - Called by MetadataProgressDialog to enqueue story downloads for BG processing. + Called by MetadataProgressDialog to start story downloads BG processing. adapter_list is a list of tuples of (url,adapter) ''' - print("enqueue_story_list_for_download") - print(adaptertuple_list) + print("download_list") - func = 'arbitrary_n' - args = ['calibre_plugins.fanfictiondownloader_plugin.jobs', 'do_story_downloads', - (adaptertuple_list, fileform)] # adaptertuple_list - desc = 'Download FanFiction Stories' - - print("pre self.gui.job_manager.run_job") - # job = self.gui.job_manager.run_job( - # self.Dispatcher(self._get_stories_completed), func, args=args, - # description=desc) - job = ThreadedJob('FanFictionDownload', 'Downloading FanFiction Stories', - func=do_story_downloads, + func=self.do_story_downloads, args=(adaptertuple_list, fileform, self.db), kwargs={}, callback=self._get_stories_completed) self.gui.job_manager.run_threaded_job(job) - print("post self.gui.job_manager.run_job") - # job.tdir = tdir - # job.pages_custom_column = pages_custom_column - # job.words_custom_column = words_custom_column + self.gui.status_bar.show_message('Downloading %d stories'%len(adaptertuple_list)) def _get_stories_completed(self, job): @@ -225,57 +207,61 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # 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. + def do_story_downloads(self, adaptertuple_list, fileform, db, + **kwargs): # lambda x,y:x lambda makes small anonymous function. + # abort=None, log=None, + ''' + Master job, loop to download this list of stories + ''' + print("do_story_downloads") + notifications=kwargs['notifications'] + notifications.put((0.01, 'Start Downloading Stories')) + count = 0 + total = len(adaptertuple_list) + # Queue all the jobs + for (url,adapter) in adaptertuple_list: + self.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 {},{} - writer = writers.getWriter(fileform,adapter.config,adapter) - tmp = PersistentTemporaryFile("."+fileform) - print("tmp: "+tmp.name) + def do_story_download(self,adapter,fileform,db): + print("do_story_download") - writer.writeStory(tmp) + story = adapter.getStoryMetadataOnly() - 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 - + 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 + +def f(x): + if x.strip(): return True + else: return False + +def get_url_list(urls): + return filter(f,urls.strip().splitlines()) diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index f498cc0..ef33e42 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -55,7 +55,10 @@ class TestSiteAdapter(BaseSiteAdapter): raise exceptions.StoryDoesNotExist(self.url) if self.story.getMetadata('storyId').startswith('670'): - time.sleep(2.0) + time.sleep(1.0) + + if self.story.getMetadata('storyId').startswith('671'): + time.sleep(1.0) if self.getConfig("username"): self.username = self.getConfig("username") @@ -131,8 +134,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!" if self.story.getMetadata('storyId') == '667': raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url) - if self.story.getMetadata('storyId').startswith('670'): - time.sleep(2.0) + if self.story.getMetadata('storyId').startswith('670') or \ + self.story.getMetadata('storyId').startswith('672'): + time.sleep(1.0) if "chapter=1" in url : text=u''' @@ -146,6 +150,8 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"

http://test1.com?sid=668 - raises FailedToLogin unless username='Me'

http://test1.com?sid=669 - Succeeds with Updated Date=now

http://test1.com?sid=670 - Succeeds, but sleeps 2sec on each chapter

+

http://test1.com?sid=671 - Succeeds, but sleeps 2sec metadata only

+

http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only

And other storyId will succeed with the same output.

''' From c9a69ff3c828c7262fca97b5dc15b05505d8c391 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Tue, 20 Dec 2011 14:40:05 -0600 Subject: [PATCH 10/30] TTH won't send you future updates if you aren't 'caught up' on the story. Login isn't required for F21, but logging in will mark stories you've downloaded as 'read' on tth. --- .../adapters/adapter_tthfanficorg.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/fanficdownloader/adapters/adapter_tthfanficorg.py b/fanficdownloader/adapters/adapter_tthfanficorg.py index 4e9b0a3..35dbec8 100644 --- a/fanficdownloader/adapters/adapter_tthfanficorg.py +++ b/fanficdownloader/adapters/adapter_tthfanficorg.py @@ -34,6 +34,8 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter): self.story.setMetadata('siteabbrev','tth') self.dateformat = "%d %b %y" self.is_adult=False + self.username = None + self.password = None # get storyId from url--url validation guarantees query correct m = re.match(self.getSiteURLPattern(),url) if m: @@ -60,6 +62,48 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter): def getSiteURLPattern(self): return r"http://www.tthfanfic.org/(T-\d+/)?Story-(?P\d+)(-\d+)?(/.*)?$" + # tth won't send you future updates if you aren't 'caught up' + # on the story. Login isn't required for F21, but logging in will + # mark stories you've downloaded as 'read' on tth. + def performLogin(self): + params = {} + + if self.password: + params['urealname'] = self.username + params['password'] = self.password + else: + params['urealname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['loginsubmit'] = 'Login' + + if not params['password']: + return + + loginUrl = 'http://' + self.getSiteDomain() + '/login.php' + logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['urealname'])) + + ## need to pull empty login page first to get ctkn and + ## password name, which are BUSs +#
+# +# +# +# + soup = bs.BeautifulSoup(self._fetchUrl(loginUrl)) + params['ctkn']=soup.find('input', {'name':'ctkn'})['value'] + params[soup.find('input', {'id':'password'})['name']] = params['password'] + + d = self._fetchUrl(loginUrl, params) + + if "Stories Published" not in d : #Member Account + logging.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + def extractChapterUrlsAndMetadata(self): # fetch the chapter. From that we will get almost all the # metadata and chapter list @@ -67,6 +111,11 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter): url=self.url logging.debug("URL: "+url) + # tth won't send you future updates if you aren't 'caught up' + # on the story. Login isn't required for F21, but logging in will + # mark stories you've downloaded as 'read' on tth. + self.performLogin() + # use BeautifulSoup HTML parser to make everything easier to find. try: data = self._fetchUrl(url) From dbda2f96a61dd8c25ffc4a0d188bf1e361d1e011 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 21 Dec 2011 15:01:18 -0600 Subject: [PATCH 11/30] Plugin improvements: options, copy clipboard as url, save options, etc. --- calibre-plugin/__init__.py | 2 + calibre-plugin/config.py | 50 +++++- calibre-plugin/dialogs.py | 91 ++++++++--- calibre-plugin/ffdl_plugin.py | 153 ++++++++++-------- .../adapters/adapter_tthfanficorg.py | 4 +- 5 files changed, 208 insertions(+), 92 deletions(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 5ae0bde..e6f6815 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -28,6 +28,8 @@ class InterfacePluginDemo(InterfaceActionBase): version = (1, 0, 0) minimum_calibre_version = (0, 7, 53) +# action_menu_clone_qaction = True + #: 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. diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index aae3a56..c1e2e74 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -7,10 +7,13 @@ __license__ = 'GPL v3' __copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit, QTextEdit +from PyQt4.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, + QComboBox, QCheckBox) from calibre.utils.config import JSONConfig +from calibre_plugins.fanfictiondownloader_plugin.dialogs import (OVERWRITE, ADDNEW, SKIP) + # This is where all preferences for this plugin will be stored # Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also # in a global namespace, so make it as unique as possible. @@ -20,6 +23,9 @@ prefs = JSONConfig('plugins/fanfictiondownloader_plugin') # Set defaults prefs.defaults['personal.ini'] = get_resources('example.ini') +prefs.defaults['updatemeta'] = True +prefs.defaults['fileform'] = 'epub' +prefs.defaults['collision'] = OVERWRITE class ConfigWidget(QWidget): @@ -28,6 +34,43 @@ class ConfigWidget(QWidget): self.l = QVBoxLayout() self.setLayout(self.l) + horz = QHBoxLayout() + label = QLabel('Default Output &Format:') + horz.addWidget(label) + self.fileform = QComboBox(self) + self.fileform.addItem('epub') + self.fileform.addItem('mobi') + self.fileform.addItem('html') + self.fileform.addItem('txt') + self.fileform.setCurrentIndex(self.fileform.findText(prefs['fileform'])) + self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.') + label.setBuddy(self.fileform) + horz.addWidget(self.fileform) + self.l.addLayout(horz) + + horz = QHBoxLayout() + label = QLabel('Default On &Collision?') + label.setToolTip("What to do if there's already an existing story with the same title and author.") + horz.addWidget(label) + self.collision = QComboBox(self) + self.collision.addItem(OVERWRITE) + self.collision.addItem(ADDNEW) + self.collision.addItem(SKIP) + self.collision.setCurrentIndex(self.collision.findText(prefs['collision'])) + self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.') + label.setBuddy(self.collision) + horz.addWidget(self.collision) + self.l.addLayout(horz) + + horz = QHBoxLayout() + horz.addStretch(1) + + self.updatemeta = QCheckBox('Default Update &Metadata?',self) + self.updatemeta.setToolTip('Update metadata for story in Calibre from web site?') + horz.addWidget(self.updatemeta) + self.updatemeta.setChecked(prefs['updatemeta']) + self.l.addLayout(horz) + self.label = QLabel('personal.ini:') self.l.addWidget(self.label) @@ -37,6 +80,10 @@ class ConfigWidget(QWidget): self.l.addWidget(self.ini) def save_settings(self): + prefs['fileform'] = unicode(self.fileform.currentText()) + prefs['collision'] = unicode(self.collision.currentText()) + prefs['updatemeta'] = self.updatemeta.isChecked() + ini = unicode(self.ini.toPlainText()) if ini: prefs['personal.ini'] = ini @@ -44,4 +91,5 @@ class ConfigWidget(QWidget): # if they've removed everything, clear it so they get the # default next time. del prefs['personal.ini'] + diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index b68f5b7..a58ee25 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -7,18 +7,22 @@ __license__ = 'GPL v3' __copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QGridLayout, - QPushButton, QProgressDialog, QString, QLabel, - QTextEdit, QLineEdit, QInputDialog, QComboBox, - QProgressDialog, QTimer ) +from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout, + QPushButton, QProgressDialog, QString, QLabel, QCheckBox, + QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, + QProgressDialog, QTimer, QApplication ) from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions +OVERWRITE='Overwrite' +ADDNEW='Add New' +SKIP='Skip' + class DownloadDialog(QDialog): - def __init__(self, gui, icon, do_user_config, start_downloads): + def __init__(self, gui, prefs, icon, do_user_config, start_downloads): QDialog.__init__(self, gui) self.gui = gui self.do_user_config = do_user_config @@ -33,8 +37,12 @@ class DownloadDialog(QDialog): self.l.addWidget(QLabel('Story URL(s), one per line:')) self.url = QTextEdit(self) + self.url.setToolTip('URLs for stories, one per line.') self.url.setLineWrapMode(QTextEdit.NoWrap) -# self.url.setText('''http://test1.com?sid=6700 + clipboard = QApplication.instance().clipboard() + self.url.setText(clipboard.text()) + #'''http://test1.com?sid=6 +#''') # http://test1.com?sid=6701 # http://test1.com?sid=6702 # http://test1.com?sid=6703 @@ -44,34 +52,64 @@ class DownloadDialog(QDialog): # http://test1.com?sid=6707 # http://test1.com?sid=6708 # http://test1.com?sid=6709 -# ''') 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.ffdl_button = QPushButton( + 'Download Stories', self) + self.ffdl_button.setToolTip('Start download(s).') + self.ffdl_button.clicked.connect(self.ffdl) + self.l.addWidget(self.ffdl_button) + + horz = QHBoxLayout() + label = QLabel('Output &Format:') + horz.addWidget(label) self.fileform = QComboBox(self) self.fileform.addItem('epub') self.fileform.addItem('mobi') self.fileform.addItem('html') self.fileform.addItem('txt') - self.l.addWidget(self.fileform) + self.fileform.setCurrentIndex(self.fileform.findText(prefs['fileform'])) + self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.') + label.setBuddy(self.fileform) + horz.addWidget(self.fileform) + self.l.addLayout(horz) - self.ffdl_button = QPushButton( - 'Download Stories', self) - self.ffdl_button.clicked.connect(self.ffdl) - self.l.addWidget(self.ffdl_button) + horz = QHBoxLayout() + label = QLabel('On &Collision?') + label.setToolTip("What to do if there's already an existing story with the same title and author.") + horz.addWidget(label) + self.collision = QComboBox(self) + self.collision.addItem(OVERWRITE) + self.collision.addItem(ADDNEW) + self.collision.addItem(SKIP) + self.collision.setCurrentIndex(self.collision.findText(prefs['collision'])) + self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.') + label.setBuddy(self.collision) + horz.addWidget(self.collision) + self.l.addLayout(horz) + horz = QHBoxLayout() + horz.addStretch(1) + + self.updatemeta = QCheckBox('Update &Metadata?',self) + self.updatemeta.setChecked(prefs['updatemeta']) + self.updatemeta.setToolTip('Update metadata for story in Calibre from web site?') + horz.addWidget(self.updatemeta) + self.l.addLayout(horz) + + horz = QHBoxLayout() + self.about_button = QPushButton('About', self) + self.about_button.clicked.connect(self.about) + horz.addWidget(self.about_button) self.conf_button = QPushButton( 'Configure this plugin', self) self.conf_button.clicked.connect(self.config) - self.l.addWidget(self.conf_button) - - self.about_button = QPushButton('About', self) - self.about_button.clicked.connect(self.about) - self.l.addWidget(self.about_button) + horz.addWidget(self.conf_button) + self.l.addLayout(horz) self.resize(self.sizeHint()) @@ -91,7 +129,9 @@ class DownloadDialog(QDialog): def ffdl(self): self.start_downloads(unicode(self.url.toPlainText()), - unicode(self.fileform.currentText())) + unicode(self.fileform.currentText()), + unicode(self.collision.currentText()), + self.updatemeta.isChecked()) self.hide() def config(self): @@ -180,7 +220,10 @@ class MetadataProgressDialog(QProgressDialog): current = self.loop_list[self.i] try: retval = self.getadapter_function(current,self.fileform) - self.loop_good.append((current,retval)) + if retval: + self.loop_good.append((current,retval)) + else: + self.loop_bad.append((current,'Duplicate--skipped.')) except Exception as e: self.loop_bad.append((current,e)) @@ -203,8 +246,8 @@ class MetadataProgressDialog(QProgressDialog): _('Could not get metadata for %d of %d stories.') % (len(self.loop_bad), len(self.loop_list)), msg).exec_() - else: - info_dialog(self.gui, "Starting Downloads", - "Got metadata and started download for %d stories."%len(self.loop_good), - show_copy_button=False).exec_() + # else: + # info_dialog(self.gui, "Starting Downloads", + # "Got metadata and started download for %d stories."%len(self.loop_good), + # show_copy_button=False).exec_() self.gui = None diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index e14fa63..00a7a2c 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from StringIO import StringIO import ConfigParser +from functools import partial # The class that all interface action plugins must inherit from from calibre.ptempfile import PersistentTemporaryFile @@ -19,7 +20,18 @@ 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.dialogs import DownloadDialog, MetadataProgressDialog, UserPassDialog +from calibre_plugins.fanfictiondownloader_plugin.dialogs import ( + DownloadDialog, MetadataProgressDialog, UserPassDialog, OVERWRITE, ADDNEW, SKIP) + +# because calibre immediately transforms html into zip and don't want +# to have an 'if html'. db.has_format is cool with the case mismatch, +# but if I'm doing it anyway... +formmapping = { + 'epub':'EPUB', + 'mobi':'MOBI', + 'html':'ZIP', + 'txt':'TXT' + } class FanFictionDownLoaderPlugin(InterfaceAction): @@ -34,7 +46,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): action_spec = ('FanFictionDownLoader', None, 'Download FanFiction stories from various web sites', None) - action_type = 'current' + action_type = 'global' def genesis(self): # This method is called once per plugin, do initial setup here @@ -72,11 +84,22 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # things. self.db = self.gui.current_db + rows = self.gui.library_view.selectionModel().selectedRows() + if rows: + book_ids = self.gui.library_view.get_selected_ids() + print("book_ids: %s"%book_ids) + + row = self.gui.library_view.currentIndex() + if row.isValid(): + print("current id:%d"%self.gui.library_view.model().id(row)) + #self.db.get_identifiers()['url'] + # 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 # DownloadDialog just collects URLs, format and presents buttons. d = DownloadDialog(self.gui, + prefs, self.qaction.icon(), do_user_config, # method for config button self.start_downloads, # method to start downloads @@ -87,31 +110,23 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # No need to do anything with perfs here, but we could. prefs - def start_downloads(self,urls,fileform): + def start_downloads(self,urls,fileform, + collision,updatemeta): self.ffdlconfig = ConfigParser.SafeConfigParser() self.ffdlconfig.readfp(StringIO(get_resources("defaults.ini"))) self.ffdlconfig.readfp(StringIO(prefs['personal.ini'])) url_list = get_url_list(urls) - ''' - 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 = \ MetadataProgressDialog(self.gui, url_list, fileform, - self.get_adapter_for_story, - self.download_list, + partial(self.get_adapter_for_story, collision=collision), + partial(self.download_list,collision=collision,updatemeta=updatemeta), self.db) - def get_adapter_for_story(self,url,fileform): + def get_adapter_for_story(self,url,fileform,collision=SKIP): ''' Returns adapter object for story at URL. To be called from MetadataProgressDialog 'loop' to build up list of adapters. Also @@ -145,25 +160,34 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # let exceptions percolate up. story = adapter.getStoryMetadataOnly() - mi = MetaInformation(story.getMetadata("title"), - (story.getMetadata("author"),)) # author is a list. - add=True - identicalbooks = self.db.find_identical_books(mi) - if identicalbooks: - add=False - if question_dialog(self.gui, 'Add Duplicate?', '

'+ - "%s by %s is already in your library. Create a new one?"% - (story.getMetadata("title"),story.getMetadata("author")), - show_copy_button=False): - add=True + if collision != ADDNEW: + mi = MetaInformation(story.getMetadata("title"), + (story.getMetadata("author"),)) # author is a list. + + identicalbooks = self.db.find_identical_books(mi) + print(identicalbooks) + ## more than one match will need to be handled differently. + if identicalbooks and collision == SKIP: + add=False + # book_id = identicalbooks.pop() + # print("formats:"+self.db.formats(book_id,index_is_id=True)) + # print("has format:%s"%self.db.has_format(book_id,formmapping[fileform],index_is_id=True)) + # if self.db.has_format(book_id,formmapping[fileform],index_is_id=True): + # if question_dialog(self.gui, 'Update?', '

'+ + # "%s by %s is already in your library more than once. Add/Replace this format?"% + # (story.getMetadata("title"),story.getMetadata("author")), + # show_copy_button=False): + # add=True if add: return adapter else: return None - def download_list(self,adaptertuple_list,fileform): + def download_list(self,adaptertuple_list,fileform, + collision=ADDNEW, + updatemeta=True): ''' Called by MetadataProgressDialog to start story downloads BG processing. adapter_list is a list of tuples of (url,adapter) @@ -174,38 +198,16 @@ class FanFictionDownLoaderPlugin(InterfaceAction): 'Downloading FanFiction Stories', func=self.do_story_downloads, args=(adaptertuple_list, fileform, self.db), - kwargs={}, + kwargs={'collision':collision,'updatemeta':updatemeta}, callback=self._get_stories_completed) + self.gui.job_manager.run_threaded_job(job) self.gui.status_bar.show_message('Downloading %d stories'%len(adaptertuple_list)) def _get_stories_completed(self, job): print("_get_stories_completed") - # remove_dir(job.tdir) - if job.failed: - return self.gui.job_exception(job, dialog_title='Failed to download stories') -# self.gui.status_bar.show_message('Download Stories completed', 3000) - # not really, but leave it for now. - book_pages_map, book_words_map = job.result - - # if len(book_pages_map) + len(book_words_map) == 0: - # # Must have been some sort of error in processing this book - # msg = 'Failed to generate any statistics. View Log for details' - # p = ErrorNotification(job.details, 'Count log', 'Count Pages failed', msg, - # show_copy_button=False, parent=self.gui) - # else: - # payload = (book_pages_map, job.pages_custom_column, book_words_map, job.words_custom_column) - # all_ids = set(book_pages_map.keys()) | set(book_words_map.keys()) - # msg = '

Count Pages plugin found %d statistics(s). ' % len(all_ids) + \ - # 'Proceed with updating your library?' - # p = ProceedNotification(self._update_database_columns, - # payload, job.details, - # 'Count log', 'Count complete', msg, - # show_copy_button=False, parent=self.gui) - # p.show() - #info_dialog(self.gui,'FFDL Complete','It worked?') def do_story_downloads(self, adaptertuple_list, fileform, db, **kwargs): # lambda x,y:x lambda makes small anonymous function. @@ -214,20 +216,30 @@ class FanFictionDownLoaderPlugin(InterfaceAction): Master job, loop to download this list of stories ''' print("do_story_downloads") + abort = kwargs['abort'] notifications=kwargs['notifications'] + log = kwargs['log'] notifications.put((0.01, 'Start Downloading Stories')) - count = 0 + count = 0.01 total = len(adaptertuple_list) # Queue all the jobs for (url,adapter) in adaptertuple_list: - self.do_story_download(adapter,fileform,db) + if abort.is_set(): + notifications.put(1.0,'Aborting...') + return + notifications.put((float(count)/total, + 'Downloading %s'%adapter.getStoryMetadataOnly().getMetadata("title"))) + log.prints(log.INFO,'Downloading %s'%adapter.getStoryMetadataOnly().getMetadata("title")) + try: + self.do_story_download(adapter,fileform,db,kwargs['collision'],kwargs['updatemeta']) + except Exception as e: + log.prints(log.ERROR,'Failed Downloading %s: %s'% + (adapter.getStoryMetadataOnly().getMetadata("title"),e)) + 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 {},{} + return - def do_story_download(self,adapter,fileform,db): + def do_story_download(self,adapter,fileform,db,collision,updatemeta): print("do_story_download") story = adapter.getStoryMetadataOnly() @@ -237,24 +249,35 @@ class FanFictionDownLoaderPlugin(InterfaceAction): writer = writers.getWriter(fileform,adapter.config,adapter) tmp = PersistentTemporaryFile("."+fileform) + print("%s by %s"%(story.getMetadata("title"), story.getMetadata("author"))) 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.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) + identicalbooks = self.db.find_identical_books(mi) + print(identicalbooks) + addedcount=0 + if identicalbooks and collision == OVERWRITE: + ## more than one match? add to first off the list. + book_id = identicalbooks.pop() + if updatemeta: + db.set_metadata(book_id,mi) + db.add_format_with_hooks(book_id, fileform, tmp, index_is_id=True) + else: + (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) + if addedcount: + self.gui.library_view.model().books_added(addedcount) del adapter del writer diff --git a/fanficdownloader/adapters/adapter_tthfanficorg.py b/fanficdownloader/adapters/adapter_tthfanficorg.py index 35dbec8..3964172 100644 --- a/fanficdownloader/adapters/adapter_tthfanficorg.py +++ b/fanficdownloader/adapters/adapter_tthfanficorg.py @@ -174,10 +174,10 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter): BtVS = True for cat in verticaltable.findAll('a', href=re.compile(r"^/Category-")): - if cat.string not in ['General', 'Non-BtVS/AtS Stories', 'BtVS/AtS Non-Crossover']: + if cat.string not in ['General', 'Non-BtVS/AtS Stories', 'BtVS/AtS Non-Crossover', 'Non-BtVS Crossovers']: self.story.addToList('category',cat.string) else: - if cat.string == 'Non-BtVS/AtS Stories': + if 'Non-BtVS' in cat.string: BtVS = False if BtVS: self.story.addToList('category','Buffy: The Vampire Slayer') From 8fb62499cdaecfc2ce1f350aa2c14b7cf5dfe01e Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 21 Dec 2011 18:08:36 -0600 Subject: [PATCH 12/30] Remove 'aa' from plugin name, create a new icon. --- calibre-plugin/__init__.py | 2 +- calibre-plugin/images/icon.png | Bin 5785 -> 23629 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index e6f6815..bfc4ad6 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -21,7 +21,7 @@ class InterfacePluginDemo(InterfaceActionBase): The reason for having two classes is that it allows the command line calibre utilities to run without needing to load the GUI libraries. ''' - name = 'aa FanFictionDownLoader Plugin' + name = 'FanFictionDownLoader Plugin' description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' diff --git a/calibre-plugin/images/icon.png b/calibre-plugin/images/icon.png index ad823e2ff4d53c376632ccb0e2fe60d96cc063ea..75b2806e15e2c1363d2c69bf9fb7cdfa43ba0c1f 100644 GIT binary patch literal 23629 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 Date: Wed, 21 Dec 2011 23:44:22 -0600 Subject: [PATCH 13/30] Added tag calibre-plugin-1.0.0 for changeset 42f22186b2dd From 6bbe59c76be83a7899076e8aa02896c99b8d620a Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 21 Dec 2011 23:46:12 -0600 Subject: [PATCH 14/30] Prefill URLbox from selected books. Filter that and clipboard for good URL. Save icon gimp file, python script to create calibre-plugin zip. --- calibre-plugin/dialogs.py | 22 ++--------- calibre-plugin/ffdl_plugin.py | 70 ++++++++++++++++++++++++++++----- calibre-plugin/images/icon.xcf | Bin 0 -> 61221 bytes makeplugin.py | 38 ++++++++++++++++++ 4 files changed, 102 insertions(+), 28 deletions(-) create mode 100644 calibre-plugin/images/icon.xcf create mode 100644 makeplugin.py diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index a58ee25..a646105 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, - QProgressDialog, QTimer, QApplication ) + QProgressDialog, QTimer ) from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog @@ -22,7 +22,7 @@ SKIP='Skip' class DownloadDialog(QDialog): - def __init__(self, gui, prefs, icon, do_user_config, start_downloads): + def __init__(self, gui, prefs, icon, url_list_text, do_user_config, start_downloads): QDialog.__init__(self, gui) self.gui = gui self.do_user_config = do_user_config @@ -39,25 +39,9 @@ class DownloadDialog(QDialog): self.url = QTextEdit(self) self.url.setToolTip('URLs for stories, one per line.') self.url.setLineWrapMode(QTextEdit.NoWrap) - clipboard = QApplication.instance().clipboard() - self.url.setText(clipboard.text()) - #'''http://test1.com?sid=6 -#''') -# 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 -# http://test1.com?sid=6707 -# http://test1.com?sid=6708 -# http://test1.com?sid=6709 + self.url.setText(url_list_text) self.l.addWidget(self.url) - # self.url = QLineEdit(self) - # self.url.setText('http://test1.com?sid=12345') - # self.l.addWidget(self.url) - self.ffdl_button = QPushButton( 'Download Stories', self) self.ffdl_button.setToolTip('Start download(s).') diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 00a7a2c..23a3b46 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -11,9 +11,12 @@ from StringIO import StringIO import ConfigParser from functools import partial +from PyQt4.Qt import (QApplication) + # The class that all interface action plugins must inherit from from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog from calibre.gui2.actions import InterfaceAction from calibre.gui2.threaded_jobs import ThreadedJob @@ -84,15 +87,63 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # things. self.db = self.gui.current_db + ## if there's rows selected, try to find a source URL from + ## either identifier in the metadata, or from the epub + ## metadata. + url_list = [] rows = self.gui.library_view.selectionModel().selectedRows() if rows: book_ids = self.gui.library_view.get_selected_ids() print("book_ids: %s"%book_ids) + for book_id in book_ids: + identifiers = self.db.get_identifiers(book_id,index_is_id=True) + if 'url' in identifiers: + # identifiers have :->| in url. + #print("url from book:"+identifiers['url'].replace('|',':')) + url_list.append(identifiers['url'].replace('|',':')) + else: + ## only epub has that in it. + if self.db.has_format(book_id,'EPUB',index_is_id=True): + stream = self.db.format(book_id,'EPUB',index_is_id=True, as_file=True) + mi = get_metadata(stream,'EPUB') + #print("mi:%s"%mi) + identifiers = mi.get_identifiers() + if 'url' in identifiers: + #print("url from epub:"+identifiers['url'].replace('|',':')) + url_list.append(identifiers['url'].replace('|',':')) + else: + # no rows selected, check for valid URLs in the clipboard. + cliptext = unicode(QApplication.instance().clipboard().text()) + url_list.extend(cliptext.split()) + + url_list_text = "" + # Check and make sure the URLs are valid ffdl URLs. + if url_list: + dummyconfig = ConfigParser.SafeConfigParser() + for url in url_list: + # pulling up an adapter is pretty low over-head. If + # it fails, it's a bad url. + try: + adapters.getAdapter(dummyconfig,url) + except: + pass + else: + if url_list_text: + url_list_text += "\n" + url_list_text += url + + #'''http://test1.com?sid=6 +#''') +# 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 +# http://test1.com?sid=6707 +# http://test1.com?sid=6708 +# http://test1.com?sid=6709 - row = self.gui.library_view.currentIndex() - if row.isValid(): - print("current id:%d"%self.gui.library_view.model().id(row)) - #self.db.get_identifiers()['url'] # 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 @@ -101,6 +152,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): d = DownloadDialog(self.gui, prefs, self.qaction.icon(), + url_list_text, do_user_config, # method for config button self.start_downloads, # method to start downloads ) @@ -120,11 +172,11 @@ class FanFictionDownLoaderPlugin(InterfaceAction): self.fetchmeta_qpd = \ MetadataProgressDialog(self.gui, - url_list, - fileform, - partial(self.get_adapter_for_story, collision=collision), - partial(self.download_list,collision=collision,updatemeta=updatemeta), - self.db) + url_list, + fileform, + partial(self.get_adapter_for_story, collision=collision), + partial(self.download_list,collision=collision,updatemeta=updatemeta), + self.db) def get_adapter_for_story(self,url,fileform,collision=SKIP): ''' diff --git a/calibre-plugin/images/icon.xcf b/calibre-plugin/images/icon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..36c63a53671fcd9e14ec5efa9f04eba9d09a2e0d GIT binary patch literal 61221 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 BseS+e literal 0 HcmV?d00001 diff --git a/makeplugin.py b/makeplugin.py new file mode 100644 index 0000000..4d2c336 --- /dev/null +++ b/makeplugin.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# epubmerge.py 1.0 + +# Copyright 2011, Jim Miller + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from glob import glob + +from makezip import createZipFile + +if __name__=="__main__": + filename="FanFictionDownLoaderPlugin.zip" + exclude=['*.pyc','*~','*.xcf'] + # from top dir. 'w' for overwrite + createZipFile(filename,"w", + ['defaults.ini','example.ini','fanficdownloader'], + exclude=exclude) + #from calibre-plugin dir. 'a' for append + os.chdir('calibre-plugin') + files=['about.txt','images',] + files.extend(glob('*.py')) + files.extend(glob('plugin-import-name-*.txt')) + createZipFile("../"+filename,"a", + files,exclude=exclude) From d4280e84d12800a33df02d5eda627190345c8daf Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Thu, 22 Dec 2011 19:21:10 -0600 Subject: [PATCH 15/30] Add epub update, option to only overwrite when newer, option to update meta only --- calibre-plugin/config.py | 21 ++- calibre-plugin/dialogs.py | 58 +++++-- calibre-plugin/ffdl_plugin.py | 175 ++++++++++++++++----- fanficdownloader/adapters/adapter_test1.py | 8 +- makeplugin.py | 2 +- 5 files changed, 191 insertions(+), 73 deletions(-) diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index c1e2e74..ac0427d 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -12,7 +12,7 @@ from PyQt4.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTex from calibre.utils.config import JSONConfig -from calibre_plugins.fanfictiondownloader_plugin.dialogs import (OVERWRITE, ADDNEW, SKIP) +from calibre_plugins.fanfictiondownloader_plugin.dialogs import (OVERWRITE, ADDNEW, SKIP,CALIBREONLY,UPDATE) # This is where all preferences for this plugin will be stored # Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also @@ -24,6 +24,7 @@ prefs = JSONConfig('plugins/fanfictiondownloader_plugin') # Set defaults prefs.defaults['personal.ini'] = get_resources('example.ini') prefs.defaults['updatemeta'] = True +prefs.defaults['onlyoverwriteifnewer'] = False prefs.defaults['fileform'] = 'epub' prefs.defaults['collision'] = OVERWRITE @@ -49,28 +50,31 @@ class ConfigWidget(QWidget): self.l.addLayout(horz) horz = QHBoxLayout() - label = QLabel('Default On &Collision?') + label = QLabel('Default If Story Already Exists?') label.setToolTip("What to do if there's already an existing story with the same title and author.") horz.addWidget(label) self.collision = QComboBox(self) self.collision.addItem(OVERWRITE) + self.collision.addItem(UPDATE) self.collision.addItem(ADDNEW) self.collision.addItem(SKIP) + self.collision.addItem(CALIBREONLY) self.collision.setCurrentIndex(self.collision.findText(prefs['collision'])) self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.') label.setBuddy(self.collision) horz.addWidget(self.collision) self.l.addLayout(horz) - horz = QHBoxLayout() - horz.addStretch(1) - - self.updatemeta = QCheckBox('Default Update &Metadata?',self) + self.updatemeta = QCheckBox('Default Update Calibre &Metadata?',self) self.updatemeta.setToolTip('Update metadata for story in Calibre from web site?') - horz.addWidget(self.updatemeta) self.updatemeta.setChecked(prefs['updatemeta']) - self.l.addLayout(horz) + self.l.addWidget(self.updatemeta) + self.onlyoverwriteifnewer = QCheckBox('Default Only Overwrite Story if Newer',self) + self.onlyoverwriteifnewer.setToolTip("Don't overwrite existing book unless the story on the web site is newer or from the same day.") + self.onlyoverwriteifnewer.setChecked(prefs['onlyoverwriteifnewer']) + self.l.addWidget(self.onlyoverwriteifnewer) + self.label = QLabel('personal.ini:') self.l.addWidget(self.label) @@ -83,6 +87,7 @@ class ConfigWidget(QWidget): prefs['fileform'] = unicode(self.fileform.currentText()) prefs['collision'] = unicode(self.collision.currentText()) prefs['updatemeta'] = self.updatemeta.isChecked() + prefs['onlyoverwriteifnewer'] = self.onlyoverwriteifnewer.isChecked() ini = unicode(self.ini.toPlainText()) if ini: diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index a646105..1ff0820 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -7,6 +7,8 @@ __license__ = 'GPL v3' __copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' +import traceback + from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, @@ -17,8 +19,10 @@ from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dia from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions OVERWRITE='Overwrite' +UPDATE='Update EPUB' ADDNEW='Add New' SKIP='Skip' +CALIBREONLY='Update Calibre Metadata Only' class DownloadDialog(QDialog): @@ -37,7 +41,7 @@ class DownloadDialog(QDialog): self.l.addWidget(QLabel('Story URL(s), one per line:')) self.url = QTextEdit(self) - self.url.setToolTip('URLs for stories, one per line.') + self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from selected books or clipboard, but only valid URLs.') self.url.setLineWrapMode(QTextEdit.NoWrap) self.url.setText(url_list_text) self.l.addWidget(self.url) @@ -46,6 +50,9 @@ class DownloadDialog(QDialog): 'Download Stories', self) self.ffdl_button.setToolTip('Start download(s).') self.ffdl_button.clicked.connect(self.ffdl) + # if there's already URL(s), focus 'go' button + if url_list_text: + self.ffdl_button.setFocus() self.l.addWidget(self.ffdl_button) horz = QHBoxLayout() @@ -63,28 +70,36 @@ class DownloadDialog(QDialog): self.l.addLayout(horz) horz = QHBoxLayout() - label = QLabel('On &Collision?') + label = QLabel('If Story Already Exists?') label.setToolTip("What to do if there's already an existing story with the same title and author.") horz.addWidget(label) self.collision = QComboBox(self) self.collision.addItem(OVERWRITE) + self.collision.addItem(UPDATE) self.collision.addItem(ADDNEW) self.collision.addItem(SKIP) + self.collision.addItem(CALIBREONLY) self.collision.setCurrentIndex(self.collision.findText(prefs['collision'])) - self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.') + self.collision.setToolTip(OVERWRITE+' will replace the existing story.\n'+ + UPDATE+' will download new chapters only and add to existing EPUB.\n'+ + ADDNEW+' will create a new story with the same title and author.\n'+ + SKIP+' will not download existing stories.\n'+ + CALIBREONLY+' will not download stories, but will update Calibre metadata.') label.setBuddy(self.collision) horz.addWidget(self.collision) self.l.addLayout(horz) - horz = QHBoxLayout() - horz.addStretch(1) - - self.updatemeta = QCheckBox('Update &Metadata?',self) - self.updatemeta.setChecked(prefs['updatemeta']) + self.updatemeta = QCheckBox('Update Calibre &Metadata?',self) self.updatemeta.setToolTip('Update metadata for story in Calibre from web site?') - horz.addWidget(self.updatemeta) - self.l.addLayout(horz) + self.updatemeta.setChecked(prefs['updatemeta']) + self.l.addWidget(self.updatemeta) + self.onlyoverwriteifnewer = QCheckBox('Only Overwrite Story if Newer',self) + self.onlyoverwriteifnewer.setToolTip("Don't overwrite existing book unless the story on the web site is newer.\n"+ + "From the same day counts as 'newer' because the sites don't give update time.") + self.onlyoverwriteifnewer.setChecked(prefs['onlyoverwriteifnewer']) + self.l.addWidget(self.onlyoverwriteifnewer) + horz = QHBoxLayout() self.about_button = QPushButton('About', self) self.about_button.clicked.connect(self.about) @@ -115,7 +130,8 @@ class DownloadDialog(QDialog): self.start_downloads(unicode(self.url.toPlainText()), unicode(self.fileform.currentText()), unicode(self.collision.currentText()), - self.updatemeta.isChecked()) + self.updatemeta.isChecked(), + self.onlyoverwriteifnewer.isChecked()) self.hide() def config(self): @@ -203,13 +219,14 @@ class MetadataProgressDialog(QProgressDialog): else: current = self.loop_list[self.i] try: + ## collision spec passed into getadapter by partial from ffdl_plugin + ## no retval only if it exists, but collision is SKIP retval = self.getadapter_function(current,self.fileform) - if retval: - self.loop_good.append((current,retval)) - else: - self.loop_bad.append((current,'Duplicate--skipped.')) + self.loop_good.append((current,retval)) except Exception as e: + print("%s:%s"%(current,e)) self.loop_bad.append((current,e)) + traceback.print_exc() self.updateStatus() self.i += 1 @@ -226,8 +243,8 @@ class MetadataProgressDialog(QProgressDialog): 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.') % + warning_dialog(self.gui, _('Not going to download some stories'), + _('Not going to download %d of %d stories.') % (len(self.loop_bad), len(self.loop_list)), msg).exec_() # else: @@ -235,3 +252,10 @@ class MetadataProgressDialog(QProgressDialog): # "Got metadata and started download for %d stories."%len(self.loop_good), # show_copy_button=False).exec_() self.gui = None + +class NotGoingToDownload(Exception): + def __init__(self,error): + self.error=error + + def __str__(self): + return self.error diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 23a3b46..3215c63 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -7,9 +7,10 @@ __license__ = 'GPL v3' __copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' +import ConfigParser, os from StringIO import StringIO -import ConfigParser from functools import partial +from datetime import datetime from PyQt4.Qt import (QApplication) @@ -22,9 +23,12 @@ 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.epubmerge import doMerge + from calibre_plugins.fanfictiondownloader_plugin.config import prefs from calibre_plugins.fanfictiondownloader_plugin.dialogs import ( - DownloadDialog, MetadataProgressDialog, UserPassDialog, OVERWRITE, ADDNEW, SKIP) + DownloadDialog, MetadataProgressDialog, UserPassDialog, + OVERWRITE, UPDATE, ADDNEW, SKIP, CALIBREONLY, NotGoingToDownload ) # because calibre immediately transforms html into zip and don't want # to have an 'if html'. db.has_format is cool with the case mismatch, @@ -104,8 +108,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): else: ## only epub has that in it. if self.db.has_format(book_id,'EPUB',index_is_id=True): - stream = self.db.format(book_id,'EPUB',index_is_id=True, as_file=True) - mi = get_metadata(stream,'EPUB') + existingepub = self.db.format(book_id,'EPUB',index_is_id=True, as_file=True) + mi = get_metadata(existingepub,'EPUB') #print("mi:%s"%mi) identifiers = mi.get_identifiers() if 'url' in identifiers: @@ -120,7 +124,11 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # Check and make sure the URLs are valid ffdl URLs. if url_list: dummyconfig = ConfigParser.SafeConfigParser() + alreadyin=[] for url in url_list: + if url in alreadyin: + continue + alreadyin.append(url) # pulling up an adapter is pretty low over-head. If # it fails, it's a bad url. try: @@ -163,10 +171,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): prefs def start_downloads(self,urls,fileform, - collision,updatemeta): - self.ffdlconfig = ConfigParser.SafeConfigParser() - self.ffdlconfig.readfp(StringIO(get_resources("defaults.ini"))) - self.ffdlconfig.readfp(StringIO(prefs['personal.ini'])) + collision,updatemeta,onlyoverwriteifnewer): url_list = get_url_list(urls) @@ -174,11 +179,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction): MetadataProgressDialog(self.gui, url_list, fileform, - partial(self.get_adapter_for_story, collision=collision), - partial(self.download_list,collision=collision,updatemeta=updatemeta), + partial(self.get_adapter_for_story, collision=collision,onlyoverwriteifnewer=onlyoverwriteifnewer), + partial(self.download_list,collision=collision,updatemeta=updatemeta,onlyoverwriteifnewer=onlyoverwriteifnewer), self.db) - def get_adapter_for_story(self,url,fileform,collision=SKIP): + def get_adapter_for_story(self,url,fileform, + collision=SKIP, + onlyoverwriteifnewer=False): ''' Returns adapter object for story at URL. To be called from MetadataProgressDialog 'loop' to build up list of adapters. Also @@ -186,7 +193,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction): ''' print("URL:"+url) - adapter = adapters.getAdapter(self.ffdlconfig,url) + ## was self.ffdlconfig, but we need to be able to change it + ## when doing epub update. + ffdlconfig = ConfigParser.SafeConfigParser() + ffdlconfig.readfp(StringIO(get_resources("defaults.ini"))) + ffdlconfig.readfp(StringIO(prefs['personal.ini'])) + adapter = adapters.getAdapter(ffdlconfig,url) try: adapter.getStoryMetadataOnly() @@ -212,7 +224,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # let exceptions percolate up. story = adapter.getStoryMetadataOnly() - add=True if collision != ADDNEW: mi = MetaInformation(story.getMetadata("title"), (story.getMetadata("author"),)) # author is a list. @@ -220,26 +231,55 @@ class FanFictionDownLoaderPlugin(InterfaceAction): identicalbooks = self.db.find_identical_books(mi) print(identicalbooks) ## more than one match will need to be handled differently. - if identicalbooks and collision == SKIP: - add=False - # book_id = identicalbooks.pop() - # print("formats:"+self.db.formats(book_id,index_is_id=True)) - # print("has format:%s"%self.db.has_format(book_id,formmapping[fileform],index_is_id=True)) - # if self.db.has_format(book_id,formmapping[fileform],index_is_id=True): - # if question_dialog(self.gui, 'Update?', '

'+ - # "%s by %s is already in your library more than once. Add/Replace this format?"% - # (story.getMetadata("title"),story.getMetadata("author")), - # show_copy_button=False): - # add=True + if identicalbooks: + book_id = identicalbooks.pop() + if collision == SKIP: + raise NotGoingToDownload("Skipping duplicate story.") - if add: - return adapter - else: - return None + if collision == OVERWRITE and len(identicalbooks) > 1: + raise NotGoingToDownload("More than one identical books--can't tell which to overwrite.") + + if collision == OVERWRITE and \ + onlyoverwriteifnewer and \ + self.db.has_format(book_id,fileform,index_is_id=True): + # check make sure incoming is newer. + lastupdated=story.getMetadataRaw('dateUpdated').date() + fileupdated=datetime.fromtimestamp(os.stat(self.db.format_abspath(book_id, fileform, index_is_id=True))[8]).date() + if fileupdated > lastupdated: + raise NotGoingToDownload("Not Overwriting, story is not newer.") + + if collision == UPDATE: + if fileform != 'epub': + raise NotGoingToDownload("Not updating non-epub format.") + # 'book' can exist without epub. If there's no existing epub, + # let it go and it will download it. + if self.db.has_format(book_id,fileform,index_is_id=True): + toupdateio = StringIO() + (epuburl,chaptercount) = doMerge(toupdateio, + [StringIO(self.db.format(book_id,'EPUB', + index_is_id=True))], + titlenavpoints=False, + striptitletoc=True, + forceunique=False) + + urlchaptercount = int(story.getMetadata('numChapters')) + if chaptercount == urlchaptercount: # and not onlyoverwriteifnewer: + raise NotGoingToDownload("%s already contains %d chapters." % (url,chaptercount)) + elif chaptercount > urlchaptercount: + raise NotGoingToDownload("%s contains %d chapters, more than epub." % (url,chaptercount)) + else: + print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)) + + else: # not identicalbooks + if collision == CALIBREONLY: + raise NotGoingToDownload("Not updating Calibre Metadata, no existing book to update.") + + return adapter def download_list(self,adaptertuple_list,fileform, collision=ADDNEW, - updatemeta=True): + updatemeta=True, + onlyoverwriteifnewer=True): ''' Called by MetadataProgressDialog to start story downloads BG processing. adapter_list is a list of tuples of (url,adapter) @@ -250,7 +290,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): 'Downloading FanFiction Stories', func=self.do_story_downloads, args=(adaptertuple_list, fileform, self.db), - kwargs={'collision':collision,'updatemeta':updatemeta}, + kwargs={'collision':collision,'updatemeta':updatemeta, + 'onlyoverwriteifnewer':onlyoverwriteifnewer}, callback=self._get_stories_completed) @@ -262,8 +303,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): print("_get_stories_completed") def do_story_downloads(self, adaptertuple_list, fileform, db, - **kwargs): # lambda x,y:x lambda makes small anonymous function. - # abort=None, log=None, + **kwargs): ''' Master job, loop to download this list of stories ''' @@ -283,7 +323,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): 'Downloading %s'%adapter.getStoryMetadataOnly().getMetadata("title"))) log.prints(log.INFO,'Downloading %s'%adapter.getStoryMetadataOnly().getMetadata("title")) try: - self.do_story_download(adapter,fileform,db,kwargs['collision'],kwargs['updatemeta']) + self.do_story_download(adapter,fileform,db, + kwargs['collision'],kwargs['updatemeta'],kwargs['onlyoverwriteifnewer']) except Exception as e: log.prints(log.ERROR,'Failed Downloading %s: %s'% (adapter.getStoryMetadataOnly().getMetadata("title"),e)) @@ -291,7 +332,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): count = count + 1 return - def do_story_download(self,adapter,fileform,db,collision,updatemeta): + def do_story_download(self,adapter,fileform,db,collision, + updatemeta,onlyoverwriteifnewer): print("do_story_download") story = adapter.getStoryMetadataOnly() @@ -301,29 +343,76 @@ class FanFictionDownLoaderPlugin(InterfaceAction): writer = writers.getWriter(fileform,adapter.config,adapter) tmp = PersistentTemporaryFile("."+fileform) - print("%s by %s"%(story.getMetadata("title"), story.getMetadata("author"))) + titleauth = "%s by %s"%(story.getMetadata("title"), story.getMetadata("author")) + url = story.getMetadata("storyUrl") + print(titleauth) print("tmp: "+tmp.name) - writer.writeStory(tmp) - 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.pubdate = story.getMetadataRaw('datePublished') + mi.timestamp = story.getMetadataRaw('dateCreated') mi.comments = story.getMetadata("description") identicalbooks = self.db.find_identical_books(mi) print(identicalbooks) addedcount=0 - if identicalbooks and collision == OVERWRITE: + if identicalbooks: ## more than one match? add to first off the list. + ## Shouldn't happen--we checked above. book_id = identicalbooks.pop() - if updatemeta: - db.set_metadata(book_id,mi) + if collision == UPDATE: + if self.db.has_format(book_id,fileform,index_is_id=True): + urlchaptercount = int(story.getMetadata('numChapters')) + ## First, get existing epub with titlepage and tocpage stripped. + updateio = StringIO() + (epuburl,chaptercount) = doMerge(updateio, + [StringIO(self.db.format(book_id,'EPUB', + index_is_id=True))], + titlenavpoints=False, + striptitletoc=True, + forceunique=False) + print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)) + + ## Get updated title page/metadata by itself in an epub. + ## Even if the title page isn't included, this carries the metadata. + titleio = StringIO() + writer.writeStory(outstream=titleio,metaonly=True) + + newchaptersio = None + if urlchaptercount > chaptercount : + ## Go get the new chapters only in another epub. + newchaptersio = StringIO() + adapter.setChaptersRange(chaptercount+1,urlchaptercount) + + adapter.config.set("overrides",'include_tocpage','false') + adapter.config.set("overrides",'include_titlepage','false') + writer.writeStory(outstream=newchaptersio) + + ## Merge the three epubs together. + doMerge(tmp, + [titleio,updateio,newchaptersio], + fromfirst=True, + titlenavpoints=False, + striptitletoc=False, + forceunique=False) + + + else: # update, but there's no epub extant, so do overwrite. + collision = OVERWRITE + + if collision == OVERWRITE: + writer.writeStory(tmp) + db.add_format_with_hooks(book_id, fileform, tmp, index_is_id=True) - else: + + if updatemeta or collision == CALIBREONLY: + db.set_metadata(book_id,mi) + + else: # no matching, adding new. + writer.writeStory(tmp) (notadded,addedcount)=db.add_books([tmp],[fileform],[mi], add_duplicates=True) diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index ef33e42..7d59b23 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -75,12 +75,12 @@ class TestSiteAdapter(BaseSiteAdapter): Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic" ''') - self.story.setMetadata('datePublished',makeDate("1972-01-31","%Y-%m-%d")) + self.story.setMetadata('datePublished',makeDate("1975-03-15","%Y-%m-%d")) self.story.setMetadata('dateCreated',datetime.datetime.now()) if self.story.getMetadata('storyId') == '669': self.story.setMetadata('dateUpdated',datetime.datetime.now()) else: - self.story.setMetadata('dateUpdated',makeDate("1975-01-31","%Y-%m-%d")) + self.story.setMetadata('dateUpdated',makeDate("1975-04-15","%Y-%m-%d")) self.story.setMetadata('numWords','123456') self.story.setMetadata('status','In-Completed') self.story.setMetadata('rating','Tweenie') @@ -104,8 +104,8 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!" ('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"), ('Chapter 2, Sinmay on Kintikin',self.url+"&chapter=3"), ('Chapter 3, Over Cinnabar',self.url+"&chapter=4"), - ('Chapter 4',self.url+"&chapter=5"), - ('Chapter 5',self.url+"&chapter=6"), + # ('Chapter 4',self.url+"&chapter=5"), + # ('Chapter 5',self.url+"&chapter=6"), # ('Chapter 6',self.url+"&chapter=6"), # ('Chapter 7',self.url+"&chapter=6"), # ('Chapter 8',self.url+"&chapter=6"), diff --git a/makeplugin.py b/makeplugin.py index 4d2c336..d828768 100644 --- a/makeplugin.py +++ b/makeplugin.py @@ -27,7 +27,7 @@ if __name__=="__main__": exclude=['*.pyc','*~','*.xcf'] # from top dir. 'w' for overwrite createZipFile(filename,"w", - ['defaults.ini','example.ini','fanficdownloader'], + ['defaults.ini','example.ini','epubmerge.py','fanficdownloader'], exclude=exclude) #from calibre-plugin dir. 'a' for append os.chdir('calibre-plugin') From 3aaf9ec91d4e49ceaa9a6b81c966a878e18beec5 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Thu, 22 Dec 2011 19:22:13 -0600 Subject: [PATCH 16/30] Added tag calibre-plugin-1.0.1 for changeset c97a71bd7dc9 From 6e52a5b0965f70bc05098447e2ce8634a48e080b Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sat, 24 Dec 2011 09:40:23 -0600 Subject: [PATCH 17/30] Fix for 'Add New' collision. --- calibre-plugin/ffdl_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 3215c63..5bf97cc 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -359,7 +359,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): identicalbooks = self.db.find_identical_books(mi) print(identicalbooks) addedcount=0 - if identicalbooks: + if identicalbooks and collision != ADDNEW: ## more than one match? add to first off the list. ## Shouldn't happen--we checked above. book_id = identicalbooks.pop() From 74ac6fb348e70965629b223a2c81e4d8ea07a9c9 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sun, 25 Dec 2011 15:27:25 -0600 Subject: [PATCH 18/30] Add options to disable/prioritize taking URLs from selected stories & clipboard. --- calibre-plugin/config.py | 33 +++++++++ calibre-plugin/ffdl_plugin.py | 130 +++++++++++++++++++++------------- 2 files changed, 114 insertions(+), 49 deletions(-) diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index ac0427d..bf34380 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -21,10 +21,17 @@ from calibre_plugins.fanfictiondownloader_plugin.dialogs import (OVERWRITE, ADDN # so as to ensure you dont accidentally clobber a calibre config file prefs = JSONConfig('plugins/fanfictiondownloader_plugin') +# urlsfrompriority values +CLIP = 'Clipboard' +SELECTED = 'Selected Stories' + # Set defaults prefs.defaults['personal.ini'] = get_resources('example.ini') prefs.defaults['updatemeta'] = True prefs.defaults['onlyoverwriteifnewer'] = False +prefs.defaults['urlsfromclip'] = True +prefs.defaults['urlsfromselected'] = True +prefs.defaults['urlsfrompriority'] = SELECTED prefs.defaults['fileform'] = 'epub' prefs.defaults['collision'] = OVERWRITE @@ -75,6 +82,29 @@ class ConfigWidget(QWidget): self.onlyoverwriteifnewer.setChecked(prefs['onlyoverwriteifnewer']) self.l.addWidget(self.onlyoverwriteifnewer) + self.urlsfromclip = QCheckBox('Take URLs from Clipboard?',self) + self.urlsfromclip.setToolTip('Prefill URLs from valid URLs in Clipboard when starting plugin?') + self.urlsfromclip.setChecked(prefs['urlsfromclip']) + self.l.addWidget(self.urlsfromclip) + + self.urlsfromselected = QCheckBox('Take URLs from Selected Stories?',self) + self.urlsfromselected.setToolTip('Prefill URLs from valid URLs in selected stories when starting plugin?') + self.urlsfromselected.setChecked(prefs['urlsfromselected']) + self.l.addWidget(self.urlsfromselected) + + horz = QHBoxLayout() + label = QLabel('Take URLs from which first:') + label.setToolTip("If both clipbaord and selected enabled and populated, which is used?") + horz.addWidget(label) + self.urlsfrompriority = QComboBox(self) + self.urlsfrompriority.addItem(SELECTED) + self.urlsfrompriority.addItem(CLIP) + self.urlsfrompriority.setCurrentIndex(self.urlsfrompriority.findText(prefs['urlsfrompriority'])) + self.urlsfrompriority.setToolTip('If both clipbaord and selected enabled and populated, which is used?') + label.setBuddy(self.urlsfrompriority) + horz.addWidget(self.urlsfrompriority) + self.l.addLayout(horz) + self.label = QLabel('personal.ini:') self.l.addWidget(self.label) @@ -87,6 +117,9 @@ class ConfigWidget(QWidget): prefs['fileform'] = unicode(self.fileform.currentText()) prefs['collision'] = unicode(self.collision.currentText()) prefs['updatemeta'] = self.updatemeta.isChecked() + prefs['urlsfrompriority'] = unicode(self.urlsfrompriority.currentText()) + prefs['urlsfromclip'] = self.urlsfromclip.isChecked() + prefs['urlsfromselected'] = self.urlsfromselected.isChecked() prefs['onlyoverwriteifnewer'] = self.onlyoverwriteifnewer.isChecked() ini = unicode(self.ini.toPlainText()) diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 5bf97cc..a5bf35c 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -25,7 +25,7 @@ from calibre.gui2.threaded_jobs import ThreadedJob from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions from calibre_plugins.fanfictiondownloader_plugin.epubmerge import doMerge -from calibre_plugins.fanfictiondownloader_plugin.config import prefs +from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, CLIP, SELECTED) from calibre_plugins.fanfictiondownloader_plugin.dialogs import ( DownloadDialog, MetadataProgressDialog, UserPassDialog, OVERWRITE, UPDATE, ADDNEW, SKIP, CALIBREONLY, NotGoingToDownload ) @@ -91,55 +91,19 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # things. self.db = self.gui.current_db - ## if there's rows selected, try to find a source URL from - ## either identifier in the metadata, or from the epub - ## metadata. - url_list = [] - rows = self.gui.library_view.selectionModel().selectedRows() - if rows: - book_ids = self.gui.library_view.get_selected_ids() - print("book_ids: %s"%book_ids) - for book_id in book_ids: - identifiers = self.db.get_identifiers(book_id,index_is_id=True) - if 'url' in identifiers: - # identifiers have :->| in url. - #print("url from book:"+identifiers['url'].replace('|',':')) - url_list.append(identifiers['url'].replace('|',':')) - else: - ## only epub has that in it. - if self.db.has_format(book_id,'EPUB',index_is_id=True): - existingepub = self.db.format(book_id,'EPUB',index_is_id=True, as_file=True) - mi = get_metadata(existingepub,'EPUB') - #print("mi:%s"%mi) - identifiers = mi.get_identifiers() - if 'url' in identifiers: - #print("url from epub:"+identifiers['url'].replace('|',':')) - url_list.append(identifiers['url'].replace('|',':')) - else: - # no rows selected, check for valid URLs in the clipboard. - cliptext = unicode(QApplication.instance().clipboard().text()) - url_list.extend(cliptext.split()) + # pre-pop urls from selected stories or clipboard. but with + # configurable priority and on/off option on each + if prefs['urlsfrompriority'] == SELECTED: + from_first_func = self.get_urls_select + from_second_func = self.get_urls_clip + elif prefs['urlsfrompriority'] == CLIP: + from_first_func = self.get_urls_clip + from_second_func = self.get_urls_select + + url_list_text = from_first_func() + if not url_list_text: + url_list_text = from_second_func() - url_list_text = "" - # Check and make sure the URLs are valid ffdl URLs. - if url_list: - dummyconfig = ConfigParser.SafeConfigParser() - alreadyin=[] - for url in url_list: - if url in alreadyin: - continue - alreadyin.append(url) - # pulling up an adapter is pretty low over-head. If - # it fails, it's a bad url. - try: - adapters.getAdapter(dummyconfig,url) - except: - pass - else: - if url_list_text: - url_list_text += "\n" - url_list_text += url - #'''http://test1.com?sid=6 #''') # http://test1.com?sid=6701 @@ -166,6 +130,70 @@ class FanFictionDownLoaderPlugin(InterfaceAction): ) d.show() + ## if there's rows selected, try to find a source URL from + ## either identifier in the metadata, or from the epub + ## metadata. + def get_urls_select(self): + url_list = [] + rows = self.gui.library_view.selectionModel().selectedRows() + if rows and prefs['urlsfromselected']: + book_ids = self.gui.library_view.get_selected_ids() + print("book_ids: %s"%book_ids) + for book_id in book_ids: + identifiers = self.db.get_identifiers(book_id,index_is_id=True) + if 'url' in identifiers: + # identifiers have :->| in url. + #print("url from book:"+identifiers['url'].replace('|',':')) + url_list.append(identifiers['url'].replace('|',':')) + else: + ## only epub has that in it. + if self.db.has_format(book_id,'EPUB',index_is_id=True): + existingepub = self.db.format(book_id,'EPUB',index_is_id=True, as_file=True) + mi = get_metadata(existingepub,'EPUB') + #print("mi:%s"%mi) + identifiers = mi.get_identifiers() + if 'url' in identifiers: + #print("url from epub:"+identifiers['url'].replace('|',':')) + url_list.append(identifiers['url'].replace('|',':')) + return self.get_valid_urls(url_list) + + def get_urls_clip(self): + url_list = [] + if prefs['urlsfromclip']: + # no rows selected, check for valid URLs in the clipboard. + cliptext = unicode(QApplication.instance().clipboard().text()) + url_list.extend(cliptext.split()) + return self.get_valid_urls(url_list) + + def get_valid_urls(self,url_list): + url_list_text = "" + + # Check and make sure the URLs are valid ffdl URLs. + if url_list: + # this is the accepted way to 'check for existance'? really? + try: + self.dummyconfig + except AttributeError: + self.dummyconfig = ConfigParser.SafeConfigParser() + + alreadyin=[] + for url in url_list: + if url in alreadyin: + continue + alreadyin.append(url) + # pulling up an adapter is pretty low over-head. If + # it fails, it's a bad url. + try: + adapters.getAdapter(self.dummyconfig,url) + except: + pass + else: + if url_list_text: + url_list_text += "\n" + url_list_text += url + + return url_list_text + def apply_settings(self): # No need to do anything with perfs here, but we could. prefs @@ -419,6 +447,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # Otherwise list of books doesn't update right away. if addedcount: self.gui.library_view.model().books_added(addedcount) + + self.gui.library_view.model().refresh() + #self.gui.library_view.model().research() + #self.gui.tags_view.recount() del adapter del writer From e3ceeb937fb04ca304faccda791d10e4bbf9115d Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sun, 25 Dec 2011 17:36:36 -0600 Subject: [PATCH 19/30] Make clickable link and HTML formatting in About box. --- calibre-plugin/about.txt | 12 ++++++------ calibre-plugin/dialogs.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/calibre-plugin/about.txt b/calibre-plugin/about.txt index df43d96..5ee89ff 100644 --- a/calibre-plugin/about.txt +++ b/calibre-plugin/about.txt @@ -1,11 +1,11 @@ -FanFictionDownLoader Plugin -=========================== +

FanFictionDownLoader Plugin

+
-http://code.google.com/p/fanficdownloader/ +

http://code.google.com/p/fanficdownloader/

-Created by Jim Miller, borrowing heavily from +

Created by Jim Miller, borrowing heavily from Kovid Goyal's 'The InterfacePlugin Demo' and -Grant Drake's 'Count Pages' plugins. +Grant Drake's 'Count Pages' plugins.

-Requires calibre >= 0.7.53 +

Requires calibre >= 0.7.53

diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index 1ff0820..534ef4c 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -12,7 +12,7 @@ import traceback from PyQt4.Qt import (QDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, - QProgressDialog, QTimer ) + QProgressDialog, QTimer, QDialogButtonBox, QPixmap, Qt ) from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog @@ -123,8 +123,9 @@ 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 FanFictionDownLoader Plugin', - text.decode('utf-8')) +# QMessageBox.about(self, 'About the FanFictionDownLoader Plugin', + # text.decode('utf-8')) + AboutDialog(self.windowIcon(),text,self).exec_() def ffdl(self): self.start_downloads(unicode(self.url.toPlainText()), @@ -253,6 +254,30 @@ class MetadataProgressDialog(QProgressDialog): # show_copy_button=False).exec_() self.gui = None +class AboutDialog(QDialog): + + def __init__(self, icon, text, parent=None): + QDialog.__init__(self, parent) + self.resize(400, 250) + self.l = QGridLayout() + self.setLayout(self.l) + self.logo = QLabel() + self.logo.setMaximumWidth(110) + self.logo.setPixmap(QPixmap(icon.pixmap(100,100))) + self.label = QLabel(text) + self.label.setOpenExternalLinks(True) + self.label.setWordWrap(True) + self.setWindowTitle(_('Update available!')) + self.setWindowIcon(icon) + self.l.addWidget(self.logo, 0, 0) + self.l.addWidget(self.label, 0, 1) + self.bb = QDialogButtonBox(self) + b = self.bb.addButton(_('OK'), self.bb.AcceptRole) + b.setDefault(True) + self.l.addWidget(self.bb, 2, 0, 1, -1) + self.bb.accepted.connect(self.accept) + + class NotGoingToDownload(Exception): def __init__(self,error): self.error=error From aa2b1961e22870849195aa88ca859b34cb2ccc53 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 28 Dec 2011 10:53:26 -0600 Subject: [PATCH 20/30] Update version numbers for plugin. --- calibre-plugin/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index bfc4ad6..f7f821a 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -25,8 +25,8 @@ class InterfacePluginDemo(InterfaceActionBase): description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' - version = (1, 0, 0) - minimum_calibre_version = (0, 7, 53) + version = (1, 0, 2) + minimum_calibre_version = (0, 8, 32) # action_menu_clone_qaction = True From 307ff464f1243447e9b1922a030d424c51cd0517 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 28 Dec 2011 10:53:36 -0600 Subject: [PATCH 21/30] Added tag calibre-plugin-1.0.2 for changeset ff8743cdd18b From 851d3df75167c5932e1d4ae77a1fbbaac3e91e3d Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 28 Dec 2011 11:00:21 -0600 Subject: [PATCH 22/30] Pull minimum calibre version back down to 0.8.30 rather than latest/greatest. --- calibre-plugin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index f7f821a..1d81db9 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -26,7 +26,7 @@ class InterfacePluginDemo(InterfaceActionBase): supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' version = (1, 0, 2) - minimum_calibre_version = (0, 8, 32) + minimum_calibre_version = (0, 8, 30) # action_menu_clone_qaction = True From 102a55131dd3401eb23d57abb25c8fcf420d39f1 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 28 Dec 2011 13:54:52 -0600 Subject: [PATCH 23/30] Plugin wasn't including extratags in calibre tags. --- fanficdownloader/adapters/adapter_test1.py | 6 +++--- fanficdownloader/writers/base_writer.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py index 7d59b23..f00b963 100644 --- a/fanficdownloader/adapters/adapter_test1.py +++ b/fanficdownloader/adapters/adapter_test1.py @@ -104,9 +104,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!" ('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"), ('Chapter 2, Sinmay on Kintikin',self.url+"&chapter=3"), ('Chapter 3, Over Cinnabar',self.url+"&chapter=4"), - # ('Chapter 4',self.url+"&chapter=5"), - # ('Chapter 5',self.url+"&chapter=6"), - # ('Chapter 6',self.url+"&chapter=6"), + ('Chapter 4',self.url+"&chapter=5"), + ('Chapter 5',self.url+"&chapter=6"), + ('Chapter 6',self.url+"&chapter=6"), # ('Chapter 7',self.url+"&chapter=6"), # ('Chapter 8',self.url+"&chapter=6"), # ('Chapter 9',self.url+"&chapter=6"), diff --git a/fanficdownloader/writers/base_writer.py b/fanficdownloader/writers/base_writer.py index e01cadf..da9822c 100644 --- a/fanficdownloader/writers/base_writer.py +++ b/fanficdownloader/writers/base_writer.py @@ -258,6 +258,9 @@ class BaseStoryWriter(Configurable): if name in self.getConfigList("include_subject_tags"): for tag in lst: subjectset.add(tag) + + for tag in self.getConfigList("extratags"): + subjectset.add(tag) return subjectset From f9c173f12f50da7e4ee2e0349ffdaf046e505847 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 28 Dec 2011 13:57:58 -0600 Subject: [PATCH 24/30] Added tag calibre-plugin-1.0.2 for changeset 14b351e079c3 From db513cb62a6fa801d830cb37c7774cfad9c6d0ec Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 30 Dec 2011 14:04:56 -0600 Subject: [PATCH 25/30] Need makezip.py for makeplugin.py. --- calibre-plugin/ffdl_plugin.py | 21 +++++++++++++- makezip.py | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 makezip.py diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index a5bf35c..213c865 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -19,6 +19,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog +from calibre.gui2.tools import convert_single_ebook from calibre.gui2.actions import InterfaceAction from calibre.gui2.threaded_jobs import ThreadedJob @@ -385,7 +386,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): mi.comments = story.getMetadata("description") identicalbooks = self.db.find_identical_books(mi) - print(identicalbooks) + #print(identicalbooks) addedcount=0 if identicalbooks and collision != ADDNEW: ## more than one match? add to first off the list. @@ -436,6 +437,24 @@ class FanFictionDownLoaderPlugin(InterfaceAction): db.add_format_with_hooks(book_id, fileform, tmp, index_is_id=True) + # get all formats. + fmts = set([x.lower() for x in db.formats(book_id, index_is_id=True).split(',')]) + for fmt in fmts: + if fmt != fileform: + print("f:"+fmt) + ## calling convert doesn't work here because we're in a BG thread. + # (jobs,changed,bad)=convert_single_ebook(self.gui, + # db, + # [book_id], + # auto_conversion=True, + # out_format=fmt) + # print("jobs:%s changed:%s bad:%s"%(jobs,changed,bad)) + db.remove_format(book_id, fmt,index_is_id=True)#, notify=False + + for a in self.gui.iactions: # ['Convert Books'] + print("a:%s"%a) + + if updatemeta or collision == CALIBREONLY: db.set_metadata(book_id,mi) diff --git a/makezip.py b/makezip.py new file mode 100644 index 0000000..55a1019 --- /dev/null +++ b/makezip.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# epubmerge.py 1.0 + +# Copyright 2011, Jim Miller + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, zipfile, sys +from glob import glob + +def addFolderToZip(myZipFile,folder,exclude=[]): + folder = folder.encode('ascii') #convert path to ascii for ZipFile Method + excludelist=[] + for ex in exclude: + excludelist.extend(glob(folder+"/"+ex)) + for file in glob(folder+"/*"): + if file in excludelist: + continue + if os.path.isfile(file): + #print file + myZipFile.write(file, file, zipfile.ZIP_DEFLATED) + elif os.path.isdir(file): + addFolderToZip(myZipFile,file,exclude=exclude) + +def createZipFile(filename,mode,files,exclude=[]): + myZipFile = zipfile.ZipFile( filename, mode ) # Open the zip file for writing + excludelist=[] + for ex in exclude: + excludelist.extend(glob(ex)) + for file in files: + if file in excludelist: + continue + file = file.encode('ascii') #convert path to ascii for ZipFile Method + if os.path.isfile(file): + (filepath, filename) = os.path.split(file) + #print file + myZipFile.write( file, filename, zipfile.ZIP_DEFLATED ) + if os.path.isdir(file): + addFolderToZip(myZipFile,file,exclude=exclude) + myZipFile.close() + return (1,filename) + From af1f69facc097705747ca3b95160aeb213172947 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 30 Dec 2011 14:07:44 -0600 Subject: [PATCH 26/30] Once again accidently checked in code I'm still working on. Sorry. :-( --- calibre-plugin/ffdl_plugin.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 213c865..9f29bd3 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -19,7 +19,6 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog -from calibre.gui2.tools import convert_single_ebook from calibre.gui2.actions import InterfaceAction from calibre.gui2.threaded_jobs import ThreadedJob @@ -438,21 +437,21 @@ class FanFictionDownLoaderPlugin(InterfaceAction): db.add_format_with_hooks(book_id, fileform, tmp, index_is_id=True) # get all formats. - fmts = set([x.lower() for x in db.formats(book_id, index_is_id=True).split(',')]) - for fmt in fmts: - if fmt != fileform: - print("f:"+fmt) - ## calling convert doesn't work here because we're in a BG thread. - # (jobs,changed,bad)=convert_single_ebook(self.gui, - # db, - # [book_id], - # auto_conversion=True, - # out_format=fmt) - # print("jobs:%s changed:%s bad:%s"%(jobs,changed,bad)) - db.remove_format(book_id, fmt,index_is_id=True)#, notify=False + # fmts = set([x.lower() for x in db.formats(book_id, index_is_id=True).split(',')]) + # for fmt in fmts: + # if fmt != fileform: + # print("f:"+fmt) + # ## calling convert doesn't work here because we're in a BG thread. + # # (jobs,changed,bad)=convert_single_ebook(self.gui, + # # db, + # # [book_id], + # # auto_conversion=True, + # # out_format=fmt) + # # print("jobs:%s changed:%s bad:%s"%(jobs,changed,bad)) + # db.remove_format(book_id, fmt,index_is_id=True)#, notify=False - for a in self.gui.iactions: # ['Convert Books'] - print("a:%s"%a) + # for a in self.gui.iactions: # ['Convert Books'] + # print("a:%s"%a) if updatemeta or collision == CALIBREONLY: From 81e613413991cc080574bb9cdb7bef3e3f68bbf7 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 30 Dec 2011 14:12:58 -0600 Subject: [PATCH 27/30] Make plugin name consistent. --- calibre-plugin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 1d81db9..cde5afc 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -21,7 +21,7 @@ class InterfacePluginDemo(InterfaceActionBase): The reason for having two classes is that it allows the command line calibre utilities to run without needing to load the GUI libraries. ''' - name = 'FanFictionDownLoader Plugin' + name = 'FanFictionDownLoader' description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' From 4ac42526bd4475154dcd3639bf81de7c17e40d27 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 30 Dec 2011 14:24:30 -0600 Subject: [PATCH 28/30] Add 'delete other formats' option, bump to 1.0.3. --- calibre-plugin/__init__.py | 2 +- calibre-plugin/config.py | 7 +++++++ calibre-plugin/ffdl_plugin.py | 22 ++++++---------------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index cde5afc..7cabe48 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -25,7 +25,7 @@ class InterfacePluginDemo(InterfaceActionBase): description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' - version = (1, 0, 2) + version = (1, 0, 3) minimum_calibre_version = (0, 8, 30) # action_menu_clone_qaction = True diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index bf34380..6b78640 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -34,6 +34,7 @@ prefs.defaults['urlsfromselected'] = True prefs.defaults['urlsfrompriority'] = SELECTED prefs.defaults['fileform'] = 'epub' prefs.defaults['collision'] = OVERWRITE +prefs.defaults['deleteotherforms'] = False class ConfigWidget(QWidget): @@ -105,6 +106,11 @@ class ConfigWidget(QWidget): horz.addWidget(self.urlsfrompriority) self.l.addLayout(horz) + self.deleteotherforms = QCheckBox('Delete other existing formats?',self) + self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.') + self.deleteotherforms.setChecked(prefs['deleteotherforms']) + self.l.addWidget(self.deleteotherforms) + self.label = QLabel('personal.ini:') self.l.addWidget(self.label) @@ -121,6 +127,7 @@ class ConfigWidget(QWidget): prefs['urlsfromclip'] = self.urlsfromclip.isChecked() prefs['urlsfromselected'] = self.urlsfromselected.isChecked() prefs['onlyoverwriteifnewer'] = self.onlyoverwriteifnewer.isChecked() + prefs['deleteotherforms'] = self.deleteotherforms.isChecked() ini = unicode(self.ini.toPlainText()) if ini: diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 9f29bd3..2570764 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -437,22 +437,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction): db.add_format_with_hooks(book_id, fileform, tmp, index_is_id=True) # get all formats. - # fmts = set([x.lower() for x in db.formats(book_id, index_is_id=True).split(',')]) - # for fmt in fmts: - # if fmt != fileform: - # print("f:"+fmt) - # ## calling convert doesn't work here because we're in a BG thread. - # # (jobs,changed,bad)=convert_single_ebook(self.gui, - # # db, - # # [book_id], - # # auto_conversion=True, - # # out_format=fmt) - # # print("jobs:%s changed:%s bad:%s"%(jobs,changed,bad)) - # db.remove_format(book_id, fmt,index_is_id=True)#, notify=False - - # for a in self.gui.iactions: # ['Convert Books'] - # print("a:%s"%a) - + if prefs['deleteotherforms']: + fmts = set([x.lower() for x in db.formats(book_id, index_is_id=True).split(',')]) + for fmt in fmts: + if fmt != fileform: + print("remove f:"+fmt) + db.remove_format(book_id, fmt,index_is_id=True)#, notify=False if updatemeta or collision == CALIBREONLY: db.set_metadata(book_id,mi) From d455a97e3ba17220feaed9b1522ccfcbf7cc6820 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Fri, 30 Dec 2011 14:24:43 -0600 Subject: [PATCH 29/30] Added tag calibre-plugin-1.0.3 for changeset db74b56aa2f0 From 82efb462aaf8f244f0a5491ee1e8806f0ea23977 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sat, 31 Dec 2011 21:51:03 -0600 Subject: [PATCH 30/30] Add siye.co.uk. Plugin-add 'view defaults.ini', fix delete other forms, 1.0.4. --- app.yaml | 4 +- calibre-plugin/__init__.py | 2 +- calibre-plugin/about.txt | 2 +- calibre-plugin/config.py | 36 ++- calibre-plugin/ffdl_plugin.py | 31 +- fanficdownloader/adapters/__init__.py | 1 + fanficdownloader/adapters/adapter_siyecouk.py | 275 ++++++++++++++++++ fanficdownloader/story.py | 3 +- index.html | 15 +- 9 files changed, 335 insertions(+), 34 deletions(-) create mode 100644 fanficdownloader/adapters/adapter_siyecouk.py diff --git a/app.yaml b/app.yaml index d0c0668..a21f6c6 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ # ffd-retief-hrd fanfictiondownloader -application: ffd-retief-hrd -version: 4-1-1 +application: fanfictiondownloader +version: 4-2-0 runtime: python27 api_version: 1 threadsafe: true diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 7cabe48..a25fee9 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -25,7 +25,7 @@ class InterfacePluginDemo(InterfaceActionBase): description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' - version = (1, 0, 3) + version = (1, 0, 4) minimum_calibre_version = (0, 8, 30) # action_menu_clone_qaction = True diff --git a/calibre-plugin/about.txt b/calibre-plugin/about.txt index 5ee89ff..1d11808 100644 --- a/calibre-plugin/about.txt +++ b/calibre-plugin/about.txt @@ -7,5 +7,5 @@ Kovid Goyal's 'The InterfacePlugin Demo' and Grant Drake's 'Count Pages' plugins.

-

Requires calibre >= 0.7.53

+

Requires calibre >= 0.8.30

diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 6b78640..8fdb9bd 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -7,8 +7,8 @@ __license__ = 'GPL v3' __copyright__ = '2011, Jim Miller' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, - QComboBox, QCheckBox) +from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QTextEdit, QComboBox, QCheckBox, QPushButton) from calibre.utils.config import JSONConfig @@ -118,6 +118,11 @@ class ConfigWidget(QWidget): self.ini.setLineWrapMode(QTextEdit.NoWrap) self.ini.setText(prefs['personal.ini']) self.l.addWidget(self.ini) + + self.defaults = QPushButton('View Defaults', self) + self.defaults.setToolTip("View all of the plugin's configurable settings\nand their default settings.") + self.defaults.clicked.connect(self.show_defaults) + self.l.addWidget(self.defaults) def save_settings(self): prefs['fileform'] = unicode(self.fileform.currentText()) @@ -137,4 +142,31 @@ class ConfigWidget(QWidget): # default next time. del prefs['personal.ini'] + def show_defaults(self): + text = get_resources('defaults.ini') + ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_() + +class ShowDefaultsIniDialog(QDialog): + def __init__(self, icon, text, parent=None): + QDialog.__init__(self, parent) + self.resize(600, 500) + self.l = QVBoxLayout() + self.setLayout(self.l) + self.label = QLabel("Plugin Defaults (Read-Only)") + self.label.setToolTip("These all of the plugin's configurable settings\nand their default settings.") + self.setWindowTitle(_('Plugin Defaults')) + self.setWindowIcon(icon) + self.l.addWidget(self.label) + + self.ini = QTextEdit(self) + self.ini.setToolTip("These all of the plugin's configurable settings\nand their default settings.") + self.ini.setLineWrapMode(QTextEdit.NoWrap) + self.ini.setText(text) + self.ini.setReadOnly(True) + self.l.addWidget(self.ini) + + self.ok_button = QPushButton('OK', self) + self.ok_button.clicked.connect(self.hide) + self.l.addWidget(self.ok_button) + diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 2570764..4adbea7 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -104,19 +104,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if not url_list_text: url_list_text = from_second_func() - #'''http://test1.com?sid=6 -#''') -# 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 -# http://test1.com?sid=6707 -# http://test1.com?sid=6708 -# http://test1.com?sid=6709 - - # 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 @@ -138,8 +125,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction): rows = self.gui.library_view.selectionModel().selectedRows() if rows and prefs['urlsfromselected']: book_ids = self.gui.library_view.get_selected_ids() - print("book_ids: %s"%book_ids) + #print("book_ids: %s"%book_ids) for book_id in book_ids: + #print("book_on_device:%s"%self.db.book_on_device(book_id)) identifiers = self.db.get_identifiers(book_id,index_is_id=True) if 'url' in identifiers: # identifiers have :->| in url. @@ -264,6 +252,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if collision == SKIP: raise NotGoingToDownload("Skipping duplicate story.") + # print("Attempting to add to list.") + # rl_plugin = self.gui.iactions['Reading List'] + # rl_plugin.remove_books_from_list('Send',[book_id],refresh_screen=False) + # rl_plugin.add_books_to_list('Send',[book_id],refresh_screen=False) + #rl_plugin.view_list('Send') + if collision == OVERWRITE and len(identicalbooks) > 1: raise NotGoingToDownload("More than one identical books--can't tell which to overwrite.") @@ -391,6 +385,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): ## more than one match? add to first off the list. ## Shouldn't happen--we checked above. book_id = identicalbooks.pop() + if collision == UPDATE: if self.db.has_format(book_id,fileform,index_is_id=True): urlchaptercount = int(story.getMetadata('numChapters')) @@ -437,7 +432,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): db.add_format_with_hooks(book_id, fileform, tmp, index_is_id=True) # get all formats. - if prefs['deleteotherforms']: + if prefs['deleteotherforms'] and collision in (OVERWRITE, UPDATE): fmts = set([x.lower() for x in db.formats(book_id, index_is_id=True).split(',')]) for fmt in fmts: if fmt != fileform: @@ -446,7 +441,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if updatemeta or collision == CALIBREONLY: db.set_metadata(book_id,mi) - + else: # no matching, adding new. writer.writeStory(tmp) (notadded,addedcount)=db.add_books([tmp],[fileform],[mi], add_duplicates=True) @@ -455,10 +450,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # Otherwise list of books doesn't update right away. if addedcount: self.gui.library_view.model().books_added(addedcount) - + self.gui.library_view.model().refresh() - #self.gui.library_view.model().research() - #self.gui.tags_view.recount() del adapter del writer diff --git a/fanficdownloader/adapters/__init__.py b/fanficdownloader/adapters/__init__.py index 70f5006..89ef2c9 100644 --- a/fanficdownloader/adapters/__init__.py +++ b/fanficdownloader/adapters/__init__.py @@ -42,6 +42,7 @@ import adapter_tthfanficorg import adapter_twilightednet import adapter_twiwritenet import adapter_whoficcom +import adapter_siyecouk ## This bit of complexity allows adapters to be added by just adding ## importing. It eliminates the long if/else clauses we used to need diff --git a/fanficdownloader/adapters/adapter_siyecouk.py b/fanficdownloader/adapters/adapter_siyecouk.py new file mode 100644 index 0000000..8222b15 --- /dev/null +++ b/fanficdownloader/adapters/adapter_siyecouk.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return SiyeCoUkAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SiyeCoUkAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8",]# 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + # self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + # self.password = "" + # self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + logging.debug("storyId: (%s)"%self.story.getMetadata('storyId')) + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/siye/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','siye') # XXX + + # If all stories from the site fall into the same category, + # the site itself isn't likely to label them as such, so we + # do. + self.story.addToList("category","Harry Potter") # XXX + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y.%m.%d" # XXX + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.siye.co.uk' # XXX + + @classmethod + def getAcceptDomains(cls): + return ['www.siye.co.uk','siye.co.uk'] + + def getSiteExampleURLs(self): + return "http://"+self.getSiteDomain()+"/siye/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?"+re.escape("siye.co.uk/siye/viewstory.php?sid=")+r"\d+$" + + # ## Login seems to be reasonably standard across eFiction sites. + # def needToLoginCheck(self, data): + # if 'Registered Users Only' in data \ + # or 'There is no such account on our website' in data \ + # or "That password doesn't match the one in our database" in data: + # return True + # else: + # return False + + # def performLogin(self, url): + # params = {} + + # if self.password: + # params['penname'] = self.username + # params['password'] = self.password + # else: + # params['penname'] = self.getConfig("username") + # params['password'] = self.getConfig("password") + # params['cookiecheck'] = '1' + # params['submit'] = 'Submit' + + # loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login' + # logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + # params['penname'])) + + # d = self._fetchUrl(loginUrl, params) + + # if "Member Account" not in d : #Member Account + # logging.info("Failed to login to URL %s as %s" % (loginUrl, + # params['penname'])) + # raise exceptions.FailedToLogin(url,params['penname']) + # return False + # else: + # return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # if self.is_adult or self.getConfig("is_adult"): + # # Weirdly, different sites use different warning numbers. + # # If the title search below fails, there's a good chance + # # you need a different number. print data at that point + # # and see what the 'click here to continue' url says. + # addurl = "&ageconsent=ok&warning=4" # XXX + # else: + # addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + # Except it doesn't this time. :-/ + url = self.url #+'&index=1'+addurl + logging.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # if self.needToLoginCheck(data): + # # need to log in for this one. + # self.performLogin(url) + # data = self._fetchUrl(url) + + # # The actual text that is used to announce you need to be an + # # adult varies from site to site. Again, print data before + # # the title search to troubleshoot. + # if "Age Consent Required" in data: # XXX + # raise exceptions.AdultCheckRequired(self.url) + + # if "Access denied. This story has not been validated by the adminstrators of this site." in data: + # raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/siye/'+a['href']) + self.story.setMetadata('author',a.string) + + # need(or easier) to pull other metadata from the author's list page. + authsoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + ## Title + titlea = authsoup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',titlea.string) + + # Find the chapters (from soup, not authsoup): + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/siye/'+chapter['href'])) + + if self.chapterUrls: + self.story.setMetadata('numChapters',len(self.chapterUrls)) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + self.story.setMetadata('numChapters',1) + + # The stuff we can get from the chapter list/one-shot page are + # in the first table with 95% width. + metatable = soup.find('table',{'width':'95%'}) + + # Categories + cat_as = metatable.findAll('a', href=re.compile(r'categories.php')) + for cat_a in cat_as: + self.story.addToList('category',stripHTML(cat_a)) + + moremetaparts = stripHTML(metatable).split('\n') + for part in moremetaparts: + part = part.strip() + if part.startswith("Characters:"): + part = part[part.find(':')+1:] + for item in part.split(','): + if item.strip() != "None": + self.story.addToList('characters',item) + + if part.startswith("Genres:"): + part = part[part.find(':')+1:] + for item in part.split(','): + if item.strip() != "None": + self.story.addToList('genre',item) + + if part.startswith("Warnings:"): + part = part[part.find(':')+1:] + for item in part.split(','): + if item.strip() != "None": + self.story.addToList('warnings',item) + + if part.startswith("Rating:"): + part = part[part.find(':')+1:] + self.story.setMetadata('rating',part) + + if part.startswith("Summary:"): + part = part[part.find(':')+1:] + self.story.setMetadata('description',part) + + + + # want to get the next tr of the table. + #print("%s"%titlea.parent.parent.findNextSibling('tr')) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + moremeta = stripHTML(titlea.parent.parent.findNextSibling('tr')) + for part in moremeta.replace(' - ','\n').split('\n'): + #print("part:%s"%part) + try: + (name,value) = part.split(': ') + except: + # not going to worry about fancier processing for the bits + # that don't match. + continue + name=name.strip() + value=value.strip() + if name == 'Published': + self.story.setMetadata('datePublished', makeDate(value, self.dateformat)) + if name == 'Updated': + self.story.setMetadata('dateUpdated', makeDate(value, self.dateformat)) + if name == 'Completed': + if value == 'Yes': + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + if name == 'Words': + self.story.setMetadata('numWords', value) + + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logging.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url)) + + # not the most unique thing in the work, bit it appears to be + # the best we can do there. + story = soup.find('span', {'style' : 'font-size: 100%;'}) + + if None == story: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return utf8FromSoup(story) diff --git a/fanficdownloader/story.py b/fanficdownloader/story.py index 68b24be..2768854 100644 --- a/fanficdownloader/story.py +++ b/fanficdownloader/story.py @@ -53,11 +53,12 @@ class Story: def addToList(self,listname,value): if value==None: return + value = conditionalRemoveEntities(value) if not self.listables.has_key(listname): self.listables[listname]=[] # prevent duplicates. if not value in self.listables[listname]: - self.listables[listname].append(conditionalRemoveEntities(value)) + self.listables[listname].append(value) def getList(self,listname): if not self.listables.has_key(listname): diff --git a/index.html b/index.html index 9b10d0b..bcde2fb 100644 --- a/index.html +++ b/index.html @@ -61,14 +61,8 @@ considers Python 2.7 Experimental still, so there may be issues.

- Good news!
- The issue that was causing problems with downloading large stories - has been fixed. -

-

- New Feature
You can now set a custom - parameter for background_color that will be used with html - and epub output. (Note: many epub readers ignore the bg color.) + New Site
+ Now supporting www.siye.co.uk.

If you have any problems with this application, please @@ -215,6 +209,11 @@
http://www.tthfanfic.org/Story-5583/Greywizard+Marked+By+Kane.htm.
http://www.tthfanfic.org/T-99999999/Story-26448-15/batzulger+Willow+Rosenberg+and+the+Mind+Riders.htm. +

www.siye.co.uk
+
+ Use the URL of the story's chapter list, such as +
http://www.siye.co.uk/siye/viewstory.php?sid=123. +