mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-15 11:36:30 +08:00
Compare commits
111
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857ae5b7fa | ||
|
|
1037fff11f | ||
|
|
f611a1e26f | ||
|
|
f3cfa72051 | ||
|
|
f13a698808 | ||
|
|
18064c269b | ||
|
|
ccb4af4f63 | ||
|
|
0ee30ddeac | ||
|
|
2bb5e5b74e | ||
|
|
f21553bc4a | ||
|
|
16ff8a1a23 | ||
|
|
045d43c040 | ||
|
|
f3d88d73f3 | ||
|
|
943d34ba35 | ||
|
|
ba022ec34e | ||
|
|
c1914651aa | ||
|
|
dfef356249 | ||
|
|
d78709f850 | ||
|
|
d6e8e69eaf | ||
|
|
0626649f1c | ||
|
|
565569e819 | ||
|
|
97e3f5796b | ||
|
|
c1a3abb773 | ||
|
|
fc0f970c61 | ||
|
|
4208f4ae6e | ||
|
|
d89a5132ce | ||
|
|
a763fc3650 | ||
|
|
c37b91a093 | ||
|
|
4f85d4eb41 | ||
|
|
44f42c0db7 | ||
|
|
3e3b24e921 | ||
|
|
d0a952ac3a | ||
|
|
e8e4180621 | ||
|
|
7d29b281b7 | ||
|
|
98460d785d | ||
|
|
37803690e5 | ||
|
|
c6ddd8e6d7 | ||
|
|
3b04b6ad61 | ||
|
|
f046605517 | ||
|
|
e504ee29c1 | ||
|
|
22994d203a | ||
|
|
cbc02b749b | ||
|
|
8761b766ca | ||
|
|
44bd7f6319 | ||
|
|
9d8508ee6f | ||
|
|
df5a91daed | ||
|
|
2195ea5792 | ||
|
|
03da5f8eb8 | ||
|
|
1b9412e36e | ||
|
|
ba7b718170 | ||
|
|
a43d9f7a03 | ||
|
|
4b17ecf6fa | ||
|
|
b6dd579c93 | ||
|
|
aa685a4c7d | ||
|
|
b0248daf07 | ||
|
|
be5fe49ab8 | ||
|
|
f42f440f1b | ||
|
|
cbf50a36ee | ||
|
|
fda0fda84e | ||
|
|
c6f5c524be | ||
|
|
00a46a7cc0 | ||
|
|
5c1ca5a188 | ||
|
|
17c6dddfac | ||
|
|
33451f1119 | ||
|
|
192ade1fca | ||
|
|
aa286a9d0d | ||
|
|
a1c19ac12e | ||
|
|
85b6e305be | ||
|
|
40fb061a86 | ||
|
|
7fdc59691f | ||
|
|
78845d0d1e | ||
|
|
786b1d5cdf | ||
|
|
b5d176f007 | ||
|
|
1c71dedfa3 | ||
|
|
2484f0f5c7 | ||
|
|
a087927929 | ||
|
|
e64d49e3e6 | ||
|
|
d9ad95467b | ||
|
|
668e0e08b5 | ||
|
|
2b0b4ca2af | ||
|
|
dca3707eac | ||
|
|
a3b23857a3 | ||
|
|
9cdccb576c | ||
|
|
cf24021f14 | ||
|
|
0bbe6287b6 | ||
|
|
0c585b6e52 | ||
|
|
aacf6dc6a2 | ||
|
|
9f56952950 | ||
|
|
c49cad8889 | ||
|
|
bf1d8e18bf | ||
|
|
e41c7dabeb | ||
|
|
face0af074 | ||
|
|
02f38eaa2e | ||
|
|
d4b5438bc5 | ||
|
|
83db111ba7 | ||
|
|
2eb8c27c80 | ||
|
|
a79066750c | ||
|
|
3857a6a89b | ||
|
|
016d4af149 | ||
|
|
a4e499866e | ||
|
|
5736f4f181 | ||
|
|
e04453d73f | ||
|
|
c3778502dc | ||
|
|
925b12e776 | ||
|
|
62ed503d10 | ||
|
|
7e35d0b710 | ||
|
|
20c7e7f075 | ||
|
|
9f61cff7cd | ||
|
|
cb5587b0c7 | ||
|
|
f2a74c9ffc | ||
|
|
eb0f013550 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-68
|
||||
version: 4-4-85
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -7,6 +7,22 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
if sys.version_info >= (2, 7):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
load_translations()
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
# The class that all Interface Action plugin wrappers must inherit from
|
||||
from calibre.customize import InterfaceActionBase
|
||||
|
||||
@@ -23,10 +39,10 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
|
||||
calibre utilities to run without needing to load the GUI libraries.
|
||||
'''
|
||||
name = 'FanFictionDownLoader'
|
||||
description = 'UI plugin to download FanFiction stories from various sites.'
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (1, 7, 39)
|
||||
version = (1, 8, 01)
|
||||
minimum_calibre_version = (0, 8, 57)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
+228
-180
@@ -7,6 +7,9 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import traceback, copy, threading
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -19,9 +22,36 @@ from calibre.gui2.ui import get_gui
|
||||
from calibre.gui2 import dynamic, info_dialog
|
||||
from calibre.constants import numeric_version as calibre_version
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
load_translations()
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
# There are a number of things used several times that shouldn't be
|
||||
# translated. This is just a way to make that easier by keeping them
|
||||
# out of the _() strings.
|
||||
# I'm tempted to override _() to include them...
|
||||
no_trans = { 'pini':'personal.ini',
|
||||
'imgset':'\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n',
|
||||
'gcset':'generate_cover_settings',
|
||||
'ccset':'custom_columns_settings',
|
||||
'gc':'Generate Cover',
|
||||
'rl':'Reading List',
|
||||
'cp':'Count Pages',
|
||||
'cmplt':'Completed',
|
||||
'inprog':'In-Progress',
|
||||
'lul':'Last Updated',
|
||||
'lus':'lastupdate',
|
||||
'is':'include_subject',
|
||||
'isa':'is_adult',
|
||||
'u':'username',
|
||||
'p':'password',
|
||||
}
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.prefs import prefs, PREFS_NAMESPACE
|
||||
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
|
||||
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order, RejectListDialog,
|
||||
import (UPDATE, UPDATEALWAYS, collision_order, save_collisions, RejectListDialog,
|
||||
EditTextDialog, RejectUrlEntry)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters \
|
||||
@@ -122,15 +152,21 @@ class ConfigWidget(QWidget):
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel('<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderSupportedsites">List of Supported Sites</a> -- <a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>')
|
||||
label = QLabel('<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderSupportedsites">'+_('List of Supported Sites')+'</a> -- <a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">'+_('FAQs')+'</a>')
|
||||
label.setOpenExternalLinks(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
tab_widget = QTabWidget(self)
|
||||
self.l.addWidget(tab_widget)
|
||||
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setFrameShape(QScrollArea.NoFrame)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.l.addWidget(self.scroll_area)
|
||||
|
||||
tab_widget = QTabWidget(self)
|
||||
self.scroll_area.setWidget(tab_widget)
|
||||
|
||||
self.basic_tab = BasicTab(self, plugin_action)
|
||||
tab_widget.addTab(self.basic_tab, 'Basic')
|
||||
tab_widget.addTab(self.basic_tab, _('Basic'))
|
||||
|
||||
self.personalini_tab = PersonalIniTab(self, plugin_action)
|
||||
tab_widget.addTab(self.personalini_tab, 'personal.ini')
|
||||
@@ -151,26 +187,27 @@ class ConfigWidget(QWidget):
|
||||
self.countpages_tab.setEnabled(False)
|
||||
|
||||
self.std_columns_tab = StandardColumnsTab(self, plugin_action)
|
||||
tab_widget.addTab(self.std_columns_tab, 'Standard Columns')
|
||||
tab_widget.addTab(self.std_columns_tab, _('Standard Columns'))
|
||||
|
||||
self.cust_columns_tab = CustomColumnsTab(self, plugin_action)
|
||||
tab_widget.addTab(self.cust_columns_tab, 'Custom Columns')
|
||||
tab_widget.addTab(self.cust_columns_tab, _('Custom Columns'))
|
||||
|
||||
self.other_tab = OtherTab(self, plugin_action)
|
||||
tab_widget.addTab(self.other_tab, 'Other')
|
||||
tab_widget.addTab(self.other_tab, _('Other'))
|
||||
|
||||
|
||||
def save_settings(self):
|
||||
|
||||
# basic
|
||||
prefs['fileform'] = unicode(self.basic_tab.fileform.currentText())
|
||||
prefs['collision'] = unicode(self.basic_tab.collision.currentText())
|
||||
prefs['collision'] = save_collisions[unicode(self.basic_tab.collision.currentText())]
|
||||
prefs['updatemeta'] = self.basic_tab.updatemeta.isChecked()
|
||||
prefs['updatecover'] = self.basic_tab.updatecover.isChecked()
|
||||
prefs['updateepubcover'] = self.basic_tab.updateepubcover.isChecked()
|
||||
prefs['keeptags'] = self.basic_tab.keeptags.isChecked()
|
||||
prefs['suppressauthorsort'] = self.basic_tab.suppressauthorsort.isChecked()
|
||||
prefs['suppresstitlesort'] = self.basic_tab.suppresstitlesort.isChecked()
|
||||
prefs['mark'] = self.basic_tab.mark.isChecked()
|
||||
prefs['showmarked'] = self.basic_tab.showmarked.isChecked()
|
||||
prefs['urlsfromclip'] = self.basic_tab.urlsfromclip.isChecked()
|
||||
prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked()
|
||||
@@ -274,17 +311,17 @@ class BasicTab(QWidget):
|
||||
topl = QVBoxLayout()
|
||||
self.setLayout(topl)
|
||||
|
||||
label = QLabel('These settings control the basic features of the plugin--downloading FanFiction.')
|
||||
label = QLabel(_('These settings control the basic features of the plugin--downloading FanFiction.'))
|
||||
label.setWordWrap(True)
|
||||
topl.addWidget(label)
|
||||
|
||||
defs_gb = groupbox = QGroupBox("Defaults Options on Download")
|
||||
defs_gb = groupbox = QGroupBox(_("Defaults Options on Download"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
|
||||
tooltip = "On each download, FFDL offers an option to select the output format. <br />This sets what that option will default to."
|
||||
tooltip = _("On each download, FFDL offers an option to select the output format. <br />This sets what that option will default to.")
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('Default Output &Format:')
|
||||
label = QLabel(_('Default Output &Format:'))
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
self.fileform = QComboBox(self)
|
||||
@@ -299,15 +336,15 @@ class BasicTab(QWidget):
|
||||
horz.addWidget(self.fileform)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
tooltip = "On each download, FFDL offers an option of what happens if that story already exists. <br />This sets what that option will default to."
|
||||
tooltip = _("On each download, FFDL offers an option of what happens if that story already exists. <br />This sets what that option will default to.")
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('Default If Story Already Exists?')
|
||||
label = QLabel(_('Default If Story Already Exists?'))
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
i = self.collision.findText(save_collisions[prefs['collision']])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
self.collision.setToolTip(tooltip)
|
||||
@@ -315,137 +352,143 @@ class BasicTab(QWidget):
|
||||
horz.addWidget(self.collision)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.updatemeta = QCheckBox('Default Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off. <br />Columns set to 'New Only' in the column tabs will only be set for new books.")
|
||||
self.updatemeta = QCheckBox(_('Default Update Calibre &Metadata?'),self)
|
||||
self.updatemeta.setToolTip(_("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off. <br />Columns set to 'New Only' in the column tabs will only be set for new books."))
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
self.l.addWidget(self.updatemeta)
|
||||
|
||||
self.updateepubcover = QCheckBox('Default Update EPUB Cover when Updating EPUB?',self)
|
||||
self.updateepubcover.setToolTip("On each download, FFDL offers an option to update the book cover image <i>inside</i> the EPUB from the web site when the EPUB is updated.<br />This sets whether that will default to on or off.")
|
||||
self.updateepubcover = QCheckBox(_('Default Update EPUB Cover when Updating EPUB?'),self)
|
||||
self.updateepubcover.setToolTip(_("On each download, FFDL offers an option to update the book cover image <i>inside</i> the EPUB from the web site when the EPUB is updated.<br />This sets whether that will default to on or off."))
|
||||
self.updateepubcover.setChecked(prefs['updateepubcover'])
|
||||
self.l.addWidget(self.updateepubcover)
|
||||
|
||||
self.smarten_punctuation = QCheckBox('Smarten Punctuation (EPUB only)',self)
|
||||
self.smarten_punctuation.setToolTip("Run Smarten Punctuation from Calibre's Polish Book feature on each EPUB download and update.")
|
||||
self.smarten_punctuation = QCheckBox(_('Smarten Punctuation (EPUB only)'),self)
|
||||
self.smarten_punctuation.setToolTip(_("Run Smarten Punctuation from Calibre's Polish Book feature on each EPUB download and update."))
|
||||
self.smarten_punctuation.setChecked(prefs['smarten_punctuation'])
|
||||
if calibre_version >= (0, 9, 39):
|
||||
self.l.addWidget(self.smarten_punctuation)
|
||||
|
||||
cali_gb = groupbox = QGroupBox("Updating Calibre Options")
|
||||
cali_gb = groupbox = QGroupBox(_("Updating Calibre Options"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
|
||||
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 = 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.updatecover = QCheckBox('Update Calibre Cover when Updating Metadata?',self)
|
||||
self.updatecover.setToolTip("Update calibre book cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'.")
|
||||
self.updatecover = QCheckBox(_('Update Calibre Cover when Updating Metadata?'),self)
|
||||
self.updatecover.setToolTip(_("Update calibre book cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'."))
|
||||
self.updatecover.setChecked(prefs['updatecover'])
|
||||
self.l.addWidget(self.updatecover)
|
||||
|
||||
self.keeptags = QCheckBox('Keep Existing Tags when Updating Metadata?',self)
|
||||
self.keeptags.setToolTip("Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")
|
||||
self.keeptags = QCheckBox(_('Keep Existing Tags when Updating Metadata?'),self)
|
||||
self.keeptags.setToolTip(_("Existing tags will be kept and any new tags added.\n%(cmplt)s and %(inprog)s tags will be still be updated, if known.\n%(lul)s tags will be updated if %(lus)s in %(is)s.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")%no_trans)
|
||||
self.keeptags.setChecked(prefs['keeptags'])
|
||||
self.l.addWidget(self.keeptags)
|
||||
|
||||
self.suppressauthorsort = QCheckBox('Force Author into Author Sort?',self)
|
||||
self.suppressauthorsort.setToolTip("If checked, the author(s) as given will be used for the Author Sort, too.\nIf not checked, calibre will apply it's built in algorithm which makes 'Bob Smith' sort as 'Smith, Bob', etc.")
|
||||
self.suppressauthorsort = QCheckBox(_('Force Author into Author Sort?'),self)
|
||||
self.suppressauthorsort.setToolTip(_("If checked, the author(s) as given will be used for the Author Sort, too.\nIf not checked, calibre will apply it's built in algorithm which makes 'Bob Smith' sort as 'Smith, Bob', etc."))
|
||||
self.suppressauthorsort.setChecked(prefs['suppressauthorsort'])
|
||||
self.l.addWidget(self.suppressauthorsort)
|
||||
|
||||
self.suppresstitlesort = QCheckBox('Force Title into Title Sort?',self)
|
||||
self.suppresstitlesort.setToolTip("If checked, the title as given will be used for the Title Sort, too.\nIf not checked, calibre will apply it's built in algorithm which makes 'The Title' sort as 'Title, The', etc.")
|
||||
self.suppresstitlesort = QCheckBox(_('Force Title into Title Sort?'),self)
|
||||
self.suppresstitlesort.setToolTip(_("If checked, the title as given will be used for the Title Sort, too.\nIf not checked, calibre will apply it's built in algorithm which makes 'The Title' sort as 'Title, The', etc."))
|
||||
self.suppresstitlesort.setChecked(prefs['suppresstitlesort'])
|
||||
self.l.addWidget(self.suppresstitlesort)
|
||||
|
||||
self.checkforseriesurlid = QCheckBox("Check for existing Series Anthology books?",self)
|
||||
self.checkforseriesurlid.setToolTip("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.")
|
||||
self.checkforseriesurlid = QCheckBox(_("Check for existing Series Anthology books?"),self)
|
||||
self.checkforseriesurlid.setToolTip(_("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found."))
|
||||
self.checkforseriesurlid.setChecked(prefs['checkforseriesurlid'])
|
||||
self.l.addWidget(self.checkforseriesurlid)
|
||||
|
||||
self.checkforurlchange = QCheckBox("Check for changed Story URL?",self)
|
||||
self.checkforurlchange.setToolTip("Warn you if an update will change the URL of an existing book.")
|
||||
self.checkforurlchange = QCheckBox(_("Check for changed Story URL?"),self)
|
||||
self.checkforurlchange.setToolTip(_("Warn you if an update will change the URL of an existing book.\nfanfiction.net URLs will change from http to https silently."))
|
||||
self.checkforurlchange.setChecked(prefs['checkforurlchange'])
|
||||
self.l.addWidget(self.checkforurlchange)
|
||||
|
||||
self.lookforurlinhtml = QCheckBox("Search EPUB text for Story URL?",self)
|
||||
self.lookforurlinhtml.setToolTip("Look for first valid story URL inside EPUB text if not found in metadata.\nSomewhat risky, could find wrong URL depending on EPUB content.\nAlso finds and corrects bad ffnet URLs from ficsaver.com files.")
|
||||
self.lookforurlinhtml = QCheckBox(_("Search EPUB text for Story URL?"),self)
|
||||
self.lookforurlinhtml.setToolTip(_("Look for first valid story URL inside EPUB text if not found in metadata.\nSomewhat risky, could find wrong URL depending on EPUB content.\nAlso finds and corrects bad ffnet URLs from ficsaver.com files."))
|
||||
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
|
||||
self.l.addWidget(self.lookforurlinhtml)
|
||||
|
||||
self.showmarked = QCheckBox("Show added/updated books when finished?",self)
|
||||
self.showmarked.setToolTip("Show added/updated books only when finished.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both.")
|
||||
self.mark = QCheckBox(_("Mark added/updated books when finished?"),self)
|
||||
self.mark.setToolTip(_("Mark added/updated books when finished. Use with option below.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both."))
|
||||
self.mark.setChecked(prefs['mark'])
|
||||
self.l.addWidget(self.mark)
|
||||
|
||||
self.showmarked = QCheckBox(_("Show Marked books when finished?"),self)
|
||||
self.showmarked.setToolTip(_("Show Marked added/updated books only when finished.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both."))
|
||||
self.showmarked.setChecked(prefs['showmarked'])
|
||||
self.l.addWidget(self.showmarked)
|
||||
|
||||
gui_gb = groupbox = QGroupBox("GUI Options")
|
||||
gui_gb = groupbox = QGroupBox(_("GUI Options"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
|
||||
self.urlsfromclip = QCheckBox('Take URLs from Clipboard?',self)
|
||||
self.urlsfromclip.setToolTip('Prefill URLs from valid URLs in Clipboard when Adding New.')
|
||||
self.urlsfromclip = QCheckBox(_('Take URLs from Clipboard?'),self)
|
||||
self.urlsfromclip.setToolTip(_('Prefill URLs from valid URLs in Clipboard when Adding New.'))
|
||||
self.urlsfromclip.setChecked(prefs['urlsfromclip'])
|
||||
self.l.addWidget(self.urlsfromclip)
|
||||
|
||||
self.updatedefault = QCheckBox('Default to Update when books selected?',self)
|
||||
self.updatedefault.setToolTip('The top FanFictionDownLoader plugin button will start Update if\n'+
|
||||
'books are selected. If unchecked, it will always bring up \'Add New\'.')
|
||||
self.updatedefault = QCheckBox(_('Default to Update when books selected?'),self)
|
||||
self.updatedefault.setToolTip(_('The top FanFictionDownLoader plugin button will start Update if\nbooks are selected. If unchecked, it will always bring up \'Add New\'.'))
|
||||
self.updatedefault.setChecked(prefs['updatedefault'])
|
||||
self.l.addWidget(self.updatedefault)
|
||||
|
||||
self.adddialogstaysontop = QCheckBox("Keep 'Add New from URL(s)' dialog on top?",self)
|
||||
self.adddialogstaysontop.setToolTip("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it.")
|
||||
self.adddialogstaysontop = QCheckBox(_("Keep 'Add New from URL(s)' dialog on top?"),self)
|
||||
self.adddialogstaysontop.setToolTip(_("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it."))
|
||||
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
|
||||
self.l.addWidget(self.adddialogstaysontop)
|
||||
|
||||
misc_gb = groupbox = QGroupBox("Misc Options")
|
||||
misc_gb = groupbox = QGroupBox(_("Misc Options"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
|
||||
# this is a cheat to make it easier for users to realize there's a new include_images features.
|
||||
self.includeimages = QCheckBox("Include images in EPUBs?",self)
|
||||
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
|
||||
self.includeimages = QCheckBox(_("Include images in EPUBs?"),self)
|
||||
self.includeimages.setToolTip(_("Download and include images in EPUB stories. This is equivalent to adding:%(imgset)s ...to the top of %(pini)s. Your settings in %(pini)s will override this.")%no_trans)
|
||||
self.includeimages.setChecked(prefs['includeimages'])
|
||||
self.l.addWidget(self.includeimages)
|
||||
|
||||
self.injectseries = QCheckBox("Inject calibre Series when none found?",self)
|
||||
self.injectseries.setToolTip("If no series is found, inject the calibre series (if there is one) so it appears on the FFDL title page(not cover).")
|
||||
self.injectseries = QCheckBox(_("Inject calibre Series when none found?"),self)
|
||||
self.injectseries.setToolTip(_("If no series is found, inject the calibre series (if there is one) so it appears on the FFDL title page(not cover)."))
|
||||
self.injectseries.setChecked(prefs['injectseries'])
|
||||
self.l.addWidget(self.injectseries)
|
||||
|
||||
rej_gb = groupbox = QGroupBox("Reject List")
|
||||
rej_gb = groupbox = QGroupBox(_("Reject List"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
|
||||
self.rejectlist = QPushButton('Edit Reject URL List', self)
|
||||
self.rejectlist.setToolTip("Edit list of URLs FFDL will automatically Reject.")
|
||||
self.rejectlist = QPushButton(_('Edit Reject URL List'), self)
|
||||
self.rejectlist.setToolTip(_("Edit list of URLs FFDL will automatically Reject."))
|
||||
self.rejectlist.clicked.connect(self.show_rejectlist)
|
||||
self.l.addWidget(self.rejectlist)
|
||||
|
||||
self.reject_urls = QPushButton('Add Reject URLs', self)
|
||||
self.reject_urls.setToolTip("Add additional URLs to Reject as text.")
|
||||
self.reject_urls = QPushButton(_('Add Reject URLs'), self)
|
||||
self.reject_urls.setToolTip(_("Add additional URLs to Reject as text."))
|
||||
self.reject_urls.clicked.connect(self.add_reject_urls)
|
||||
self.l.addWidget(self.reject_urls)
|
||||
|
||||
self.reject_reasons = QPushButton('Edit Reject Reasons List', self)
|
||||
self.reject_reasons.setToolTip("Customize the Reasons presented when Rejecting URLs")
|
||||
self.reject_reasons = QPushButton(_('Edit Reject Reasons List'), self)
|
||||
self.reject_reasons.setToolTip(_("Customize the Reasons presented when Rejecting URLs"))
|
||||
self.reject_reasons.clicked.connect(self.show_reject_reasons)
|
||||
self.l.addWidget(self.reject_reasons)
|
||||
|
||||
topl.addWidget(defs_gb)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
topl.addLayout(horz)
|
||||
horz.addWidget(cali_gb)
|
||||
horz.addWidget(rej_gb)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
topl.addLayout(horz)
|
||||
horz.addWidget(gui_gb)
|
||||
horz.addWidget(misc_gb)
|
||||
|
||||
horz.addWidget(cali_gb)
|
||||
|
||||
vert = QVBoxLayout()
|
||||
vert.addWidget(gui_gb)
|
||||
vert.addWidget(misc_gb)
|
||||
vert.addWidget(rej_gb)
|
||||
|
||||
horz.addLayout(vert)
|
||||
|
||||
topl.addLayout(horz)
|
||||
topl.insertStretch(-1)
|
||||
|
||||
def set_collisions(self):
|
||||
@@ -466,7 +509,7 @@ class BasicTab(QWidget):
|
||||
d = RejectListDialog(self,
|
||||
rejecturllist.get_list(),
|
||||
rejectreasons=rejecturllist.get_reject_reasons(),
|
||||
header="Edit Reject URLs List",
|
||||
header=_("Edit Reject URLs List"),
|
||||
show_delete=False,
|
||||
show_all_reasons=False)
|
||||
d.exec_()
|
||||
@@ -480,22 +523,22 @@ class BasicTab(QWidget):
|
||||
d = EditTextDialog(self,
|
||||
prefs['rejectreasons'],
|
||||
icon=self.windowIcon(),
|
||||
title="Reject Reasons",
|
||||
label="Customize Reject List Reasons",
|
||||
tooltip="Customize the Reasons presented when Rejecting URLs")
|
||||
title=_("Reject Reasons"),
|
||||
label=_("Customize Reject List Reasons"),
|
||||
tooltip=_("Customize the Reasons presented when Rejecting URLs"))
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
prefs['rejectreasons'] = d.get_plain_text()
|
||||
|
||||
def add_reject_urls(self):
|
||||
d = EditTextDialog(self,
|
||||
"http://example.com/story.php?sid=5,Reason why I rejected it\nhttp://example.com/story.php?sid=6,Title by Author - Reason why I rejected it",
|
||||
"http://example.com/story.php?sid=5,"+_("Reason why I rejected it")+"\nhttp://example.com/story.php?sid=6,"+_("Title by Author")+" - "+_("Reason why I rejected it"),
|
||||
icon=self.windowIcon(),
|
||||
title="Add Reject URLs",
|
||||
label="Add Reject URLs. Use: <b>http://...,note</b> or <b>http://...,title by author - note</b><br>Invalid story URLs will be ignored.",
|
||||
tooltip="One URL per line:\n<b>http://...,note</b>\n<b>http://...,title by author - note</b>",
|
||||
title=_("Add Reject URLs"),
|
||||
label=_("Add Reject URLs. Use: <b>http://...,note</b> or <b>http://...,title by author - note</b><br>Invalid story URLs will be ignored."),
|
||||
tooltip=_("One URL per line:\n<b>http://...,note</b>\n<b>http://...,title by author - note</b>"),
|
||||
rejectreasons=rejecturllist.get_reject_reasons(),
|
||||
reasonslabel='Add this reason to all URLs added:')
|
||||
reasonslabel=_('Add this reason to all URLs added:'))
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
rejecturllist.add_text(d.get_plain_text(),d.get_reason_text())
|
||||
@@ -510,7 +553,7 @@ class PersonalIniTab(QWidget):
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel('These settings provide more detailed control over what metadata will be displayed inside the ebook as well as let you set is_adult and user/password for different sites.')
|
||||
label = QLabel(_('These settings provide more detailed control over what metadata will be displayed inside the ebook as well as let you set %(isa)s and %(u)s/%(p)s for different sites.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
@@ -523,13 +566,13 @@ class PersonalIniTab(QWidget):
|
||||
self.ini.setFont(QFont("Courier",
|
||||
self.plugin_action.gui.font().pointSize()+1))
|
||||
except Exception as e:
|
||||
print("Couldn't get font: %s"%e)
|
||||
logger.error("Couldn't get font: %s"%e)
|
||||
self.ini.setLineWrapMode(QTextEdit.NoWrap)
|
||||
self.ini.setText(prefs['personal.ini'])
|
||||
self.l.addWidget(self.ini)
|
||||
|
||||
self.defaults = QPushButton('View Defaults (plugin-defaults.ini)', self)
|
||||
self.defaults.setToolTip("View all of the plugin's configurable settings\nand their default settings.")
|
||||
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', 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)
|
||||
|
||||
@@ -547,25 +590,25 @@ class ShowDefaultsIniDialog(QDialog):
|
||||
self.resize(600, 500)
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
self.label = QLabel("Plugin Defaults (plugin-defaults.ini) (Read-Only)")
|
||||
self.label.setToolTip("These are all of the plugin's configurable options\nand their default settings.")
|
||||
self.label = QLabel(_("Plugin Defaults (%s) (Read-Only)")%'plugin-defaults.ini')
|
||||
self.label.setToolTip(_("These are all of the plugin's configurable options\nand their default settings."))
|
||||
self.setWindowTitle(_('Plugin Defaults'))
|
||||
self.setWindowIcon(icon)
|
||||
self.l.addWidget(self.label)
|
||||
|
||||
self.ini = QTextEdit(self)
|
||||
self.ini.setToolTip("These are all of the plugin's configurable options\nand their default settings.")
|
||||
self.ini.setToolTip(_("These are all of the plugin's configurable options\nand their default settings."))
|
||||
try:
|
||||
self.ini.setFont(QFont("Courier",
|
||||
get_gui().font().pointSize()+1))
|
||||
except Exception as e:
|
||||
print("Couldn't get font: %s"%e)
|
||||
logger.error("Couldn't get font: %s"%e)
|
||||
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 = QPushButton(_('OK'), self)
|
||||
self.ok_button.clicked.connect(self.hide)
|
||||
self.l.addWidget(self.ok_button)
|
||||
|
||||
@@ -585,45 +628,45 @@ class ReadingListTab(QWidget):
|
||||
except KeyError:
|
||||
reading_lists= []
|
||||
|
||||
label = QLabel('These settings provide integration with the Reading List Plugin. Reading List can automatically send to devices and change custom columns. You have to create and configure the lists in Reading List to be useful.')
|
||||
label = QLabel(_('These settings provide integration with the %(rl)s Plugin. %(rl)s can automatically send to devices and change custom columns. You have to create and configure the lists in %(rl)s to be useful.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
self.addtolists = QCheckBox('Add new/updated stories to "Send to Device" Reading List(s).',self)
|
||||
self.addtolists.setToolTip('Automatically add new/updated stories to these lists in the Reading List plugin.')
|
||||
self.addtolists = QCheckBox(_('Add new/updated stories to "Send to Device" Reading List(s).'),self)
|
||||
self.addtolists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.')%no_trans)
|
||||
self.addtolists.setChecked(prefs['addtolists'])
|
||||
self.l.addWidget(self.addtolists)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('"Send to Device" Reading Lists')
|
||||
label.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
|
||||
label = QLabel(_('"Send to Device" Reading Lists'))
|
||||
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
horz.addWidget(label)
|
||||
self.send_lists_box = MultiCompleteLineEdit(self)
|
||||
self.send_lists_box.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
|
||||
self.send_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
self.send_lists_box.update_items_cache(reading_lists)
|
||||
self.send_lists_box.setText(prefs['send_lists'])
|
||||
horz.addWidget(self.send_lists_box)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.addtoreadlists = QCheckBox('Add new/updated stories to "To Read" Reading List(s).',self)
|
||||
self.addtoreadlists.setToolTip('Automatically add new/updated stories to these lists in the Reading List plugin.\nAlso offers menu option to remove stories from the "To Read" lists.')
|
||||
self.addtoreadlists = QCheckBox(_('Add new/updated stories to "To Read" Reading List(s).'),self)
|
||||
self.addtoreadlists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.\nAlso offers menu option to remove stories from the "To Read" lists.')%no_trans)
|
||||
self.addtoreadlists.setChecked(prefs['addtoreadlists'])
|
||||
self.l.addWidget(self.addtoreadlists)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('"To Read" Reading Lists')
|
||||
label.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
|
||||
label = QLabel(_('"To Read" Reading Lists'))
|
||||
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
horz.addWidget(label)
|
||||
self.read_lists_box = MultiCompleteLineEdit(self)
|
||||
self.read_lists_box.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
|
||||
self.read_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
self.read_lists_box.update_items_cache(reading_lists)
|
||||
self.read_lists_box.setText(prefs['read_lists'])
|
||||
horz.addWidget(self.read_lists_box)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.addtolistsonread = QCheckBox('Add stories back to "Send to Device" Reading List(s) when marked "Read".',self)
|
||||
self.addtolistsonread.setToolTip('Menu option to remove from "To Read" lists will also add stories back to "Send to Device" Reading List(s)')
|
||||
self.addtolistsonread = QCheckBox(_('Add stories back to "Send to Device" Reading List(s) when marked "Read".'),self)
|
||||
self.addtolistsonread.setToolTip(_('Menu option to remove from "To Read" lists will also add stories back to "Send to Device" Reading List(s)'))
|
||||
self.addtolistsonread.setChecked(prefs['addtolistsonread'])
|
||||
self.l.addWidget(self.addtolistsonread)
|
||||
|
||||
@@ -645,7 +688,7 @@ class GenerateCoverTab(QWidget):
|
||||
except KeyError:
|
||||
gc_settings= []
|
||||
|
||||
label = QLabel('The Generate Cover plugin can create cover images for books using various metadata and configurations. If you have GC installed, FFDL can run GC on new downloads and metadata updates. Pick a GC setting by site or Default.')
|
||||
label = QLabel(_('The %(gc)s plugin can create cover images for books using various metadata and configurations. If you have GC installed, FFDL can run GC on new downloads and metadata updates. Pick a GC setting by site or Default.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
@@ -663,14 +706,15 @@ class GenerateCoverTab(QWidget):
|
||||
|
||||
sitelist = getConfigSections()
|
||||
sitelist.sort()
|
||||
sitelist.insert(0,u"Default")
|
||||
sitelist.insert(0,_("Default"))
|
||||
for site in sitelist:
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(site)
|
||||
if site == u"Default":
|
||||
s = "On Metadata update, run Generate Cover with this setting, if not selected for specific site."
|
||||
if site == _("Default"):
|
||||
s = _("On Metadata update, run %(gc)s with this setting, if not selected for specific site.")%no_trans
|
||||
else:
|
||||
s = "On Metadata update, run Generate Cover with this setting for %s stories."%site
|
||||
no_trans['site']=site # not ideal, but, meh.
|
||||
s = _("On Metadata update, run %(gc)s with this setting for %(site)s stories.")%no_trans
|
||||
|
||||
label.setToolTip(s)
|
||||
horz.addWidget(label)
|
||||
@@ -679,7 +723,12 @@ class GenerateCoverTab(QWidget):
|
||||
dropdown.addItem('',QVariant('none'))
|
||||
for setting in gc_settings:
|
||||
dropdown.addItem(setting,QVariant(setting))
|
||||
self.gc_dropdowns[site] = dropdown
|
||||
if site == _("Default"):
|
||||
self.gc_dropdowns["Default"] = dropdown
|
||||
if 'Default' in prefs['gc_site_settings']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings']['Default'])))
|
||||
else:
|
||||
self.gc_dropdowns[site] = dropdown
|
||||
if site in prefs['gc_site_settings']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings'][site])))
|
||||
|
||||
@@ -688,13 +737,13 @@ class GenerateCoverTab(QWidget):
|
||||
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
self.gcnewonly = QCheckBox("Run Generate Cover Only on New Books",self)
|
||||
self.gcnewonly.setToolTip("Default is to run GC any time the calibre metadata is updated.")
|
||||
self.gcnewonly = QCheckBox(_("Run %(gc)s Only on New Books")%no_trans,self)
|
||||
self.gcnewonly.setToolTip(_("Default is to run GC any time the calibre metadata is updated."))
|
||||
self.gcnewonly.setChecked(prefs['gcnewonly'])
|
||||
self.l.addWidget(self.gcnewonly)
|
||||
|
||||
self.allow_gc_from_ini = QCheckBox('Allow generate_cover_settings from personal.ini to override',self)
|
||||
self.allow_gc_from_ini.setToolTip("The personal.ini parameter generate_cover_settings allows you to choose a GC setting based on metadata rather than site, but it's much more complex.<br \>generate_cover_settings is ignored when this is off.")
|
||||
self.allow_gc_from_ini = QCheckBox(_('Allow %(gcset)s from %(pini)s to override')%no_trans,self)
|
||||
self.allow_gc_from_ini.setToolTip(_("The %(pini)s parameter %(gcset)s allows you to choose a GC setting based on metadata rather than site, but it's much more complex.<br \>%(gcset)s is ignored when this is off.")%no_trans)
|
||||
self.allow_gc_from_ini.setChecked(prefs['allow_gc_from_ini'])
|
||||
self.l.addWidget(self.allow_gc_from_ini)
|
||||
|
||||
@@ -708,39 +757,41 @@ class CountPagesTab(QWidget):
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel('These settings provide integration with the Count Pages Plugin. Count Pages can automatically update custom columns with page, word and reading level statistics. You have to create and configure the columns in Count Pages first.')
|
||||
label = QLabel(_('These settings provide integration with the %(cp)s Plugin. %(cp)s can automatically update custom columns with page, word and reading level statistics. You have to create and configure the columns in %(cp)s first.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
label = QLabel('If any of the settings below are checked, when stories are added or updated, the Count Pages Plugin will be called to update the checked statistics.')
|
||||
label = QLabel(_('If any of the settings below are checked, when stories are added or updated, the %(cp)s Plugin will be called to update the checked statistics.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
# the same for all settings. Mostly.
|
||||
tooltip = _('Which column and algorithm to use are configured in %(cp)s.')%no_trans
|
||||
# 'PageCount', 'WordCount', 'FleschReading', 'FleschGrade', 'GunningFog'
|
||||
self.pagecount = QCheckBox('Page Count',self)
|
||||
self.pagecount.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.pagecount.setToolTip(tooltip)
|
||||
self.pagecount.setChecked('PageCount' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.pagecount)
|
||||
|
||||
self.wordcount = QCheckBox('Word Count',self)
|
||||
self.wordcount.setToolTip('Which column and algorithm to use are configured in Count Words.\nWill overwrite word count from FFDL metadata if set to update the same custom column.')
|
||||
self.wordcount.setToolTip(tooltip+"\n"+_('Will overwrite word count from FFDL metadata if set to update the same custom column.'))
|
||||
self.wordcount.setChecked('WordCount' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.wordcount)
|
||||
|
||||
self.fleschreading = QCheckBox('Flesch Reading Ease',self)
|
||||
self.fleschreading.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.fleschreading.setToolTip(tooltip)
|
||||
self.fleschreading.setChecked('FleschReading' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.fleschreading)
|
||||
|
||||
self.fleschgrade = QCheckBox('Flesch-Kincaid Grade Level',self)
|
||||
self.fleschgrade.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.fleschgrade.setToolTip(tooltip)
|
||||
self.fleschgrade.setChecked('FleschGrade' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.fleschgrade)
|
||||
|
||||
self.gunningfog = QCheckBox('Gunning Fog Index',self)
|
||||
self.gunningfog.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.gunningfog.setToolTip(tooltip)
|
||||
self.gunningfog.setChecked('GunningFog' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.gunningfog)
|
||||
|
||||
@@ -756,26 +807,23 @@ class OtherTab(QWidget):
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel("These controls aren't plugin settings as such, but convenience buttons for setting Keyboard shortcuts and getting all the FanFictionDownLoader confirmation dialogs back again.")
|
||||
label = QLabel(_("These controls aren't plugin settings as such, but convenience buttons for setting Keyboard shortcuts and getting all the FanFictionDownLoader confirmation dialogs back again."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
keyboard_shortcuts_button = QPushButton('Keyboard shortcuts...', self)
|
||||
keyboard_shortcuts_button.setToolTip(_(
|
||||
'Edit the keyboard shortcuts associated with this plugin'))
|
||||
keyboard_shortcuts_button = QPushButton(_('Keyboard shortcuts...'), self)
|
||||
keyboard_shortcuts_button.setToolTip(_('Edit the keyboard shortcuts associated with this plugin'))
|
||||
keyboard_shortcuts_button.clicked.connect(parent_dialog.edit_shortcuts)
|
||||
self.l.addWidget(keyboard_shortcuts_button)
|
||||
|
||||
reset_confirmation_button = QPushButton(_('Reset disabled &confirmation dialogs'), self)
|
||||
reset_confirmation_button.setToolTip(_(
|
||||
'Reset all show me again dialogs for the FanFictionDownLoader plugin'))
|
||||
reset_confirmation_button.setToolTip(_('Reset all show me again dialogs for the FanFictionDownLoader plugin'))
|
||||
reset_confirmation_button.clicked.connect(self.reset_dialogs)
|
||||
self.l.addWidget(reset_confirmation_button)
|
||||
|
||||
view_prefs_button = QPushButton('&View library preferences...', self)
|
||||
view_prefs_button.setToolTip(_(
|
||||
'View data stored in the library database for this plugin'))
|
||||
view_prefs_button = QPushButton(_('&View library preferences...'), self)
|
||||
view_prefs_button.setToolTip(_('View data stored in the library database for this plugin'))
|
||||
view_prefs_button.clicked.connect(self.view_prefs)
|
||||
self.l.addWidget(view_prefs_button)
|
||||
|
||||
@@ -835,35 +883,35 @@ permitted_values['text'] = permitted_values['enumeration']
|
||||
permitted_values['comments'] = permitted_values['enumeration']
|
||||
|
||||
titleLabels = {
|
||||
'category':'Category',
|
||||
'genre':'Genre',
|
||||
'language':'Language',
|
||||
'status':'Status',
|
||||
'status-C':'Status:Completed',
|
||||
'status-I':'Status:In-Progress',
|
||||
'series':'Series',
|
||||
'characters':'Characters',
|
||||
'ships':'Relationships',
|
||||
'datePublished':'Published',
|
||||
'dateUpdated':'Updated',
|
||||
'dateCreated':'Created',
|
||||
'rating':'Rating',
|
||||
'warnings':'Warnings',
|
||||
'numChapters':'Chapters',
|
||||
'numWords':'Words',
|
||||
'site':'Site',
|
||||
'storyId':'Story ID',
|
||||
'authorId':'Author ID',
|
||||
'extratags':'Extra Tags',
|
||||
'title':'Title',
|
||||
'storyUrl':'Story URL',
|
||||
'description':'Description',
|
||||
'author':'Author',
|
||||
'authorUrl':'Author URL',
|
||||
'formatname':'File Format',
|
||||
'formatext':'File Extension',
|
||||
'siteabbrev':'Site Abbrev',
|
||||
'version':'FFDL Version'
|
||||
'category':_('Category'),
|
||||
'genre':_('Genre'),
|
||||
'language':_('Language'),
|
||||
'status':_('Status'),
|
||||
'status-C':_('Status:%(cmplt)s')%no_trans,
|
||||
'status-I':_('Status:%(inprog)s')%no_trans,
|
||||
'series':_('Series'),
|
||||
'characters':_('Characters'),
|
||||
'ships':_('Relationships'),
|
||||
'datePublished':_('Published'),
|
||||
'dateUpdated':_('Updated'),
|
||||
'dateCreated':_('Created'),
|
||||
'rating':_('Rating'),
|
||||
'warnings':_('Warnings'),
|
||||
'numChapters':_('Chapters'),
|
||||
'numWords':_('Words'),
|
||||
'site':_('Site'),
|
||||
'storyId':_('Story ID'),
|
||||
'authorId':_('Author ID'),
|
||||
'extratags':_('Extra Tags'),
|
||||
'title':_('Title'),
|
||||
'storyUrl':_('Story URL'),
|
||||
'description':_('Description'),
|
||||
'author':_('Author'),
|
||||
'authorUrl':_('Author URL'),
|
||||
'formatname':_('File Format'),
|
||||
'formatext':_('File Extension'),
|
||||
'siteabbrev':_('Site Abbrev'),
|
||||
'version':_('FFDL Version')
|
||||
}
|
||||
|
||||
class CustomColumnsTab(QWidget):
|
||||
@@ -878,7 +926,7 @@ class CustomColumnsTab(QWidget):
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel("If you have custom columns defined, they will be listed below. Choose a metadata value type to fill your columns automatically.")
|
||||
label = QLabel(_("If you have custom columns defined, they will be listed below. Choose a metadata value type to fill your columns automatically."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
@@ -903,7 +951,7 @@ class CustomColumnsTab(QWidget):
|
||||
# print("column['%s'] => %s"%(k,v))
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(column['name'])
|
||||
label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
|
||||
label.setToolTip(_("Update this %s column(%s) with...")%(key,column['datatype']))
|
||||
horz.addWidget(label)
|
||||
dropdown = QComboBox(self)
|
||||
dropdown.addItem('',QVariant('none'))
|
||||
@@ -913,13 +961,13 @@ class CustomColumnsTab(QWidget):
|
||||
if key in prefs['custom_cols']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['custom_cols'][key])))
|
||||
if column['datatype'] == 'enumeration':
|
||||
dropdown.setToolTip("Metadata values valid for this type of column.\nValues that aren't valid for this enumeration column will be ignored.")
|
||||
dropdown.setToolTip(_("Metadata values valid for this type of column.")+"\n"+_("Values that aren't valid for this enumeration column will be ignored."))
|
||||
else:
|
||||
dropdown.setToolTip("Metadata values valid for this type of column.")
|
||||
dropdown.setToolTip(_("Metadata values valid for this type of column."))
|
||||
horz.addWidget(dropdown)
|
||||
|
||||
newonlycheck = QCheckBox("New Only",self)
|
||||
newonlycheck.setToolTip("Write to %s(%s) only for new\nbooks, not updates to existing books."%(column['name'],key))
|
||||
newonlycheck = QCheckBox(_("New Only"),self)
|
||||
newonlycheck.setToolTip(_("Write to %s(%s) only for new\nbooks, not updates to existing books.")%(column['name'],key))
|
||||
self.custcol_newonlycheck[key] = newonlycheck
|
||||
if key in prefs['custom_cols_newonly']:
|
||||
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
|
||||
@@ -930,19 +978,19 @@ class CustomColumnsTab(QWidget):
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
self.allow_custcol_from_ini = QCheckBox('Allow custom_columns_settings from personal.ini to override',self)
|
||||
self.allow_custcol_from_ini.setToolTip("The personal.ini parameter custom_columns_settings allows you to set custom columns to site specific values that aren't common to all sites.<br \>custom_columns_settings is ignored when this is off.")
|
||||
self.allow_custcol_from_ini = QCheckBox(_('Allow %(ccset)s from %(pini)s to override')%no_trans,self)
|
||||
self.allow_custcol_from_ini.setToolTip(_("The %(pini)s parameter %(ccset)s allows you to set custom columns to site specific values that aren't common to all sites.<br />%(ccset)s is ignored when this is off.")%no_trans)
|
||||
self.allow_custcol_from_ini.setChecked(prefs['allow_custcol_from_ini'])
|
||||
self.l.addWidget(self.allow_custcol_from_ini)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
label = QLabel("Special column:")
|
||||
label = QLabel(_("Special column:"))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel("Update/Overwrite Error Column:")
|
||||
tooltip="When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)"
|
||||
label = QLabel(_("Update/Overwrite Error Column:"))
|
||||
tooltip=_("When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)")
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
self.errorcol = QComboBox(self)
|
||||
@@ -967,21 +1015,21 @@ class StandardColumnsTab(QWidget):
|
||||
|
||||
columns=OrderedDict()
|
||||
|
||||
columns["title"]="Title"
|
||||
columns["authors"]="Author(s)"
|
||||
columns["publisher"]="Publisher"
|
||||
columns["tags"]="Tags"
|
||||
columns["languages"]="Languages"
|
||||
columns["pubdate"]="Published Date"
|
||||
columns["timestamp"]="Date"
|
||||
columns["comments"]="Comments"
|
||||
columns["series"]="Series"
|
||||
columns["identifiers"]="Ids(url id only)"
|
||||
columns["title"]=_("Title")
|
||||
columns["authors"]=_("Author(s)")
|
||||
columns["publisher"]=_("Publisher")
|
||||
columns["tags"]=_("Tags")
|
||||
columns["languages"]=_("Languages")
|
||||
columns["pubdate"]=_("Published Date")
|
||||
columns["timestamp"]=_("Date")
|
||||
columns["comments"]=_("Comments")
|
||||
columns["series"]=_("Series")
|
||||
columns["identifiers"]=_("Ids(url id only)")
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel("The standard calibre metadata columns are listed below. You may choose whether FFDL will fill each column automatically on updates or only for new books.")
|
||||
label = QLabel(_("The standard calibre metadata columns are listed below. You may choose whether FFDL will fill each column automatically on updates or only for new books."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
@@ -994,8 +1042,8 @@ class StandardColumnsTab(QWidget):
|
||||
#label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
|
||||
horz.addWidget(label)
|
||||
|
||||
newonlycheck = QCheckBox("New Only",self)
|
||||
newonlycheck.setToolTip("Write to %s only for new\nbooks, not updates to existing books."%column)
|
||||
newonlycheck = QCheckBox(_("New Only"),self)
|
||||
newonlycheck.setToolTip(_("Write to %s only for new\nbooks, not updates to existing books.")%column)
|
||||
self.stdcol_newonlycheck[key] = newonlycheck
|
||||
if key in prefs['std_cols_newonly']:
|
||||
newonlycheck.setChecked(prefs['std_cols_newonly'][key])
|
||||
|
||||
+111
-73
@@ -7,9 +7,15 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import traceback, re
|
||||
from functools import partial
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import urllib
|
||||
import email
|
||||
|
||||
@@ -23,6 +29,12 @@ from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayo
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
load_translations()
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
import (ReadOnlyTableWidgetItem, ReadOnlyTextIconWidgetItem, SizePersistedDialog,
|
||||
ImageTitleLayout, get_icon)
|
||||
@@ -30,13 +42,13 @@ from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_html, get_urls_from_text
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getNormalStoryURL
|
||||
|
||||
SKIP=u'Skip'
|
||||
ADDNEW=u'Add New Book'
|
||||
UPDATE=u'Update EPUB if New Chapters'
|
||||
UPDATEALWAYS=u'Update EPUB Always'
|
||||
OVERWRITE=u'Overwrite if Newer'
|
||||
OVERWRITEALWAYS=u'Overwrite Always'
|
||||
CALIBREONLY=u'Update Calibre Metadata Only'
|
||||
SKIP=_('Skip')
|
||||
ADDNEW=_('Add New Book')
|
||||
UPDATE=_('Update EPUB if New Chapters')
|
||||
UPDATEALWAYS=_('Update EPUB Always')
|
||||
OVERWRITE=_('Overwrite if Newer')
|
||||
OVERWRITEALWAYS=_('Overwrite Always')
|
||||
CALIBREONLY=_('Update Calibre Metadata Only')
|
||||
collision_order=[SKIP,
|
||||
ADDNEW,
|
||||
UPDATE,
|
||||
@@ -45,6 +57,32 @@ collision_order=[SKIP,
|
||||
OVERWRITEALWAYS,
|
||||
CALIBREONLY,]
|
||||
|
||||
# best idea I've had for how to deal with config/pref saving the
|
||||
# collision name in english.
|
||||
SAVE_SKIP='Skip'
|
||||
SAVE_ADDNEW='Add New Book'
|
||||
SAVE_UPDATE='Update EPUB if New Chapters'
|
||||
SAVE_UPDATEALWAYS='Update EPUB Always'
|
||||
SAVE_OVERWRITE='Overwrite if Newer'
|
||||
SAVE_OVERWRITEALWAYS='Overwrite Always'
|
||||
SAVE_CALIBREONLY='Update Calibre Metadata Only'
|
||||
save_collisions={
|
||||
SKIP:SAVE_SKIP,
|
||||
ADDNEW:SAVE_ADDNEW,
|
||||
UPDATE:SAVE_UPDATE,
|
||||
UPDATEALWAYS:SAVE_UPDATEALWAYS,
|
||||
OVERWRITE:SAVE_OVERWRITE,
|
||||
OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS,
|
||||
CALIBREONLY:SAVE_CALIBREONLY,
|
||||
SAVE_SKIP:SKIP,
|
||||
SAVE_ADDNEW:ADDNEW,
|
||||
SAVE_UPDATE:UPDATE,
|
||||
SAVE_UPDATEALWAYS:UPDATEALWAYS,
|
||||
SAVE_OVERWRITE:OVERWRITE,
|
||||
SAVE_OVERWRITEALWAYS:OVERWRITEALWAYS,
|
||||
SAVE_CALIBREONLY:CALIBREONLY,
|
||||
}
|
||||
|
||||
anthology_collision_order=[UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITEALWAYS]
|
||||
@@ -98,7 +136,7 @@ class RejectUrlEntry:
|
||||
def fullnote(self):
|
||||
retval = ""
|
||||
if self.title and self.auth:
|
||||
retval = retval + "%s by %s"%(self.title,self.auth)
|
||||
retval = retval + _("%s by %s")%(self.title,self.auth)
|
||||
if self.note:
|
||||
retval = retval + " - "
|
||||
|
||||
@@ -186,7 +224,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
self.setWindowTitle('FanFictionDownLoader')
|
||||
self.setWindowTitle(_('FanFictionDownLoader'))
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.toplabel=QLabel("Toplabel")
|
||||
@@ -203,11 +241,11 @@ class AddNewDialog(SizePersistedDialog):
|
||||
# elements to show again when doing *update* merge
|
||||
self.mergeupdateshow = []
|
||||
|
||||
self.groupbox = QGroupBox("Show Download Options")
|
||||
self.groupbox = QGroupBox(_("Show Download Options"))
|
||||
self.groupbox.setCheckable(True)
|
||||
self.groupbox.setChecked(False)
|
||||
self.groupbox.setFlat(True)
|
||||
print("style:%s"%self.groupbox.styleSheet())
|
||||
#print("style:%s"%self.groupbox.styleSheet())
|
||||
self.groupbox.setStyleSheet(gpstyle)
|
||||
|
||||
self.gbf = QFrame()
|
||||
@@ -222,7 +260,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.groupbox.toggled.connect(self.gbf.setVisible)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('Output &Format:')
|
||||
label = QLabel(_('Output &Format:'))
|
||||
self.mergehide.append(label)
|
||||
|
||||
self.fileform = QComboBox(self)
|
||||
@@ -230,7 +268,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.fileform.addItem('mobi')
|
||||
self.fileform.addItem('html')
|
||||
self.fileform.addItem('txt')
|
||||
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
|
||||
self.fileform.setToolTip(_('Choose output format to create. May set default from plugin configuration.'))
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
|
||||
horz.addWidget(label)
|
||||
@@ -246,7 +284,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.collision.setToolTip("CollisionToolTip")
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
i = self.collision.findText(save_collisions[prefs['collision']])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
self.collisionlabel.setBuddy(self.collision)
|
||||
@@ -258,15 +296,15 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.mergeupdateshow.append(self.collision)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
|
||||
self.updatemeta = QCheckBox(_('Update Calibre &Metadata?'),self)
|
||||
self.updatemeta.setToolTip(_("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)"))
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
horz.addWidget(self.updatemeta)
|
||||
self.mergehide.append(self.updatemeta)
|
||||
self.mergeupdateshow.append(self.updatemeta)
|
||||
|
||||
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
|
||||
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
|
||||
self.updateepubcover = QCheckBox(_('Update EPUB Cover?'),self)
|
||||
self.updateepubcover.setToolTip(_('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.'))
|
||||
self.updateepubcover.setChecked(prefs['updateepubcover'])
|
||||
horz.addWidget(self.updateepubcover)
|
||||
self.mergehide.append(self.updateepubcover)
|
||||
@@ -313,10 +351,10 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.groupbox.setVisible(not(self.merge and self.newmerge))
|
||||
|
||||
if self.merge:
|
||||
self.toplabel.setText('Story URL(s) for anthology, one per line:')
|
||||
self.url.setToolTip('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.')
|
||||
self.collisionlabel.setText('If Story Already Exists in Anthology?')
|
||||
self.collision.setToolTip("What to do if there's already an existing story with the same URL in the anthology.")
|
||||
self.toplabel.setText(_('Story URL(s) for anthology, one per line:'))
|
||||
self.url.setToolTip(_('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.'))
|
||||
self.collisionlabel.setText(_('If Story Already Exists in Anthology?'))
|
||||
self.collision.setToolTip(_("What to do if there's already an existing story with the same URL in the anthology."))
|
||||
for widget in self.mergehide:
|
||||
widget.setVisible(False)
|
||||
if not self.newmerge:
|
||||
@@ -325,10 +363,10 @@ class AddNewDialog(SizePersistedDialog):
|
||||
else:
|
||||
for widget in self.mergehide:
|
||||
widget.setVisible(True)
|
||||
self.toplabel.setText('Story URL(s), one per line:')
|
||||
self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.')
|
||||
self.collisionlabel.setText('If Story Already Exists?')
|
||||
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
|
||||
self.toplabel.setText(_('Story URL(s), one per line:'))
|
||||
self.url.setToolTip(_('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.'))
|
||||
self.collisionlabel.setText(_('If Story Already Exists?'))
|
||||
self.collision.setToolTip(_("What to do if there's already an existing story with the same URL or title and author."))
|
||||
|
||||
# Need to re-able after hiding/showing
|
||||
self.setAcceptDrops(True)
|
||||
@@ -344,10 +382,10 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
|
||||
i = self.collision.findText(self.prefs['collision'])
|
||||
i = self.collision.findText(save_collisions[self.prefs['collision']])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
self.updatemeta.setChecked(self.prefs['updatemeta'])
|
||||
|
||||
if not self.merge:
|
||||
@@ -428,18 +466,18 @@ class CollectURLDialog(SizePersistedDialog):
|
||||
self.url.setText(url_text)
|
||||
self.l.addWidget(self.url,1,1,1,2)
|
||||
|
||||
self.indiv_button = QPushButton('For Individual Books', self)
|
||||
self.indiv_button.setToolTip('Get URLs and go to dialog for individual story downloads.')
|
||||
self.indiv_button = QPushButton(_('For Individual Books'), self)
|
||||
self.indiv_button.setToolTip(_('Get URLs and go to dialog for individual story downloads.'))
|
||||
self.indiv_button.clicked.connect(self.indiv)
|
||||
self.l.addWidget(self.indiv_button,2,0)
|
||||
|
||||
self.merge_button = QPushButton('For Anthology Epub', self)
|
||||
self.merge_button.setToolTip('Get URLs and go to dialog for Anthology download.\nRequires EpubMerge 1.3.1+ plugin.')
|
||||
self.merge_button = QPushButton(_('For Anthology Epub'), self)
|
||||
self.merge_button.setToolTip(_('Get URLs and go to dialog for Anthology download.\nRequires %s plugin.')%'EpubMerge 1.3.1+')
|
||||
self.merge_button.clicked.connect(self.merge)
|
||||
self.l.addWidget(self.merge_button,2,1)
|
||||
self.merge_button.setEnabled(epubmerge_plugin!=None)
|
||||
|
||||
self.cancel_button = QPushButton('Cancel', self)
|
||||
self.cancel_button = QPushButton(_('Cancel'), self)
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
self.l.addWidget(self.cancel_button,2,2)
|
||||
|
||||
@@ -471,29 +509,29 @@ class UserPassDialog(QDialog):
|
||||
self.setLayout(self.l)
|
||||
|
||||
if exception.passwdonly:
|
||||
self.setWindowTitle('Password')
|
||||
self.l.addWidget(QLabel("Author requires a password for this story(%s)."%exception.url),0,0,1,2)
|
||||
self.setWindowTitle(_('Password'))
|
||||
self.l.addWidget(QLabel(_("Author requires a password for this story(%s).")%exception.url),0,0,1,2)
|
||||
# user isn't used, but it's easier to still have it for
|
||||
# post processing.
|
||||
self.user = FakeLineEdit()
|
||||
else:
|
||||
self.setWindowTitle('User/Password')
|
||||
self.l.addWidget(QLabel("%s requires you to login to download this story."%site),0,0,1,2)
|
||||
self.setWindowTitle(_('User/Password'))
|
||||
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.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.l.addWidget(QLabel(_("Password:")),2,0)
|
||||
self.passwd = QLineEdit(self)
|
||||
self.passwd.setEchoMode(QLineEdit.Password)
|
||||
self.l.addWidget(self.passwd,2,1)
|
||||
|
||||
self.ok_button = QPushButton('OK', self)
|
||||
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 = QPushButton(_('Cancel'), self)
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
self.l.addWidget(self.cancel_button,3,1)
|
||||
|
||||
@@ -515,9 +553,9 @@ class LoopProgressDialog(QProgressDialog):
|
||||
book_list,
|
||||
foreach_function,
|
||||
finish_function,
|
||||
init_label="Fetching metadata for stories...",
|
||||
win_title="Downloading metadata for stories",
|
||||
status_prefix="Fetched metadata for"):
|
||||
init_label=_("Fetching metadata for stories..."),
|
||||
win_title=_("Downloading metadata for stories"),
|
||||
status_prefix=_("Fetched metadata for")):
|
||||
QProgressDialog.__init__(self,
|
||||
init_label,
|
||||
QString(), 0, len(book_list), gui)
|
||||
@@ -535,9 +573,9 @@ class LoopProgressDialog(QProgressDialog):
|
||||
self.exec_()
|
||||
|
||||
def updateStatus(self):
|
||||
self.setLabelText("%s %d of %d"%(self.status_prefix,self.i+1,len(self.book_list)))
|
||||
self.setLabelText("%s %d / %d"%(self.status_prefix,self.i+1,len(self.book_list)))
|
||||
self.setValue(self.i+1)
|
||||
print(self.labelText())
|
||||
#print(self.labelText())
|
||||
|
||||
def do_loop(self):
|
||||
|
||||
@@ -558,7 +596,7 @@ class LoopProgressDialog(QProgressDialog):
|
||||
except Exception as e:
|
||||
book['good']=False
|
||||
book['comment']=unicode(e)
|
||||
print("Exception: %s:%s"%(book,unicode(e)))
|
||||
logger.error("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
|
||||
self.updateStatus()
|
||||
@@ -641,7 +679,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
self.remove_button = QtGui.QToolButton(self)
|
||||
self.remove_button.setToolTip('Remove selected books from the list')
|
||||
self.remove_button.setToolTip(_('Remove selected books from the list'))
|
||||
self.remove_button.setIcon(get_icon('list_remove.png'))
|
||||
self.remove_button.clicked.connect(self.remove_from_list)
|
||||
button_layout.addWidget(self.remove_button)
|
||||
@@ -650,7 +688,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
|
||||
options_layout = QHBoxLayout()
|
||||
|
||||
groupbox = QGroupBox("Show Download Options")
|
||||
groupbox = QGroupBox(_("Show Download Options"))
|
||||
groupbox.setCheckable(True)
|
||||
groupbox.setChecked(False)
|
||||
groupbox.setFlat(True)
|
||||
@@ -667,7 +705,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
gbf.setVisible(False)
|
||||
groupbox.toggled.connect(gbf.setVisible)
|
||||
|
||||
label = QLabel('Output &Format:')
|
||||
label = QLabel(_('Output &Format:'))
|
||||
gbl.addWidget(label)
|
||||
self.fileform = QComboBox(self)
|
||||
self.fileform.addItem('epub')
|
||||
@@ -675,30 +713,30 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
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.')
|
||||
self.fileform.setToolTip(_('Choose output format to create. May set default from plugin configuration.'))
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
label.setBuddy(self.fileform)
|
||||
gbl.addWidget(self.fileform)
|
||||
|
||||
label = QLabel('Update Mode:')
|
||||
label = QLabel(_('Update Mode:'))
|
||||
gbl.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
self.collision.setToolTip("What sort of update to perform. May set default from plugin configuration.")
|
||||
self.collision.setToolTip(_("What sort of update to perform. May set default from plugin configuration."))
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
i = self.collision.findText(save_collisions[prefs['collision']])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
label.setBuddy(self.collision)
|
||||
gbl.addWidget(self.collision)
|
||||
|
||||
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
|
||||
self.updatemeta = QCheckBox(_('Update Calibre &Metadata?'),self)
|
||||
self.updatemeta.setToolTip(_("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)"))
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
gbl.addWidget(self.updatemeta)
|
||||
|
||||
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
|
||||
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
|
||||
self.updateepubcover = QCheckBox(_('Update EPUB Cover?'),self)
|
||||
self.updateepubcover.setToolTip(_('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.'))
|
||||
self.updateepubcover.setChecked(prefs['updateepubcover'])
|
||||
gbl.addWidget(self.updateepubcover)
|
||||
|
||||
@@ -751,7 +789,7 @@ class StoryListTableWidget(QTableWidget):
|
||||
self.clear()
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setRowCount(len(books))
|
||||
header_labels = ['','Title', 'Author', 'URL', 'Comment']
|
||||
header_labels = ['',_('Title'), _('Author'), 'URL', _('Comment')]
|
||||
self.setColumnCount(len(header_labels))
|
||||
self.setHorizontalHeaderLabels(header_labels)
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
@@ -819,9 +857,9 @@ class StoryListTableWidget(QTableWidget):
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
message = '<p>Are you sure you want to remove this book from the list?'
|
||||
message = '<p>'+_('Are you sure you want to remove this book from the list?')
|
||||
if len(rows) > 1:
|
||||
message = '<p>Are you sure you want to remove the selected %d books from the list?'%len(rows)
|
||||
message = '<p>'+_('Are you sure you want to remove the selected %d books from the list?')%len(rows)
|
||||
if not confirm(message,'fanfictiondownloader_delete_item', self):
|
||||
return
|
||||
first_sel_row = self.currentRow()
|
||||
@@ -847,7 +885,7 @@ class RejectListTableWidget(QTableWidget):
|
||||
self.clear()
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setRowCount(len(reject_list))
|
||||
header_labels = ['URL', 'Title', 'Author', 'Note']
|
||||
header_labels = ['URL', _('Title'), _('Author'), _('Note')]
|
||||
self.setColumnCount(len(header_labels))
|
||||
self.setHorizontalHeaderLabels(header_labels)
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
@@ -889,7 +927,7 @@ class RejectListTableWidget(QTableWidget):
|
||||
note_cell.update_items_cache(items)
|
||||
note_cell.show_initial_value(rej.note)
|
||||
note_cell.set_separator(None)
|
||||
note_cell.setToolTip('Select or Edit Reject Note.')
|
||||
note_cell.setToolTip(_('Select or Edit Reject Note.'))
|
||||
self.setCellWidget(row, 3, note_cell)
|
||||
|
||||
def remove_selected_rows(self):
|
||||
@@ -897,9 +935,9 @@ class RejectListTableWidget(QTableWidget):
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
message = '<p>Are you sure you want to remove this URL from the list?'
|
||||
message = '<p>'+_('Are you sure you want to remove this URL from the list?')
|
||||
if len(rows) > 1:
|
||||
message = '<p>Are you sure you want to remove the %d selected URLs from the list?'%len(rows)
|
||||
message = '<p>'+_('Are you sure you want to remove the %d selected URLs from the list?')%len(rows)
|
||||
if not confirm(message,'ffdl_rejectlist_delete_item_again', self):
|
||||
return
|
||||
first_sel_row = self.currentRow()
|
||||
@@ -917,7 +955,7 @@ class RejectListTableWidget(QTableWidget):
|
||||
class RejectListDialog(SizePersistedDialog):
|
||||
def __init__(self, gui, reject_list,
|
||||
rejectreasons=[],
|
||||
header="List of Books to Reject",
|
||||
header=_("List of Books to Reject"),
|
||||
icon='rotate-right.png',
|
||||
show_delete=True,
|
||||
show_all_reasons=True,
|
||||
@@ -930,7 +968,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
title_layout = ImageTitleLayout(self, icon, header,
|
||||
'<i></i>FFDL will remember these URLs and display the note and offer to reject them if you try to download them again later.')
|
||||
'<i></i>'+_('FFDL will remember these URLs and display the note and offer to reject them if you try to download them again later.'))
|
||||
layout.addLayout(title_layout)
|
||||
rejects_layout = QHBoxLayout()
|
||||
layout.addLayout(rejects_layout)
|
||||
@@ -944,7 +982,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
button_layout.addItem(spacerItem)
|
||||
|
||||
self.remove_button = QtGui.QToolButton(self)
|
||||
self.remove_button.setToolTip('Remove selected URL(s) from the list')
|
||||
self.remove_button.setToolTip(_('Remove selected URL(s) from the list'))
|
||||
self.remove_button.setIcon(get_icon('list_remove.png'))
|
||||
self.remove_button.clicked.connect(self.remove_from_list)
|
||||
button_layout.addWidget(self.remove_button)
|
||||
@@ -962,11 +1000,11 @@ class RejectListDialog(SizePersistedDialog):
|
||||
self.reason_edit.update_items_cache(items)
|
||||
self.reason_edit.show_initial_value('')
|
||||
self.reason_edit.set_separator(None)
|
||||
self.reason_edit.setToolTip("This will be added to whatever note you've set for each URL above.")
|
||||
self.reason_edit.setToolTip(_("This will be added to whatever note you've set for each URL above."))
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel("Add this reason to all URLs added:")
|
||||
label.setToolTip("This will be added to whatever note you've set for each URL above.")
|
||||
label = QLabel(_("Add this reason to all URLs added:"))
|
||||
label.setToolTip(_("This will be added to whatever note you've set for each URL above."))
|
||||
horz.addWidget(label)
|
||||
horz.addWidget(self.reason_edit)
|
||||
horz.insertStretch(-1)
|
||||
@@ -975,8 +1013,8 @@ class RejectListDialog(SizePersistedDialog):
|
||||
options_layout = QHBoxLayout()
|
||||
|
||||
if show_delete:
|
||||
self.deletebooks = QCheckBox('Delete Books (including books without FanFiction URLs)?',self)
|
||||
self.deletebooks.setToolTip("Delete the selected books after adding them to the Rejected URLs list.")
|
||||
self.deletebooks = QCheckBox(_('Delete Books (including books without FanFiction URLs)?'),self)
|
||||
self.deletebooks.setToolTip(_("Delete the selected books after adding them to the Rejected URLs list."))
|
||||
self.deletebooks.setChecked(True)
|
||||
options_layout.addWidget(self.deletebooks)
|
||||
|
||||
|
||||
+266
-214
File diff suppressed because it is too large
Load Diff
+24
-17
@@ -8,6 +8,9 @@ __copyright__ = '2012, Jim Miller'
|
||||
__copyright__ = '2011, Grant Drake <grant.drake@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import time, os, traceback
|
||||
|
||||
from StringIO import StringIO
|
||||
@@ -38,13 +41,13 @@ def do_download_worker(book_list, options,
|
||||
'''
|
||||
server = Server(pool_size=cpus)
|
||||
|
||||
print(options['version'])
|
||||
logger.info(options['version'])
|
||||
total = 0
|
||||
alreadybad = []
|
||||
# Queue all the jobs
|
||||
print("Adding jobs for URLs:")
|
||||
logger.info("Adding jobs for URLs:")
|
||||
for book in book_list:
|
||||
print("%s"%book['url'])
|
||||
logger.info("%s"%book['url'])
|
||||
if book['good']:
|
||||
total += 1
|
||||
args = ['calibre_plugins.fanfictiondownloader_plugin.jobs',
|
||||
@@ -87,19 +90,19 @@ def do_download_worker(book_list, options,
|
||||
count = count + 1
|
||||
notification(float(count)/total, '%d of %d stories finished downloading'%(count,total))
|
||||
# Add this job's output to the current log
|
||||
print('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
|
||||
print(job.details)
|
||||
logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
|
||||
logger.info(job.details)
|
||||
|
||||
if count >= total:
|
||||
# All done! Output some lists for convenience of some users.
|
||||
print("Successfully downloaded:")
|
||||
logger.info("Successfully downloaded:")
|
||||
for book in book_list:
|
||||
if book['good']:
|
||||
print("%s %s"%(book['title'],book['url']))
|
||||
print("\nUnsuccessful:")
|
||||
logger.info("%s %s"%(book['title'],book['url']))
|
||||
logger.info("\nUnsuccessful:")
|
||||
for book in book_list:
|
||||
if not book['good']:
|
||||
print("%s %s"%(book['title'],book['url']))
|
||||
logger.info("%s %s"%(book['title'],book['url']))
|
||||
break
|
||||
|
||||
server.close()
|
||||
@@ -147,7 +150,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
|
||||
## No need to download at all. Shouldn't ever get down here.
|
||||
if options['collision'] in (CALIBREONLY):
|
||||
print("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
|
||||
logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
|
||||
book['comment'] = 'Metadata collected.'
|
||||
|
||||
## checks were done earlier, it's new or not dup or newer--just write it.
|
||||
@@ -170,7 +173,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
if adapter.logfile:
|
||||
adapter.logfile = adapter.logfile.replace("span id","span notid")
|
||||
|
||||
print("write to %s"%outfile)
|
||||
logger.info("write to %s"%outfile)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
|
||||
|
||||
@@ -179,7 +182,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
|
||||
# update now handled by pre-populating the old images and
|
||||
# chapters in the adapter rather than merging epubs.
|
||||
urlchaptercount = int(story.getMetadata('numChapters'))
|
||||
urlchaptercount = int(story.getMetadata('numChapters').replace(',',''))
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
@@ -198,9 +201,13 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
# dup handling from ffdl_plugin needed for anthology updates.
|
||||
if chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update." % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
|
||||
print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
|
||||
print("write to %s"%outfile)
|
||||
|
||||
if not (options['collision'] == UPDATEALWAYS and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
logger.info("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
|
||||
logger.info("write to %s"%outfile)
|
||||
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
|
||||
@@ -222,7 +229,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
|
||||
log = Log(level=Log.DEBUG)
|
||||
# report = []
|
||||
polish({outfile:outfile}, opts, log, print) # report.append
|
||||
polish({outfile:outfile}, opts, log, logger.info) # report.append
|
||||
|
||||
except NotGoingToDownload as d:
|
||||
book['good']=False
|
||||
@@ -234,7 +241,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
book['comment']=unicode(e)
|
||||
book['icon']='dialog_error.png'
|
||||
book['status'] = 'Error'
|
||||
print("Exception: %s:%s"%(book,unicode(e)))
|
||||
logger.info("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
|
||||
#time.sleep(10)
|
||||
|
||||
@@ -32,6 +32,7 @@ default_prefs['updateepubcover'] = False
|
||||
default_prefs['keeptags'] = False
|
||||
default_prefs['suppressauthorsort'] = False
|
||||
default_prefs['suppresstitlesort'] = False
|
||||
default_prefs['mark'] = False
|
||||
default_prefs['showmarked'] = False
|
||||
default_prefs['urlsfromclip'] = True
|
||||
default_prefs['updatedefault'] = True
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+103
-7
@@ -16,7 +16,22 @@
|
||||
[defaults]
|
||||
|
||||
## [defaults] section applies to all formats and sites but may be
|
||||
## overridden at several levels
|
||||
## overridden at several levels. Example:
|
||||
|
||||
## [defaults]
|
||||
## titlepage_entries: category,genre, status
|
||||
## [www.whofic.com]
|
||||
## # overrides defaults.
|
||||
## titlepage_entries: category,genre, status,dateUpdated,rating
|
||||
## [epub]
|
||||
## # overrides defaults & site section
|
||||
## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated
|
||||
## [www.whofic.com:epub]
|
||||
## # overrides defaults, site section & format section
|
||||
## titlepage_entries: category,genre, status,datePublished
|
||||
## [overrides]
|
||||
## # overrides all other sections
|
||||
## titlepage_entries: category
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. Uncomment by removing '#' in front of is_adult.
|
||||
@@ -155,6 +170,14 @@ extratags: FanFiction
|
||||
## Add this to genre if there's more than one category.
|
||||
#add_genre_when_multi_category: Crossover
|
||||
|
||||
## default_value_(entry) can be used to set the value for a metadata
|
||||
## entry when no value has been found on the site. For example, some
|
||||
## sites doesn't have a status metadatum. If uncommented, this will
|
||||
## use 'Unknown' for status when no status is found.
|
||||
#default_value_status:Unknown
|
||||
## Can also be used for other metadata values
|
||||
#default_value_category:FanFiction
|
||||
|
||||
## number of seconds to sleep between calls to the story site. May by
|
||||
## useful if pulling large numbers of stories or if the site is slow.
|
||||
#slow_down_sleep_time:0.5
|
||||
@@ -197,6 +220,11 @@ extratags: FanFiction
|
||||
## doesn't work on some devices either.)
|
||||
#replace_hr: false
|
||||
|
||||
## Some sites/authors/stories use br tags instead of p tags for
|
||||
## paragraphs. This feature uses some heuristics to find and replace
|
||||
## br paragraphs with p tags while preserving scene breaks.
|
||||
#replace_br_with_p: false
|
||||
|
||||
## If set false, the summary will have all html stripped.
|
||||
## Both this and include_images must be true to get images in the
|
||||
## summary.
|
||||
@@ -262,6 +290,8 @@ sort_ships:false
|
||||
## you added calibre_author: keep_in_order_calibre_author:true
|
||||
#keep_in_order_author:true
|
||||
|
||||
## User-agent
|
||||
user_agent:FFDL/1.7
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
@@ -443,15 +473,16 @@ remove_transparency: true
|
||||
## confused and displays it on every page after that under the text
|
||||
## for the rest of the chapter. I doubt adding a div around the img
|
||||
## will break any other readers, but in case it does, the fix can be
|
||||
## turned off.
|
||||
## turned off. This setting is not used if replace_br_with_p is
|
||||
## true--replace_br_with_p also fixes the problem.
|
||||
nook_img_fix:true
|
||||
|
||||
[mobi]
|
||||
## mobi TOC cannot be turned off right now.
|
||||
#include_tocpage: true
|
||||
|
||||
## Each site has a section that overrides [defaults] *and* the format
|
||||
## sections test1.com specifically is not a real story site. Instead,
|
||||
## Each site has a section that overrides [defaults].
|
||||
## test1.com specifically is not a real story site. Instead,
|
||||
## it is a fake site for testing configuration and output. It uses
|
||||
## URLs like: http://test1.com?sid=12345
|
||||
[test1.com]
|
||||
@@ -508,7 +539,8 @@ extratags: FanFiction,Testing,HTML
|
||||
#is_adult:true
|
||||
|
||||
## AO3 adapter defines a few extra metadata entries.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections
|
||||
## If there's ever more than 4 series, add series04,series04Url etc.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
fandoms_label:Fandoms
|
||||
freeformtags_label:Freeform Tags
|
||||
freefromtags_label:Freeform Tags
|
||||
@@ -524,7 +556,7 @@ bookmarks_label:Bookmarks
|
||||
include_in_freefromtags:freeformtags
|
||||
|
||||
## adds to titlepage_entries instead of replacing it.
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
@@ -742,6 +774,23 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[fictionpad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
extra_valid_entries:followers,comments,views,likes,dislikes
|
||||
#extra_titlepage_entries:followers,comments,views,likes,dislikes
|
||||
|
||||
followers_label:Followers
|
||||
comments_label:Comments
|
||||
views_label:Views
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
|
||||
[finestories.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -750,6 +799,24 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[storiesonline.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,codes
|
||||
#extra_titlepage_entries:size,universe,codes
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
codes_label:Codes
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -792,6 +859,10 @@ extracategories:Highlander
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:In Death
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/public/style_emoticons/.*
|
||||
|
||||
[ksarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Star Trek
|
||||
@@ -1099,6 +1170,7 @@ context_label:Context
|
||||
type_label:Type of Couple
|
||||
|
||||
[www.fanfiction.net]
|
||||
user_agent:
|
||||
## fanfiction.net's 'cover' images are really just tiny thumbnails.
|
||||
## Change this to false to use them anyway.
|
||||
never_make_cover: true
|
||||
@@ -1107,6 +1179,17 @@ never_make_cover: true
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,favs,follows
|
||||
|
||||
## ffnet uses 'Pairings', not 'Relationship', stating they don't have
|
||||
## to be romantic pairings.
|
||||
ships_label:Pairings
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't have time.
|
||||
## See http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
#dateCreated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
|
||||
[www.fanfiktion.de]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1129,7 +1212,12 @@ extracategories:Harry Potter
|
||||
## fictionally.org storyIds are not unique. Combine with authorId.
|
||||
output_filename: ${title}-${siteabbrev}_${authorId}_${storyId}${formatext}
|
||||
|
||||
## fictionalley.org doesn't have a status metadatum. If uncommented,
|
||||
## this will be used for status.
|
||||
#default_value_status:Unknown
|
||||
|
||||
[www.fictionpress.com]
|
||||
user_agent:
|
||||
## Clear FanFiction from defaults, fictionpress.com is original fiction.
|
||||
extratags:
|
||||
|
||||
@@ -1151,12 +1239,13 @@ extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:likes,dislikes,views,total_views,short_description
|
||||
extra_valid_entries:likes,dislikes,views,total_views,short_description,groups
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
views_label:Highest Single Chapter Views
|
||||
total_views_label:Total Views
|
||||
short_description_label:Short Summary
|
||||
groups_label:Groups
|
||||
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -1168,6 +1257,13 @@ short_description_label:Short Summary
|
||||
## when a password is required rather than prompting every time.
|
||||
#fail_on_password: false
|
||||
|
||||
## fimfiction.net stories allow chapters to be added out of order. So
|
||||
## the newest chapter may not be the last one. FFDL update doesn't
|
||||
## like that. If do_update_hook is uncommented and set true, the
|
||||
## adapter will discard all existing chapters from the newest one on
|
||||
## when updating to enforce accurate chapters.
|
||||
#do_update_hook:false
|
||||
|
||||
[www.harrypotterfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
+6
-1
@@ -270,7 +270,6 @@ def main(argv,
|
||||
elif chaptercount == 0:
|
||||
print "%s doesn't contain any recognizable chapters, probably from a different source. Not updating." % (output_filename)
|
||||
else:
|
||||
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
|
||||
if not options.metaonly:
|
||||
|
||||
# update now handled by pre-populating the old
|
||||
@@ -284,6 +283,12 @@ def main(argv,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(output_filename)
|
||||
|
||||
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
|
||||
|
||||
if not (options.update and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
writeStory(configuration,adapter,"epub")
|
||||
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# coding: utf-8
|
||||
|
||||
import re
|
||||
import codecs
|
||||
|
||||
stack = []
|
||||
|
||||
def get_end_tag(tag):
|
||||
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
|
||||
return re.sub(r'.*<([^\ >]+).*', r'</\1>', tag)
|
||||
return u''
|
||||
|
||||
def get_tag_name(tag):
|
||||
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
|
||||
return re.sub(r'</*([^\ >]+).*', r'\1', tag)
|
||||
return u''
|
||||
|
||||
def push(tag):
|
||||
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
|
||||
stack.append(tag)
|
||||
|
||||
def pop():
|
||||
if len(stack) > 0:
|
||||
return stack.pop()
|
||||
return u''
|
||||
|
||||
def pop_end_tag():
|
||||
return unicode(get_end_tag(pop()))
|
||||
|
||||
def spool_end():
|
||||
html = u''
|
||||
for tag in reversed(stack):
|
||||
html += get_end_tag(tag)
|
||||
return html
|
||||
|
||||
def spool_start():
|
||||
html = u''
|
||||
for item in stack:
|
||||
html += item
|
||||
return html
|
||||
|
||||
def has_elements():
|
||||
return len(stack) > 0
|
||||
|
||||
def get_last():
|
||||
# t = pop()
|
||||
# push(t)
|
||||
# return t
|
||||
if len(stack) > 0:
|
||||
return stack[len(stack)-1]
|
||||
return u''
|
||||
|
||||
def flush():
|
||||
del stack[:]
|
||||
|
||||
def get_stack():
|
||||
return stack
|
||||
@@ -4,13 +4,16 @@ try:
|
||||
# just a way to switch between web service and CLI/PI
|
||||
import google.appengine.api
|
||||
except:
|
||||
import sys
|
||||
if sys.version_info >= (2, 7):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
try: # just a way to switch between CLI and PI
|
||||
import calibre.constants
|
||||
except:
|
||||
import sys
|
||||
if sys.version_info >= (2, 7):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ import adapter_nickandgregnet
|
||||
import adapter_potterheadsanonymouscom
|
||||
import adapter_simplyundeniablecom
|
||||
import adapter_scarheadnet
|
||||
import adapter_fictionpadcom
|
||||
import adapter_storiesonlinenet
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
|
||||
@@ -210,10 +210,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
if a != None:
|
||||
warnings = a.findAll('a',{'class':"tag"})
|
||||
for warning in warnings:
|
||||
if warning.string == "Author Chose Not To Use Archive Warnings":
|
||||
warning.string = "No Archive Warnings Apply"
|
||||
if warning.string != "No Archive Warnings Apply":
|
||||
self.story.addToList('warnings',warning.string)
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
a = metasoup.find('dd',{'class':"freeform tags"})
|
||||
if a != None:
|
||||
@@ -288,25 +285,25 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = metasoup.find('dd',{'class':"series"})
|
||||
b = a.find('a', href=re.compile(r"/series/\d+"))
|
||||
series_name = b.string
|
||||
series_url = 'http://'+self.host+b['href']
|
||||
series_index = int(a.text.split(' ')[1])
|
||||
self.setSeries(series_name, series_index)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
# Find Series name from series URL.
|
||||
ddseries = metasoup.find('dd',{'class':"series"})
|
||||
|
||||
if ddseries:
|
||||
for i, a in enumerate(ddseries.findAll('a', href=re.compile(r"/series/\d+"))):
|
||||
series_name = stripHTML(a)
|
||||
series_url = 'http://'+self.host+a['href']
|
||||
series_index = int(stripHTML(a.previousSibling).replace(', ','').split(' ')[1]) # "Part # of" or ", Part #"
|
||||
self.story.setMetadata('series%02d'%i,"%s [%s]"%(series_name,series_index))
|
||||
self.story.setMetadata('series%02dUrl'%i,series_url)
|
||||
if i == 0:
|
||||
self.setSeries(series_name, series_index)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
chapter=bs.BeautifulSoup('<div class="story"></div>')
|
||||
chapter=bs.BeautifulSoup('<div class="story"></div>').find('div')
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr'))
|
||||
|
||||
|
||||
@@ -94,11 +94,10 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['rememberme'] = '1'
|
||||
params['action'] = 'login'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/elysian/user.php'
|
||||
loginUrl = 'http://www.' + self.getSiteDomain() + '/elysian/user.php'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
@@ -43,7 +44,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
self._setURL("https://"+self.getSiteDomain()\
|
||||
+"/s/"+self.story.getMetadata('storyId')+"/1/")
|
||||
|
||||
# ffnet update emails have the latest chapter URL.
|
||||
@@ -52,9 +53,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# chapter list doesn't get the latest. So save and use the
|
||||
# original URL given to pull chapter list & metadata.
|
||||
self.origurl = url
|
||||
if "http://m." in self.origurl:
|
||||
if "https://m." in self.origurl:
|
||||
## accept m(mobile)url, but use www.
|
||||
self.origurl = self.origurl.replace("http://m.","http://www.")
|
||||
self.origurl = self.origurl.replace("https://m.","https://www.")
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
@@ -66,10 +67,10 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.fanfiction.net/s/1234/1/ http://www.fanfiction.net/s/1234/12/ http://www.fanfiction.net/s/1234/1/Story_Title http://m.fanfiction.net/s/1234/1/"
|
||||
return "https://www.fanfiction.net/s/1234/1/ https://www.fanfiction.net/s/1234/12/ http://www.fanfiction.net/s/1234/1/Story_Title http://m.fanfiction.net/s/1234/1/"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www|m)?\.fanfiction\.net/s/\d+(/\d+)?(/|/[^/]+)?/?$"
|
||||
return r"https?://(www|m)?\.fanfiction\.net/s/\d+(/\d+)?(/|/[^/]+)?/?$"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
@@ -107,9 +108,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
except:
|
||||
chapcount = 1
|
||||
chapter = url.split('/',)[5]
|
||||
tryurl = "http://%s/s/%s/%d/"%(self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
chapcount+1)
|
||||
tryurl = "https://%s/s/%s/%d/"%(self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
chapcount+1)
|
||||
logger.debug('=Trying newer chapter: %s' % tryurl)
|
||||
newdata = self._fetchUrl(tryurl)
|
||||
if "not found. Please check to see you are not using an outdated url." \
|
||||
@@ -122,7 +123,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"^/u/\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.setMetadata('authorUrl','https://'+self.host+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
## Pull some additional data from html.
|
||||
@@ -141,9 +142,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# of Book, Movie, etc.
|
||||
self.story.addToList('category',stripHTML(categories[1]))
|
||||
elif 'Crossover' in categories[0]['href']:
|
||||
caturl = "http://%s%s"%(self.getSiteDomain(),categories[0]['href'])
|
||||
caturl = "https://%s%s"%(self.getSiteDomain(),categories[0]['href'])
|
||||
catsoup = bs.BeautifulSoup(self._fetchUrl(caturl))
|
||||
for a in catsoup.findAll('a',href=re.compile(r"^/crossovers/")):
|
||||
for a in catsoup.findAll('a',href=re.compile(r"^/crossovers/.+?/\d+/")):
|
||||
self.story.addToList('category',stripHTML(a))
|
||||
else:
|
||||
# Fall back. I ran across a story with a Crossver
|
||||
@@ -165,7 +166,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# after Rating, the same bit of text containing id:123456 contains
|
||||
# Complete--if completed.
|
||||
gui_table1i = soup.find('table',{'cellpadding':'5'})
|
||||
gui_table1i = soup.find('div',{'id':'content_wrapper_inner'})
|
||||
|
||||
self.story.setMetadata('title', stripHTML(gui_table1i.find('b'))) # title appears to be only(or at least first) bold tag in gui_table1i
|
||||
|
||||
@@ -199,15 +200,23 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
for g in genrelist:
|
||||
#logger.debug("g:(%s)"%g)
|
||||
if g.strip() not in ffnetgenres:
|
||||
logger.info("g not in ffnetgenres")
|
||||
#logger.info("g not in ffnetgenres")
|
||||
goodgenres=False
|
||||
if goodgenres:
|
||||
self.story.extendList('genre',genrelist)
|
||||
metalist=metalist[1:]
|
||||
|
||||
# Updated: <span data-xutime='1368059198'>5/8</span> - Published: <span data-xutime='1278984264'>7/12/2010</span>
|
||||
# Published: <span data-xutime='1384358726'>8m ago</span>
|
||||
dates = soup.findAll('span',{'data-xutime':re.compile(r'^\d+$')})
|
||||
if len(dates) > 1 :
|
||||
# updated get set to the same as published upstream if not found.
|
||||
self.story.setMetadata('dateUpdated',datetime.fromtimestamp(float(dates[0]['data-xutime'])))
|
||||
self.story.setMetadata('datePublished',datetime.fromtimestamp(float(dates[-1]['data-xutime'])))
|
||||
|
||||
donechars = False
|
||||
while len(metalist) > 0:
|
||||
if metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:'):
|
||||
if metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:') or metalist[0].startswith('Updated:') or metalist[0].startswith('Published:'):
|
||||
pass
|
||||
elif metalist[0].startswith('Reviews'):
|
||||
self.story.setMetadata('reviews',metalist[0].split(':')[1].strip())
|
||||
@@ -215,21 +224,21 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('favs',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Follows:'):
|
||||
self.story.setMetadata('follows',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Updated'):
|
||||
self.story.setMetadata('dateUpdated',makeDate(metalist[0].split(':')[1].strip(), '%m-%d-%y'))
|
||||
elif metalist[0].startswith('Published'):
|
||||
self.story.setMetadata('datePublished',makeDate(metalist[0].split(':')[1].strip(), '%m-%d-%y'))
|
||||
elif metalist[0].startswith('Words'):
|
||||
self.story.setMetadata('numWords',metalist[0].split(':')[1].strip())
|
||||
elif not donechars:
|
||||
self.story.extendList('characters',metalist[0].split('&'))
|
||||
# with 'pairing' support, pairings are bracketed w/o comma after
|
||||
# [Caspian X, Lucy Pevensie] Edmund Pevensie, Peter Pevensie
|
||||
self.story.extendList('characters',metalist[0].replace('[','').replace(']',',').split(','))
|
||||
|
||||
l = metalist[0]
|
||||
while '[' in l:
|
||||
self.story.addToList('ships',l[l.index('[')+1:l.index(']')].replace(', ','/'))
|
||||
l = l[l.index(']')+1:]
|
||||
|
||||
donechars = True
|
||||
metalist=metalist[1:]
|
||||
|
||||
# next might be characters, otherwise Reviews, Updated, Published, Words
|
||||
# if not ( metalist[0].startswith('Reviews') or metalist[0].startswith('Updated') or metalist[0].startswith('Published') or metalist[0].startswith('Words') or metalist[0].startswith('Chapters') ):
|
||||
# self.story.extendList('characters',metalist[0].split('&'))
|
||||
|
||||
if 'Status: Complete' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
@@ -253,9 +262,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
allOptions = select.findAll('option')
|
||||
for o in allOptions:
|
||||
url = u'http://%s/s/%s/%s/' % ( self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
o['value'])
|
||||
url = u'https://%s/s/%s/%s/' % ( self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
o['value'])
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
title = u"%s" % o
|
||||
title = re.sub(r'<[^>]+>','',title)
|
||||
@@ -266,11 +275,11 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
time.sleep(0.5) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
time.sleep(5.0) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
## more if hit too fast. This is in
|
||||
## additional to what ever the
|
||||
## slow_down_sleep_time setting is.
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "Please email this error message in full to <a href='mailto:support@fanfiction.com'>support@fanfiction.com</a>" in data:
|
||||
@@ -281,10 +290,11 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# don't care about anything before "<div class='storytextp"
|
||||
# (there's a space after storytextp, so no close quote(')) and
|
||||
# this kills any body tags.
|
||||
if "<div class='storytextp" not in data:
|
||||
divstr = "<div role='main'"
|
||||
if divstr not in data:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
else:
|
||||
data = data[data.index("<div class='storytextp"):]
|
||||
data = data[data.index(divstr):]
|
||||
data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
|
||||
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 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
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import time
|
||||
import json
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
#from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
class FictionPadSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
self.story.setMetadata('siteabbrev','fpad')
|
||||
self.dateformat = "%Y-%m-%dT%H:%M:%SZ"
|
||||
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:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("https://"+self.getSiteDomain()
|
||||
+"/author/"+m.group('author')
|
||||
+"/stories/"+self.story.getMetadata('storyId'))
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'fictionpad.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "https://fictionpad.com/author/Author/stories/1234/Some-Title"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
# http://fictionpad.com/author/Serdd/stories/4275
|
||||
return r"http(s)?://(www\.)?fictionpad\.com/author/(?P<author>[^/]+)/stories/(?P<id>\d+)"
|
||||
|
||||
# <form method="post" action="/signin">
|
||||
# <input name="authenticity_token" type="hidden" value="u+cfdXh46dRnwVnSlmE2B2BFmHgu760paqgBG6KQeos=" />
|
||||
# <input type="hidden" name="remember" value="1">
|
||||
# <strong class="help-start text-center">or with FictionPad</strong>
|
||||
# <label class="control-label hidden-placeholder">Pseudonym or Email Address</label>
|
||||
# <input name="login" class="input-block-level" type="text" placeholder="Pseudonym or Email Address" maxlength="50" required autofocus>
|
||||
# <label class="control-label hidden-placeholder">Password</label>
|
||||
# <input name="password" class="input-block-level" type="password" placeholder="Password" minlength="6" required>
|
||||
# <button type="submit" class="btn btn-primary btn-block">Sign In</button>
|
||||
# <p class="help-end">
|
||||
# <a href="/passwordreset">Forgot your password?</a>
|
||||
# </p>
|
||||
# </form>
|
||||
def performLogin(self):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['login'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['login'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['remember'] = '1'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/signin'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['login']))
|
||||
|
||||
## need to pull empty login page first to get authenticity_token
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(loginUrl))
|
||||
params['authenticity_token']=soup.find('input', {'name':'authenticity_token'})['value']
|
||||
|
||||
data = self._postUrl(loginUrl, params)
|
||||
|
||||
if "Invalid email/pseudonym and password combination." in data:
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['login']))
|
||||
raise exceptions.FailedToLogin(loginUrl,params['login'])
|
||||
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
# fetch the chapter. From that we will get almost all the
|
||||
# metadata and chapter list
|
||||
|
||||
url=self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
if "This is a mature story. Please sign in to read it." in data:
|
||||
self.performLogin()
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
find = "wordyarn.config.page = "
|
||||
data = data[data.index(find)+len(find):]
|
||||
data = data[:data.index("</script>")]
|
||||
data = data[:data.rindex(";")]
|
||||
data = data.replace('tables:','"tables":')
|
||||
tables = json.loads(data)['tables']
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# looks like only one author per story allowed.
|
||||
author = tables['users'][0]
|
||||
story = tables['stories'][0]
|
||||
story_ver = tables['story_versions'][0]
|
||||
|
||||
self.story.setMetadata('authorId',author['id'])
|
||||
self.story.setMetadata('author',author['display_name'])
|
||||
self.story.setMetadata('authorUrl','https://'+self.host+'/author/'+author['display_name']+'/stories')
|
||||
|
||||
self.story.setMetadata('title',story_ver['title'])
|
||||
self.setDescription(url,story_ver['description'])
|
||||
|
||||
if not ('assets/story_versions/covers' in story_ver['profile_image_url@2x']):
|
||||
self.setCoverImage(url,story_ver['profile_image_url@2x'])
|
||||
|
||||
self.story.setMetadata('datePublished',makeDate(story['published_at'], self.dateformat))
|
||||
self.story.setMetadata('dateUpdated',makeDate(story['published_at'], self.dateformat))
|
||||
|
||||
self.story.setMetadata('followers',story['followers_count'])
|
||||
self.story.setMetadata('comments',story['comments_count'])
|
||||
self.story.setMetadata('views',story['views_count'])
|
||||
self.story.setMetadata('likes',int(story['likes'])) # no idea why they floated these.
|
||||
self.story.setMetadata('dislikes',int(story['dislikes']))
|
||||
|
||||
if story_ver['is_complete']:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
self.story.setMetadata('rating', story_ver['maturity_level'])
|
||||
self.story.setMetadata('numWords', unicode(story_ver['word_count']))
|
||||
|
||||
for i in tables['fandoms']:
|
||||
self.story.addToList('category',i['name'])
|
||||
|
||||
for i in tables['genres']:
|
||||
self.story.addToList('genre',i['name'])
|
||||
|
||||
for i in tables['characters']:
|
||||
self.story.addToList('characters',i['name'])
|
||||
|
||||
for c in tables['chapters']:
|
||||
chtitle = "Chapter %d"%c['number']
|
||||
if c['title']:
|
||||
chtitle += " - %s"%c['title']
|
||||
self.chapterUrls.append((chtitle,c['body_url']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
if not url:
|
||||
data = u"<em>This chapter has no text.</em>"
|
||||
else:
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(u"<div id='story'>"+data+u"</div>")
|
||||
return self.utf8FromSoup(url,soup)
|
||||
|
||||
def getClass():
|
||||
return FictionPadSiteAdapter
|
||||
|
||||
@@ -85,7 +85,11 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
try:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url))
|
||||
data = self._fetchUrl(url)
|
||||
# non-existent/removed story urls get thrown to the front page.
|
||||
if "<h2>Welcome to FicWad</h2>" in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
@@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
@@ -42,6 +41,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self._setURL("http://"+self.getSiteDomain()+"/story/"+self.story.getMetadata('storyId')+"/")
|
||||
self.is_adult = False
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %b %Y"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'www.fimfiction.net'
|
||||
@@ -93,8 +96,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource" in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
if "/images/missing_story.png" in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
# Can cause problems if a missing story is referenced in a comment.
|
||||
# Shouldn't be needed anyway.
|
||||
# if "/images/missing_story.png" in data:
|
||||
# raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
if "This story has been marked as having adult content." in data:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
@@ -185,14 +190,42 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
hrstr="<hr />"
|
||||
descdivstr = '<div class="description">'+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
|
||||
self.setDescription(self.url,descdivstr)
|
||||
|
||||
|
||||
# Can't trust dates from API anymore I'm told.
|
||||
# Dates are in Unix time
|
||||
# Take the publish date from the first chapter posted
|
||||
rawDatePublished = storyMetadata["chapters"][0]["date_modified"]
|
||||
self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
|
||||
rawDateUpdated = storyMetadata["date_modified"]
|
||||
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
|
||||
# rawDatePublished = storyMetadata["chapters"][0]["date_modified"]
|
||||
# self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
|
||||
# rawDateUpdated = storyMetadata["date_modified"]
|
||||
# self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
|
||||
|
||||
oldestChapter = None
|
||||
newestChapter = None
|
||||
self.newestChapterNum = None # save for comparing during update.
|
||||
# Scan all chapters to find the oldest and newest, on
|
||||
# FiMFiction it's possible for authors to insert new chapters
|
||||
# out-of-order or change the dates of earlier ones by editing
|
||||
# them--That WILL break epub update.
|
||||
for index, chapterDate in enumerate(soup.findAll('span', {'class':'date'})):
|
||||
date=re.sub(r"(\d+)(st|nd|rd|th)",r"\1",chapterDate.contents[1].strip())
|
||||
chapterDate = makeDate(date,self.dateformat)
|
||||
if oldestChapter == None or chapterDate < oldestChapter:
|
||||
oldestChapter = chapterDate
|
||||
if newestChapter == None or chapterDate > newestChapter:
|
||||
newestChapter = chapterDate
|
||||
self.newestChapterNum = index
|
||||
|
||||
self.story.setMetadata("dateUpdated", newestChapter)
|
||||
|
||||
pubdatetag = soup.find('span', {'class':'date_approved'})
|
||||
if pubdatetag is None:
|
||||
self.story.setMetadata("datePublished", oldestChapter)
|
||||
else:
|
||||
pubdateraw = pubdatetag('span')[1].text
|
||||
datestripped=re.sub(r"(\d+)(st|nd|rd|th)",r"\1",pubdateraw.strip())
|
||||
pubDate = makeDate(datestripped,self.dateformat)
|
||||
self.story.setMetadata("datePublished", pubDate)
|
||||
|
||||
chars = soup.find("div", {"class":"inner_data"})
|
||||
# fimfic stopped putting the char name on or around the char
|
||||
# icon now for some reason. Pull it from the image name with
|
||||
@@ -215,10 +248,22 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if not isinstance(value,basestring):
|
||||
value = unicode(value)
|
||||
self.story.setMetadata(metakey, value)
|
||||
|
||||
|
||||
rawGroupList = soup.find('ul', {'id':'story_group_list'})
|
||||
if rawGroupList is not None:
|
||||
for groupName in rawGroupList.findAll('a', {'href':re.compile('^/group/')}):
|
||||
self.story.addToList("groups",stripHTML(groupName).replace(',', ';'))
|
||||
|
||||
def hookForUpdates(self,chaptercount):
|
||||
if self.oldchapters and len(self.oldchapters) > self.newestChapterNum:
|
||||
print("Existing epub has %s chapters\nNewest chapter is %s. Discarding old chapters from there on."%(len(self.oldchapters), self.newestChapterNum+1))
|
||||
self.oldchapters = self.oldchapters[:self.newestChapterNum]
|
||||
return len(self.oldchapters)
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'})
|
||||
if soup == None:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
@@ -93,7 +93,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
params['theusername'] = self.getConfig("username")
|
||||
params['thepassword'] = self.getConfig("password")
|
||||
params['rememberMe'] = '1'
|
||||
params['page'] = 'http://finestories.com/'
|
||||
params['page'] = 'http://'+self.getSiteDomain()+'/'
|
||||
params['submit'] = 'Login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
|
||||
@@ -262,8 +262,6 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
last[len(last)-1]=last[len(last)-1].append(next)
|
||||
div.append(div1)
|
||||
|
||||
|
||||
|
||||
# removing all the left-over stuff
|
||||
for a in div.findAll('span'):
|
||||
a.extract()
|
||||
|
||||
@@ -131,55 +131,55 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
## Finding the metadata is a bit of a pain. Most of the meta
|
||||
## data is in a center.table without a bgcolor.
|
||||
for center in soup.findAll('center'):
|
||||
table = center.find('table',{'bgcolor':None})
|
||||
if table:
|
||||
metastr = stripHTML(str(table)).replace('\n',' ').replace('\t',' ')
|
||||
# Rating: 12+ Story Reviews: 3
|
||||
# Chapters: 3
|
||||
# Characters: Andromeda, Ted, Bellatrix, R. Lestrange, Lucius, Narcissa, OC
|
||||
# Genre(s): Fluff, Romance, Young Adult Era: OtherPairings: Other Pairing, Lucius/Narcissa
|
||||
# Status: Completed
|
||||
# First Published: 2010.09.02
|
||||
# Last Published Chapter: 2010.09.28
|
||||
# Last Updated: 2010.09.28
|
||||
# Favorite Story Of: 1 users
|
||||
# Warnings: Scenes of a Mild Sexual Nature
|
||||
#for center in soup.findAll('center'):
|
||||
table = soup.find('table',{'class':'storymaininfo'})
|
||||
if table:
|
||||
metastr = stripHTML(str(table)).replace('\n',' ').replace('\t',' ')
|
||||
# Rating: 12+ Story Reviews: 3
|
||||
# Chapters: 3
|
||||
# Characters: Andromeda, Ted, Bellatrix, R. Lestrange, Lucius, Narcissa, OC
|
||||
# Genre(s): Fluff, Romance, Young Adult Era: OtherPairings: Other Pairing, Lucius/Narcissa
|
||||
# Status: Completed
|
||||
# First Published: 2010.09.02
|
||||
# Last Published Chapter: 2010.09.28
|
||||
# Last Updated: 2010.09.28
|
||||
# Favorite Story Of: 1 users
|
||||
# Warnings: Scenes of a Mild Sexual Nature
|
||||
|
||||
m = re.match(r".*?Status: Completed.*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('status','Completed')
|
||||
else:
|
||||
self.story.setMetadata('status','In-Progress')
|
||||
m = re.match(r".*?Status: Completed.*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('status','Completed')
|
||||
else:
|
||||
self.story.setMetadata('status','In-Progress')
|
||||
|
||||
m = re.match(r".*?Rating: (.+?) Story Reviews.*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('rating', m.group(1))
|
||||
m = re.match(r".*?Rating: (.+?) Story Reviews.*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('rating', m.group(1))
|
||||
|
||||
m = re.match(r".*?Genre\(s\): (.+?) Era.*?",metastr)
|
||||
if m:
|
||||
for g in m.group(1).split(','):
|
||||
self.story.addToList('genre',g)
|
||||
|
||||
m = re.match(r".*?Characters: (.+?) Genre.*?",metastr)
|
||||
if m:
|
||||
for g in m.group(1).split(','):
|
||||
self.story.addToList('characters',g)
|
||||
|
||||
m = re.match(r".*?Warnings: (.+).*?",metastr)
|
||||
if m:
|
||||
for w in m.group(1).split(','):
|
||||
if w != 'Now Warnings':
|
||||
self.story.addToList('warnings',w)
|
||||
|
||||
m = re.match(r".*?First Published: ([0-9\.]+).*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y.%m.%d"))
|
||||
m = re.match(r".*?Genre\(s\): (.+?) Era.*?",metastr)
|
||||
if m:
|
||||
for g in m.group(1).split(','):
|
||||
self.story.addToList('genre',g)
|
||||
|
||||
m = re.match(r".*?Characters: (.+?) Genre.*?",metastr)
|
||||
if m:
|
||||
for g in m.group(1).split(','):
|
||||
self.story.addToList('characters',g)
|
||||
|
||||
m = re.match(r".*?Warnings: (.+).*?",metastr)
|
||||
if m:
|
||||
for w in m.group(1).split(','):
|
||||
if w != 'Now Warnings':
|
||||
self.story.addToList('warnings',w)
|
||||
|
||||
m = re.match(r".*?First Published: ([0-9\.]+).*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y.%m.%d"))
|
||||
|
||||
# Updated can have more than one space after it. <shrug>
|
||||
m = re.match(r".*?Last Updated: ([0-9\.]+).*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y.%m.%d"))
|
||||
# Updated can have more than one space after it. <shrug>
|
||||
m = re.match(r".*?Last Updated: ([0-9\.]+).*?",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y.%m.%d"))
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
|
||||
@@ -37,11 +37,6 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
# hpfanficarchive.com blocks the default user-agent. However,
|
||||
# when asked, they said it was just general anti-spam, not
|
||||
# targeted at us. That lets me do this in good conscience:
|
||||
self.opener.addheaders = [('User-agent', 'FFDL/1.7')]
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
|
||||
@@ -46,8 +46,8 @@ class InDeathNetAdapter(BaseSiteAdapter):
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
@@ -87,6 +87,34 @@ class InDeathNetAdapter(BaseSiteAdapter):
|
||||
postdate = makeDate(d.group('day')+' '+ym.group('mon')+' '+ym.group('year'),self.dateformat)
|
||||
return postdate
|
||||
|
||||
def getAuthorData(self):
|
||||
|
||||
mainUrl = self.url.replace("/archive","")
|
||||
|
||||
try:
|
||||
maindata = self._fetchUrl(mainUrl)
|
||||
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.meta)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
mainsoup = bs.BeautifulSoup(maindata)
|
||||
|
||||
# find first entry
|
||||
e = mainsoup.find('div',{'class':"entry"})
|
||||
|
||||
# get post author as author
|
||||
d = e.find('div',{'class':"desc"})
|
||||
a = d.find('strong')
|
||||
self.story.setMetadata('author',a.contents[0].string.strip())
|
||||
|
||||
# Don't seem to be able to get author pages anymore
|
||||
self.story.setMetadata('authorUrl','http://www.indeath.net/')
|
||||
self.story.setMetadata('authorId','0')
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
@@ -110,17 +138,13 @@ class InDeathNetAdapter(BaseSiteAdapter):
|
||||
h = soup.find('a', id="blog_title")
|
||||
t = h.find('span')
|
||||
self.story.setMetadata('title',stripHTML(t.contents[0]).strip())
|
||||
|
||||
|
||||
s = t.find('div')
|
||||
if s != None:
|
||||
self.setDescription(url,s)
|
||||
|
||||
# Find authorid and URL from first link in Recent Entries (don't yet reference 'recent entries' - let's see if that is required)
|
||||
a = soup.find('a', href=re.compile(r"http://www.indeath.net/user/\d+\-[a-z0-9]+/$")) #http://www.indeath.net/user/9083-cyrex/
|
||||
m = re.search('http://www.indeath.net/user/(?P<id>\d+)\-(?P<name>[a-z0-9]*)/$',a['href'])
|
||||
self.story.setMetadata('authorId',m.group('id'))
|
||||
self.story.setMetadata('authorUrl',a['href'])
|
||||
self.story.setMetadata('author',m.group('name'))
|
||||
|
||||
# Get Author from main blog page since it's not reliably on the archive page
|
||||
self.getAuthorData()
|
||||
|
||||
# Find the chapters:
|
||||
chapters=soup.findAll('a', title="View entry", href=re.compile(r'http://www.indeath.net/blog/'+self.story.getMetadata('storyId')+"/entry\-(\d+)\-([^/]*)/$"))
|
||||
@@ -149,21 +173,20 @@ class InDeathNetAdapter(BaseSiteAdapter):
|
||||
if len(chapters)==1:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),chapter['href']))
|
||||
else:
|
||||
ct = stripHTML(chapter)
|
||||
tnew = re.match("(?i)"+self.story.getMetadata('title')+r" - (?P<newtitle>.*)$",ct)
|
||||
if tnew:
|
||||
chaptertitle = tnew.group('newtitle')
|
||||
else:
|
||||
chaptertitle = ct
|
||||
ct = stripHTML(chapter)
|
||||
tnew = re.match("(?i)"+self.story.getMetadata('title')+r" - (?P<newtitle>.*)$",ct)
|
||||
if tnew:
|
||||
chaptertitle = tnew.group('newtitle')
|
||||
else:
|
||||
chaptertitle = ct
|
||||
self.chapterUrls.append((chaptertitle,chapter['href']))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
|
||||
#chapter=bs.BeautifulSoup('<div class="story"></div>')
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr','span','center'))
|
||||
|
||||
@@ -37,11 +37,6 @@ class NCISFictionNetAdapter(BaseSiteAdapter):
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
# ncisfiction.net blocks the default user-agent. However,
|
||||
# when asked, they said it was just general anti-spam, not
|
||||
# targeted at us. That lets me do this in good conscience:
|
||||
self.opener.addheaders = [('User-agent', 'FFDL/1.7')]
|
||||
|
||||
self.decode = ["iso-8859-1",
|
||||
"Windows-1252"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
|
||||
@@ -166,10 +166,11 @@ class NickAndGregNetAdapter(BaseSiteAdapter):
|
||||
|
||||
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div = soup.find('table', {'class' : 'tblborder6'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# wrap a div around it.
|
||||
divsoup = bs.BeautifulStoneSoup('<div class="story"></div>',
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
div = divsoup.find('div')
|
||||
div.append(soup.find('table', {'class' : 'tblborder6'}))
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -251,8 +251,10 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulStoneSoup(data,
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
data = data.replace("HTML>","div>")
|
||||
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
#print("soup:%s"%soup)
|
||||
tag = soup.find('td', {'class' : 'story'})
|
||||
|
||||
@@ -22,7 +22,6 @@ import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
@@ -214,5 +214,6 @@ class SimplyUndeniableComAdapter(BaseSiteAdapter):
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
div.name='div'
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -241,5 +241,6 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
if None == story:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
story.name='div'
|
||||
|
||||
return self.utf8FromSoup(url,story)
|
||||
|
||||
@@ -103,6 +103,9 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "fatal MySQL error was encountered" in data:
|
||||
raise exceptions.FailedToDownload("Site SQL Error--bad story")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 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
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return StoriesOnlineNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
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
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0])
|
||||
if 'storyInfo' in self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/'+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','strol')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y-%m-%d"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'storiesonline.net'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/s/1234 http://"+self.getSiteDomain()+"/s/1234:4010"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Free Registration' in data \
|
||||
or "Invalid Password!" in data \
|
||||
or "Invalid User Name!" in data \
|
||||
or "Log In" in data \
|
||||
or "Access to unlinked chapters requires" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['theusername'] = self.username
|
||||
params['thepassword'] = self.password
|
||||
else:
|
||||
params['theusername'] = self.getConfig("username")
|
||||
params['thepassword'] = self.getConfig("password")
|
||||
params['rememberMe'] = '1'
|
||||
params['page'] = 'http://'+self.getSiteDomain()+'/'
|
||||
params['submit'] = 'Login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['theusername']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "My Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['theusername']))
|
||||
raise exceptions.FailedToLogin(url,params['theusername'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url+":i")
|
||||
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+":i")
|
||||
|
||||
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.")
|
||||
elif "Error! The story you're trying to access is being filtered by your choice of contents filtering." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! The story you're trying to access is being filtered by your choice of contents filtering.")
|
||||
|
||||
# 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.
|
||||
|
||||
## Title
|
||||
a = soup.find('h1')
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"/a/\w+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.setMetadata('author',stripHTML(a).replace("'s Page",""))
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.findAll('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId')+":\d+$"))
|
||||
if len(chapters) != 0:
|
||||
for chapter in chapters:
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']))
|
||||
else:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId')))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# surprisingly, the detailed page does not give enough details, so go to author's page
|
||||
skip=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+str(skip)))
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
|
||||
i=1
|
||||
break
|
||||
if a[len(a)-1] == lc2:
|
||||
skip=skip+10
|
||||
|
||||
for cat in lc2.findAll('div', {'class' : 'typediv'}):
|
||||
self.story.addToList('genre',cat.text)
|
||||
|
||||
# in lieu of word count.
|
||||
self.story.setMetadata('size', lc2.findNext('td', {'class' : 'num'}).text)
|
||||
|
||||
lc4 = lc2.findNext('td', {'class' : 'lc4'})
|
||||
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
|
||||
i = a.parent.text.split('(')[1].split(')')[0]
|
||||
self.setSeries(stripHTML(a), i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
|
||||
if a:
|
||||
self.story.setMetadata("universe",stripHTML(a))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
desc = lc4.contents[0]
|
||||
self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),desc)
|
||||
|
||||
for b in lc4.findAll('b'):
|
||||
#logger.debug('Getting metadata: "%s"' % b)
|
||||
label = b.text
|
||||
if label in ['Posted:', 'Concluded:', 'Updated:']:
|
||||
value = b.findNext('noscript').text
|
||||
#logger.debug('Have a date field label: "%s", value: "%s"' % (label, value))
|
||||
else:
|
||||
value = b.nextSibling
|
||||
#logger.debug('label: "%s", value: "%s"' % (label, value))
|
||||
|
||||
if 'Sex' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Codes' in label:
|
||||
for code in value.split(' '):
|
||||
self.story.addToList('codes',code)
|
||||
|
||||
if 'Posted' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Concluded' in label:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
#
|
||||
status = lc4.find('span', {'class' : 'ab'})
|
||||
if status != None:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
if "Last Activity" in status.text:
|
||||
# date is passed as a timestamp and converted in JS.
|
||||
value = status.findNext('noscript').text
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
else:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
# some big chapters are split over several pages
|
||||
pager = div.find('span', {'class' : 'pager'})
|
||||
if pager != None:
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
|
||||
for ur in urls:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div1 = soup.find('div', {'id' : 'story'})
|
||||
|
||||
# appending next section
|
||||
last=div.findAll('p')
|
||||
next=div1.find('span', {'class' : 'conTag'}).nextSibling
|
||||
|
||||
last[len(last)-1]=last[len(last)-1].append(next)
|
||||
div.append(div1)
|
||||
|
||||
# removing all the left-over stuff
|
||||
for a in div.findAll('span'):
|
||||
a.extract()
|
||||
|
||||
for a in div.findAll('h1'):
|
||||
a.extract()
|
||||
for a in div.findAll('h2'):
|
||||
a.extract()
|
||||
for a in div.findAll('h3'):
|
||||
a.extract()
|
||||
for a in div.findAll('h4'):
|
||||
a.extract()
|
||||
for a in div.findAll('br'):
|
||||
a.extract()
|
||||
for a in div.findAll('div', {'class' : 'date'}):
|
||||
a.extract()
|
||||
|
||||
a = div.find('form')
|
||||
if a != None:
|
||||
b = a.nextSibling
|
||||
while b != None:
|
||||
a.extract()
|
||||
a=b
|
||||
b=b.nextSibling
|
||||
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -323,7 +323,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
</div>
|
||||
'''
|
||||
elif self.story.getMetadata('storyId') == '0':
|
||||
text=u'''
|
||||
text=u'''<div>
|
||||
<h3>45. Pronglet Returns to Hogwarts: Chapter 7</h3>
|
||||
<br />
|
||||
eyes… but I’m not convinced we should automatically<br />
|
||||
@@ -332,6 +332,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
<br /><br />
|
||||
“Sure, invite her along. Does she have children?”<br />
|
||||
<br />
|
||||
</div>
|
||||
'''
|
||||
else:
|
||||
if self.story.getMetadata('storyId') == '667':
|
||||
|
||||
@@ -231,10 +231,10 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
BtVS = True
|
||||
BtVSNonX = False
|
||||
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', 'Non-BtVS Crossovers']:
|
||||
if cat.string not in ['General', 'Non-BtVS/AtS Stories', 'Non-BTVS/AtS Stories', 'BtVS/AtS Non-Crossover', 'Non-BtVS Crossovers']:
|
||||
self.story.addToList('category',cat.string)
|
||||
else:
|
||||
if 'Non-BtVS' in cat.string:
|
||||
if 'Non-BtVS' in cat.string or 'Non-BTVS' in cat.string:
|
||||
BtVS = False
|
||||
if 'BtVS/AtS Non-Crossover' == cat.string:
|
||||
BtVSNonX = True
|
||||
|
||||
@@ -22,6 +22,7 @@ import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
@@ -227,7 +228,8 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
if None == span:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
|
||||
span.name='div'
|
||||
return self.utf8FromSoup(url,span)
|
||||
|
||||
def getClass():
|
||||
|
||||
@@ -26,6 +26,7 @@ from functools import partial
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from ..htmlheuristics import replace_br_with_p
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,6 +78,8 @@ class BaseSiteAdapter(Configurable):
|
||||
self.is_adult=False
|
||||
|
||||
self.opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
|
||||
## Specific UA because too many sites are blocking the default python UA.
|
||||
self.opener.addheaders = [('User-agent', self.getConfig('user_agent'))]
|
||||
self.storyDone = False
|
||||
self.metadataDone = False
|
||||
self.story = Story(configuration)
|
||||
@@ -245,6 +248,10 @@ class BaseSiteAdapter(Configurable):
|
||||
self.metadataDone = True
|
||||
return self.story
|
||||
|
||||
def hookForUpdates(self,chaptercount):
|
||||
"Usually not needed."
|
||||
return chaptercount
|
||||
|
||||
###############################
|
||||
|
||||
@staticmethod
|
||||
@@ -345,18 +352,13 @@ class BaseSiteAdapter(Configurable):
|
||||
if t.name in ('center'):
|
||||
t['class']=t.name
|
||||
t.name='div'
|
||||
# removes paired, but empty tags.
|
||||
if t.string != None and len(t.string.strip()) == 0 :
|
||||
# removes paired, but empty non paragraph tags.
|
||||
if t.name not in ('p') and t.string != None and len(t.string.strip()) == 0 :
|
||||
t.extract()
|
||||
|
||||
|
||||
retval = soup.__str__('utf8').decode('utf-8')
|
||||
|
||||
if self.getConfig('replace_hr'):
|
||||
# replacing a self-closing tag with a container tag in the
|
||||
# soup is more difficult than it first appears. So cheat.
|
||||
retval = retval.replace("<hr />","<div class='center'>* * *</div>")
|
||||
|
||||
if self.getConfig('nook_img_fix'):
|
||||
if self.getConfig('nook_img_fix') and not self.getConfig('replace_br_with_p'):
|
||||
# if the <img> tag doesn't have a div or a p around it,
|
||||
# nook gets confused and displays it on every page after
|
||||
# that under the text for the rest of the chapter.
|
||||
@@ -365,7 +367,19 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
# Don't want body tags in chapter html--writers add them.
|
||||
# This is primarily for epub updates.
|
||||
return re.sub(r"</?body>\r?\n?","",retval)
|
||||
retval = re.sub(r"</?body>\r?\n?","",retval)
|
||||
|
||||
if self.getConfig("replace_br_with_p"):
|
||||
# Apply heuristic processing to replace <br> paragraph
|
||||
# breaks with <p> tags.
|
||||
retval = replace_br_with_p(retval)
|
||||
|
||||
if self.getConfig('replace_hr'):
|
||||
# replacing a self-closing tag with a container tag in the
|
||||
# soup is more difficult than it first appears. So cheat.
|
||||
retval = retval.replace("<hr />","<div class='center'>* * *</div>")
|
||||
|
||||
return retval
|
||||
|
||||
def cachedfetch(realfetch,cache,url):
|
||||
if url in cache:
|
||||
@@ -377,7 +391,7 @@ fullmon = {"January":"01", "February":"02", "March":"03", "April":"04", "May":"0
|
||||
"June":"06","July":"07", "August":"08", "September":"09", "October":"10",
|
||||
"November":"11", "December":"12" }
|
||||
|
||||
def makeDate(string,format):
|
||||
def makeDate(string,dateform):
|
||||
# Surprise! Abstracting this turned out to be more useful than
|
||||
# just saving bytes.
|
||||
|
||||
@@ -386,10 +400,10 @@ def makeDate(string,format):
|
||||
# there's non-english content. -- ficbook.net now makes that a
|
||||
# lie. It has to do something even more complicated to get
|
||||
# Russian month names correct everywhere.
|
||||
do_abbrev = "%b" in format
|
||||
do_abbrev = "%b" in dateform
|
||||
|
||||
if "%B" in format or do_abbrev:
|
||||
format = format.replace("%B","%m").replace("%b","%m")
|
||||
if "%B" in dateform or do_abbrev:
|
||||
dateform = dateform.replace("%B","%m").replace("%b","%m")
|
||||
for (name,num) in fullmon.items():
|
||||
if do_abbrev:
|
||||
name = name[:3] # first three for abbrev
|
||||
@@ -397,5 +411,5 @@ def makeDate(string,format):
|
||||
string = string.replace(name,num)
|
||||
break
|
||||
|
||||
return datetime.datetime.strptime(string,format)
|
||||
return datetime.datetime.strptime(string,dateform)
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import re, os, traceback
|
||||
from zipfile import ZipFile
|
||||
from xml.dom.minidom import parseString
|
||||
@@ -72,7 +75,7 @@ def get_update_data(inputio,
|
||||
# remove all .. and the path part above it, if present.
|
||||
# Mostly for epubs edited by Sigil.
|
||||
src = re.sub(r"([^/]+/\.\./)","",src)
|
||||
print("epubutils: found pre-existing cover image:%s"%src)
|
||||
#print("epubutils: found pre-existing cover image:%s"%src)
|
||||
oldcoverimghref = src
|
||||
oldcoverimgdata = epub.read(src)
|
||||
for item in contentdom.getElementsByTagName("item"):
|
||||
@@ -81,8 +84,8 @@ def get_update_data(inputio,
|
||||
break
|
||||
oldcover = (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata)
|
||||
except Exception as e:
|
||||
print("Cover Image %s not found"%src)
|
||||
print("Exception: %s"%(unicode(e)))
|
||||
logger.warn("Cover Image %s not found"%src)
|
||||
logger.warn("Exception: %s"%(unicode(e)))
|
||||
traceback.print_exc()
|
||||
|
||||
filecount = 0
|
||||
@@ -118,8 +121,8 @@ def get_update_data(inputio,
|
||||
images[longdesc] = data
|
||||
img['src'] = img['longdesc']
|
||||
except Exception as e:
|
||||
print("Image %s not found!\n(originally:%s)"%(newsrc,longdesc))
|
||||
print("Exception: %s"%(unicode(e)))
|
||||
logger.warn("Image %s not found!\n(originally:%s)"%(newsrc,longdesc))
|
||||
logger.warn("Exception: %s"%(unicode(e)))
|
||||
traceback.print_exc()
|
||||
soup = soup.find('body')
|
||||
# ffdl epubs have chapter title h3
|
||||
@@ -143,8 +146,8 @@ def get_update_data(inputio,
|
||||
except:
|
||||
pass
|
||||
|
||||
for k in images.keys():
|
||||
print("\tlongdesc:%s\n\tData len:%s\n"%(k,len(images[k])))
|
||||
#for k in images.keys():
|
||||
#print("\tlongdesc:%s\n\tData len:%s\n"%(k,len(images[k])))
|
||||
return (source,filecount,soups,images,oldcover,calibrebookmark,logfile)
|
||||
|
||||
def get_path_part(n):
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import re
|
||||
|
||||
def _unirepl(match):
|
||||
@@ -31,7 +34,7 @@ def _unirepl(match):
|
||||
except:
|
||||
# This way, at least if there's more of entities out there
|
||||
# that fail, it doesn't blow the entire download.
|
||||
print "Numeric entity translation failed, skipping: &#x%s%s"%(match.group(1),match.group(2))
|
||||
logger.warn("Numeric entity translation failed, skipping: &#x%s%s"%(match.group(1),match.group(2)))
|
||||
retval = ""
|
||||
return retval
|
||||
|
||||
@@ -69,8 +72,9 @@ def removeEntities(text):
|
||||
|
||||
if text is None:
|
||||
return ""
|
||||
if not (isinstance(text,str) or isinstance(text,unicode)):
|
||||
return str(text)
|
||||
|
||||
if not isinstance(text,basestring):
|
||||
return unicode(text)
|
||||
|
||||
try:
|
||||
t = text.decode('utf-8')
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 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 logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import codecs
|
||||
import BeautifulSoup as bs
|
||||
import HtmlTagStack as stack
|
||||
|
||||
from . import exceptions as exceptions
|
||||
|
||||
def replace_br_with_p(body):
|
||||
|
||||
# Ascii character (and Unicode as well) xA0 is a non-breaking space, ascii code 160.
|
||||
# However, Python Regex does not recognize it as a whitespace, so we'll be changing it to a reagular space.
|
||||
body = body.replace(u'\xa0', u' ')
|
||||
|
||||
if body.find('>') == -1 or body.rfind('<') == -1:
|
||||
return body
|
||||
|
||||
# logger.debug(u'BODY start.: ' + body[:250])
|
||||
# logger.debug(u'BODY end...: ' + body[-250:])
|
||||
# logger.debug(u'BODY.......: ' + body)
|
||||
|
||||
# clean breaks (<br />), removing whitespaces between them.
|
||||
body = re.sub(r'\s*<br[^>]*>\s*', r'<br />', body)
|
||||
|
||||
# change surrounding div to a p and remove attrs Top surrounding
|
||||
# tag in all cases now should be div, to just strip the first and
|
||||
# last tags.
|
||||
if is_valid_block(body) and body.find('<div') == 0:
|
||||
body = body[body.index('>')+1:body.rindex('<')]
|
||||
|
||||
body = soup_up_div(u'<div>' + body + u'</div>')
|
||||
|
||||
body = body[body.index('>')+1:body.rindex('<')]
|
||||
|
||||
# Find all bexisting blocks with p, pre and blockquote tags, we need to shields break tags inside those.
|
||||
# This is for "lenient" mode, however it is also used to clear break tags before and after the block elements.
|
||||
blocksRegex = re.compile(r'(\s*<br\ />\s*)*\s*<(pre|p|blockquote|table)([^>]*)>(.+?)</\2>\s*(\s*<br\ />\s*)*', re.DOTALL)
|
||||
body = blocksRegex.sub(r'\n<\2\3>\4</\2>\n', body)
|
||||
|
||||
# if aggressive mode = true
|
||||
# blocksRegex = re.compile(r'(\s*<br\ */*>\s*)*\s*<(pre)([^>]*)>(.+?)</\2>\s*(\s*<br\ */*>\s*)*', re.DOTALL)
|
||||
# In aggressive mode, we also check breakes inside blockquotes, meaning we can get orphaned paragraph tags.
|
||||
# body = re.sub(r'<blockquote([^>]*)>(.+?)</blockquote>', r'<blockquote\1><p>\2</p></blockquote>', body, re.DOTALL)
|
||||
# end aggressive mode
|
||||
|
||||
blocks = blocksRegex.finditer(body)
|
||||
# For our replacements to work, we need to work backwards, so we reverse the iterator.
|
||||
blocksList = []
|
||||
for match in blocks:
|
||||
blocksList.insert(0, match)
|
||||
|
||||
for match in blocksList:
|
||||
group4 = match.group(4).replace(u'<br />', u'{br /}')
|
||||
body = body[:match.start(4)] + group4 + body[match.end(4):]
|
||||
|
||||
# change surrounding div to a p and remove attrs Top surrounding
|
||||
# tag in all cases now should be div, to just strip the first and
|
||||
# last tags.
|
||||
# body = u'<p>' + body + u'</p>'
|
||||
|
||||
# Nuke div tags surrounding a HR tag.
|
||||
body = re.sub(r'<div[^>]+>\s*<hr[^>]+>\s*</div>', r'\n<hr />\n', body)
|
||||
|
||||
# So many people add formatting to their HR tags, and ePub does not allow those, we are supposed to use css.
|
||||
# This nukes the hr tag attributes.
|
||||
body = re.sub(r'\s*<hr[^>]+>\s*', r'\n<hr />\n', body)
|
||||
|
||||
# Remove leading and trailing breaks from HR tags
|
||||
body = re.sub(r'\s*(<br\ \/>)*\s*<hr\ \/>\s*(<br\ \/>)*\s*', r'\n<hr />\n', body)
|
||||
# Nuking breaks leading paragraps that may be in the body. They are eventually treated as <p><br /></p>
|
||||
body = re.sub(r'\s*(<br\ \/>)+\s*<p', r'\n<p></p>\n<p', body)
|
||||
# Nuking breaks trailing paragraps that may be in the body. They are eventually treated as <p><br /></p>
|
||||
body = re.sub(r'</p>\s*(<br\ \/>)+\s*', r'</p>\n<p></p>\n', body)
|
||||
|
||||
# Because a leading or trailing non break tag will break the following code, we have to mess around rather badly for a few lines.
|
||||
body = body.replace(u'[',u'&squareBracketStart;')
|
||||
body = body.replace(u']',u'&squareBracketEnd;')
|
||||
body = body.replace(u'<br />',u'[br /]')
|
||||
|
||||
breaksRegexp = [
|
||||
re.compile(r'([^\]])(\[br\ \/\])([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){2}([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){3}([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){4}([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){5}([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){6}([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){7}([^\[])'),
|
||||
re.compile(r'([^\]])(\[br\ \/\]){8}([^\[])'),
|
||||
re.compile(r'(\[br\ \/\]){9,}')]
|
||||
|
||||
breaksCount = [
|
||||
len(breaksRegexp[0].findall(body)),
|
||||
len(breaksRegexp[1].findall(body)),
|
||||
len(breaksRegexp[2].findall(body)),
|
||||
len(breaksRegexp[3].findall(body)),
|
||||
len(breaksRegexp[4].findall(body)),
|
||||
len(breaksRegexp[5].findall(body)),
|
||||
len(breaksRegexp[6].findall(body)),
|
||||
len(breaksRegexp[7].findall(body))]
|
||||
|
||||
breaksMax = 0
|
||||
breaksMaxIndex = 0;
|
||||
|
||||
for i in range(1,len(breaksCount)):
|
||||
if breaksCount[i] >= breaksMax:
|
||||
breaksMax = breaksCount[i]
|
||||
breaksMaxIndex = i
|
||||
|
||||
lines = body.split(u'[br /]')
|
||||
contentLines = 0;
|
||||
contentLinesSum = 0;
|
||||
longestLineLength = 0;
|
||||
averageLineLength = 0;
|
||||
|
||||
for line in lines:
|
||||
lineLen = len(line.strip())
|
||||
if lineLen > 0:
|
||||
contentLines += 1
|
||||
contentLinesSum += lineLen
|
||||
if lineLen > longestLineLength:
|
||||
longestLineLength = lineLen
|
||||
|
||||
averageLineLength = contentLinesSum/contentLines
|
||||
|
||||
logger.debug(u'---')
|
||||
logger.debug(u'Lines.............: ' + str(len(lines)))
|
||||
logger.debug(u'contentLines......: ' + str(contentLines))
|
||||
logger.debug(u'contentLinesSum...: ' + str(contentLinesSum))
|
||||
logger.debug(u'longestLineLength.: ' + str(longestLineLength))
|
||||
logger.debug(u'averageLineLength.: ' + str(averageLineLength))
|
||||
|
||||
if breaksMaxIndex == len(breaksCount)-1 and breaksMax < 2:
|
||||
breaksMaxIndex = 0
|
||||
breaksMax = breaksCount[0]
|
||||
|
||||
logger.debug(u'---')
|
||||
logger.debug(u'breaks 1: ' + str(breaksCount[0]))
|
||||
logger.debug(u'breaks 2: ' + str(breaksCount[1]))
|
||||
logger.debug(u'breaks 3: ' + str(breaksCount[2]))
|
||||
logger.debug(u'breaks 4: ' + str(breaksCount[3]))
|
||||
logger.debug(u'breaks 5: ' + str(breaksCount[4]))
|
||||
logger.debug(u'breaks 6: ' + str(breaksCount[5]))
|
||||
logger.debug(u'breaks 7: ' + str(breaksCount[6]))
|
||||
logger.debug(u'breaks 8: ' + str(breaksCount[7]))
|
||||
logger.debug(u'----')
|
||||
logger.debug(u'max found: ' + str(breaksMax))
|
||||
logger.debug(u'max Index: ' + str(breaksMaxIndex))
|
||||
logger.debug(u'----')
|
||||
|
||||
if breaksMaxIndex > 0 and breaksCount[0] > breaksMax and averageLineLength < 90:
|
||||
body = breaksRegexp[0].sub(r'\1 \n\3', body)
|
||||
|
||||
# Find all instances of consecutive breaks less than otr equal to the max count use most often
|
||||
# replase those tags to inverted p tag pairs, those with more connsecutive breaks are replaced them with a horisontal line
|
||||
for i in range(len(breaksCount)):
|
||||
# if i > 0 or breaksMaxIndex == 0:
|
||||
if i <= breaksMaxIndex:
|
||||
logger.debug(str(i) + u' <= breaksMaxIndex (' + str(breaksMaxIndex) + u')')
|
||||
body = breaksRegexp[i].sub(r'\1</p>\n<p>\3', body)
|
||||
elif i == breaksMaxIndex+1:
|
||||
logger.debug(str(i) + u' == breaksMaxIndex+1 (' + str(breaksMaxIndex+1) + u')')
|
||||
body = breaksRegexp[i].sub(r'\1</p>\n<p><br/></p>\n<p>\3', body)
|
||||
else:
|
||||
logger.debug(str(i) + u' > breaksMaxIndex+1 (' + str(breaksMaxIndex+1) + u')')
|
||||
body = breaksRegexp[i].sub(r'\1</p>\n<hr />\n<p>\3', body)
|
||||
|
||||
body = breaksRegexp[8].sub(r'</p>\n<hr />\n<p>', body)
|
||||
|
||||
# Reverting the square brackets
|
||||
body = body.replace(u'[', u'<')
|
||||
body = body.replace(u']', u'>')
|
||||
body = body.replace(u'&squareBracketStart;', u'[')
|
||||
body = body.replace(u'&squareBracketEnd;', u']')
|
||||
|
||||
body = body.replace(u'{p}', u'<p>')
|
||||
body = body.replace(u'{/p}', u'</p>')
|
||||
|
||||
# If for some reason, a third break makes its way inside the paragraph, preplace that with the empty paragraph for the additional linespaing.
|
||||
body = re.sub(r'<p>\s*(<br\ \/>)+', r'<p><br /></p>\n<p>', body)
|
||||
|
||||
# change empty p tags to include a br to force spacing.
|
||||
body = re.sub(r'<p>\s*</p>', r'<p><br/></p>', body)
|
||||
|
||||
# Clean up hr tags, and add inverted p tag pairs
|
||||
body = re.sub(r'(<div[^>]+>)*\s*<hr\ \/>\s*(</div>)*', r'\n<hr />\n', body)
|
||||
|
||||
# Clean up hr tags, and add inverted p tag pairs
|
||||
body = re.sub(r'\s*<hr\ \/>\s*', r'</p>\n<hr />\n<p>', body)
|
||||
|
||||
# Because the previous regexp may cause trouble if the hr tag already had a p tag pair around it, w nee dot repair that.
|
||||
# Repeated opening p tags are condenced to one. As we added the extra leading opening p tags, we can safely assume that
|
||||
# the last in such a chain must be the original. Lets keep its attributes if they are there.
|
||||
body = re.sub(r'\s*(<p[^>]*>\s*)+<p([^>]*)>\s*', r'\n<p\2>', body)
|
||||
# Repeated closing p tags are condenced to one
|
||||
body = re.sub(r'\s*(<\/\s*p>\s*){2,}', r'</p>\n', body)
|
||||
|
||||
# superflous cleaning, remove whitespaces traling opening p tags. These does affect formatting.
|
||||
body = re.sub(r'\s*<p([^>]*)>\s*', r'\n<p\1>', body)
|
||||
# superflous cleaning, remove whitespaces leading closing p tags. These does not affect formatting.
|
||||
body = re.sub(r'\s*</p>\s*', r'</p>\n', body)
|
||||
|
||||
# Remove empty tag pairs
|
||||
body = re.sub(r'\s*<(\S+)[^>]*>\s*</\1>', r'', body)
|
||||
|
||||
body = body.replace(u'{br /}', u'<br />')
|
||||
body = body.strip()
|
||||
|
||||
# re-wrap in div tag.
|
||||
body = u'<div>\n' + body + u'</div>\n'
|
||||
|
||||
# return body
|
||||
return tag_sanitizer(body)
|
||||
|
||||
def is_valid_block(block):
|
||||
return str(block).find('<') == 0 and str(block).find('<!') != 0
|
||||
|
||||
def soup_up_div(body):
|
||||
blockTags = ['address', 'blockquote', 'del', 'div', 'dl', 'fieldset', 'form', 'ins', 'noscript', 'ol', 'p', 'pre', 'table', 'ul']
|
||||
recurseTags = ['blockquote', 'div', 'noscript']
|
||||
|
||||
tag = body[:body.index('>')+1]
|
||||
tagend = body[body.rindex('<'):]
|
||||
|
||||
body = body.replace(u'<br />', u'[br /]')
|
||||
|
||||
soup = bs.BeautifulSoup(body)
|
||||
|
||||
body = u''
|
||||
lastElement = 1 # 1 = block, 2 = nested, 3 = invalid
|
||||
|
||||
for i in soup.contents[0]:
|
||||
if str(i).strip().__len__() > 0:
|
||||
s = str(i)
|
||||
if type(i) == bs.Tag:
|
||||
if i.name in blockTags:
|
||||
if lastElement > 1:
|
||||
body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*')
|
||||
body += u'{/p}'
|
||||
|
||||
lastElement = 1
|
||||
|
||||
if i.name in recurseTags:
|
||||
s = soup_up_div(s)
|
||||
|
||||
body += s.strip() + '\n'
|
||||
else:
|
||||
if lastElement == 1:
|
||||
body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*')
|
||||
body += u'{p}'
|
||||
|
||||
lastElement = 2
|
||||
body += s
|
||||
elif type(i) == bs.Comment:
|
||||
body += s
|
||||
else:
|
||||
if lastElement == 1:
|
||||
body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*')
|
||||
body += u'{p}'
|
||||
|
||||
lastElement = 3
|
||||
body += s
|
||||
|
||||
if lastElement > 1:
|
||||
body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*')
|
||||
body += u'{/p}'
|
||||
|
||||
body = body.replace(u'[br /]', u'<br />')
|
||||
|
||||
return tag + body + tagend
|
||||
|
||||
|
||||
def is_end_tag(tag):
|
||||
return re.match(r'</([^\ >]+)>', tag) != None
|
||||
|
||||
def is_comment_tag(tag):
|
||||
return re.match(r'<\!\-\-([^>]+)>', tag) != None
|
||||
|
||||
def is_closed_tag(tag):
|
||||
return re.match(r'<(.+?)/>', tag) != None
|
||||
|
||||
def tag_sanitizer(html):
|
||||
blockTags = ['address', 'blockquote', 'del', 'div', 'dl', 'fieldset', 'form', 'ins', 'noscript', 'ol', 'pre', 'table', 'ul']
|
||||
|
||||
body = u''
|
||||
tags = re.findall(r'(<[^>]+>)([^<]*)', html)
|
||||
|
||||
for rTag in tags:
|
||||
name = stack.get_tag_name(rTag[0])
|
||||
is_end = is_end_tag(rTag[0])
|
||||
is_closed = is_closed_tag(rTag[0]) or is_comment_tag(rTag[0])
|
||||
|
||||
# is_comment = is_comment_tag(rTag[0])
|
||||
# logger.debug(u'%s > isEnd: %s > isClosed: %s > isComment: %s'%(name, str(is_end), str(is_closed), str(is_comment)))
|
||||
# logger.debug(u'> %s%s\n'%(rTag[0], rTag[1]))
|
||||
|
||||
if name in blockTags:
|
||||
body += rTag[0]
|
||||
body += rTag[1]
|
||||
elif name == u'p':
|
||||
if is_end:
|
||||
body += stack.spool_end()
|
||||
body += rTag[0]
|
||||
body += rTag[1]
|
||||
elif is_closed:
|
||||
body += rTag[0]
|
||||
body += rTag[1]
|
||||
else:
|
||||
body += rTag[0]
|
||||
body += stack.spool_start()
|
||||
body += rTag[1]
|
||||
else:
|
||||
if is_end:
|
||||
t = stack.get_last()
|
||||
tn = stack.get_tag_name(t)
|
||||
rTn = stack.get_tag_name(rTag[0])
|
||||
if tn == rTn:
|
||||
body += rTag[0]
|
||||
stack.pop()
|
||||
elif not is_closed:
|
||||
stack.push(rTag[0])
|
||||
body += rTag[0]
|
||||
else:
|
||||
body += rTag[0]
|
||||
|
||||
body += rTag[1]
|
||||
stack.flush()
|
||||
return body
|
||||
@@ -349,6 +349,8 @@ class Story(Configurable):
|
||||
return removeAllEntities(value)
|
||||
else:
|
||||
return value
|
||||
else: #if self.getConfig("default_value_"+key):
|
||||
return self.getConfig("default_value_"+key)
|
||||
|
||||
def getAllMetadata(self,
|
||||
removeallentities=False,
|
||||
@@ -485,6 +487,8 @@ class Story(Configurable):
|
||||
|
||||
if None in subjectset:
|
||||
subjectset.remove(None)
|
||||
if '' in subjectset:
|
||||
subjectset.remove('')
|
||||
|
||||
return list(subjectset | set(self.getConfigList("extratags")))
|
||||
|
||||
@@ -627,7 +631,7 @@ class Story(Configurable):
|
||||
ext)
|
||||
self.imgtuples.append({'newsrc':newsrc,'mime':mime,'data':data})
|
||||
|
||||
logger.debug("\nimgurl:%s\nnewsrc:%s\nimage size:%d\n"%(imgurl,newsrc,len(data)))
|
||||
#logger.debug("\nimgurl:%s\nnewsrc:%s\nimage size:%d\n"%(imgurl,newsrc,len(data)))
|
||||
else:
|
||||
newsrc = self.imgtuples[self.imgurls.index(imgurl)]['newsrc']
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ class BaseStoryWriter(Configurable):
|
||||
lastupdated=self.story.getMetadataRaw('dateUpdated').date()
|
||||
fileupdated=datetime.datetime.fromtimestamp(os.stat(outfilename)[8]).date()
|
||||
if fileupdated > lastupdated:
|
||||
print "File(%s) Updated(%s) more recently than Story(%s) - Skipping" % (outfilename,fileupdated,lastupdated)
|
||||
logger.warn("File(%s) Updated(%s) more recently than Story(%s) - Skipping" % (outfilename,fileupdated,lastupdated))
|
||||
return
|
||||
if not metaonly:
|
||||
self.story = self.adapter.getStory() # get full story
|
||||
|
||||
@@ -361,7 +361,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
|
||||
metadata.appendChild(newTag(contentdom,"dc:contributor",text="fanficdownloader [http://fanficdownloader.googlecode.com]",attrs={"opf:role":"bkp"}))
|
||||
metadata.appendChild(newTag(contentdom,"dc:rights",text=""))
|
||||
if self.story.getMetadata('langcode') != None:
|
||||
if self.story.getMetadata('langcode'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:language",text=self.story.getMetadata('langcode')))
|
||||
else:
|
||||
metadata.appendChild(newTag(contentdom,"dc:language",text='en'))
|
||||
|
||||
+14
-3
@@ -46,6 +46,17 @@
|
||||
{{yourfile}}
|
||||
<!-- </div> -->
|
||||
|
||||
<h3>fanfiction.net / fimfiction.net</h3>
|
||||
<p>
|
||||
Fanfiction.net appears to be blocking access from Google
|
||||
App Engine, which prevents this web service. There's
|
||||
nothing I can do about it. At the time of writing, the
|
||||
latest CLI and calibre plugin versions worked.
|
||||
</p>
|
||||
<p>It appears that FimFiction.net is also blocking access from Google
|
||||
App Engine now.
|
||||
</p>
|
||||
|
||||
{% if authorized %}
|
||||
<form action="/fdown" method="post">
|
||||
<div id='urlbox'>
|
||||
@@ -57,10 +68,10 @@
|
||||
<h3>Changes:</h3>
|
||||
<p>
|
||||
<ul>
|
||||
<li>Minor optimizations.</li>
|
||||
<li>Additional improvements to the replace_br_with_p heuristic HTML processing feature.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
|
||||
@@ -69,7 +80,7 @@
|
||||
If you have any problems with this application, please
|
||||
report them in
|
||||
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
|
||||
<a href="http://4-4-67.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://4-4-84.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
|
||||
+2
-2
@@ -24,14 +24,14 @@ from makezip import createZipFile
|
||||
|
||||
if __name__=="__main__":
|
||||
filename="FanFictionDownLoader.zip"
|
||||
exclude=['*.pyc','*~','*.xcf','*[0-9].png']
|
||||
exclude=['*.pyc','*~','*.xcf','*[0-9].png','*.po','*.pot','*default.mo']
|
||||
# from top dir. 'w' for overwrite
|
||||
createZipFile(filename,"w",
|
||||
['plugin-defaults.ini','plugin-example.ini','fanficdownloader','downloader.py','defaults.ini'],
|
||||
exclude=exclude)
|
||||
#from calibre-plugin dir. 'a' for append
|
||||
os.chdir('calibre-plugin')
|
||||
files=['about.txt','images',]
|
||||
files=['about.txt','images','translations']
|
||||
files.extend(glob('*.py'))
|
||||
files.extend(glob('plugin-import-name-*.txt'))
|
||||
createZipFile("../"+filename,"a",
|
||||
|
||||
+103
-7
@@ -16,7 +16,22 @@
|
||||
[defaults]
|
||||
|
||||
## [defaults] section applies to all formats and sites but may be
|
||||
## overridden at several levels
|
||||
## overridden at several levels. Example:
|
||||
|
||||
## [defaults]
|
||||
## titlepage_entries: category,genre, status
|
||||
## [www.whofic.com]
|
||||
## # overrides defaults.
|
||||
## titlepage_entries: category,genre, status,dateUpdated,rating
|
||||
## [epub]
|
||||
## # overrides defaults & site section
|
||||
## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated
|
||||
## [www.whofic.com:epub]
|
||||
## # overrides defaults, site section & format section
|
||||
## titlepage_entries: category,genre, status,datePublished
|
||||
## [overrides]
|
||||
## # overrides all other sections
|
||||
## titlepage_entries: category
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. Uncomment by removing '#' in front of is_adult.
|
||||
@@ -127,6 +142,14 @@ extratags: FanFiction
|
||||
## Add this to genre if there's more than one category.
|
||||
#add_genre_when_multi_category: Crossover
|
||||
|
||||
## default_value_(entry) can be used to set the value for a metadata
|
||||
## entry when no value has been found on the site. For example, some
|
||||
## sites doesn't have a status metadatum. If uncommented, this will
|
||||
## use 'Unknown' for status when no status is found.
|
||||
#default_value_status:Unknown
|
||||
## Can also be used for other metadata values
|
||||
#default_value_category:FanFiction
|
||||
|
||||
## number of seconds to sleep between calls to the story site. May by
|
||||
## useful if pulling large numbers of stories or if the site is slow.
|
||||
#slow_down_sleep_time:0.5
|
||||
@@ -164,6 +187,11 @@ extratags: FanFiction
|
||||
## doesn't work on some devices either.)
|
||||
#replace_hr: false
|
||||
|
||||
## Some sites/authors/stories use br tags instead of p tags for
|
||||
## paragraphs. This feature uses some heuristics to find and replace
|
||||
## br paragraphs with p tags while preserving scene breaks.
|
||||
#replace_br_with_p: false
|
||||
|
||||
## If you have the Generate Cover plugin installed, you can use the
|
||||
## generate_cover_settings parameter to intelligently decide which GC
|
||||
## setting to run. There are three parts 1) a template of which
|
||||
@@ -249,6 +277,8 @@ sort_ships:false
|
||||
## you added calibre_author: keep_in_order_calibre_author:true
|
||||
#keep_in_order_author:true
|
||||
|
||||
## User-agent
|
||||
user_agent:FFDL/1.7
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
@@ -412,15 +442,16 @@ image_max_size: 580, 725
|
||||
## confused and displays it on every page after that under the text
|
||||
## for the rest of the chapter. I doubt adding a div around the img
|
||||
## will break any other readers, but in case it does, the fix can be
|
||||
## turned off.
|
||||
## turned off. This setting is not used if replace_br_with_p is
|
||||
## true--replace_br_with_p also fixes the problem.
|
||||
nook_img_fix:true
|
||||
|
||||
[mobi]
|
||||
## mobi TOC cannot be turned off right now.
|
||||
#include_tocpage: true
|
||||
|
||||
## Each site has a section that overrides [defaults] *and* the format
|
||||
## sections test1.com specifically is not a real story site. Instead,
|
||||
## Each site has a section that overrides [defaults].
|
||||
## test1.com specifically is not a real story site. Instead,
|
||||
## it is a fake site for testing configuration and output. It uses
|
||||
## URLs like: http://test1.com?sid=12345
|
||||
[test1.com]
|
||||
@@ -477,7 +508,8 @@ extratags: FanFiction,Testing,HTML
|
||||
#is_adult:true
|
||||
|
||||
## AO3 adapter defines a few extra metadata entries.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections
|
||||
## If there's ever more than 4 series, add series04,series04Url etc.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
fandoms_label:Fandoms
|
||||
freeformtags_label:Freeform Tags
|
||||
freefromtags_label:Freeform Tags
|
||||
@@ -493,7 +525,7 @@ bookmarks_label:Bookmarks
|
||||
include_in_freefromtags:freeformtags
|
||||
|
||||
## adds to titlepage_entries instead of replacing it.
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
@@ -727,6 +759,23 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[fictionpad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
extra_valid_entries:followers,comments,views,likes,dislikes
|
||||
#extra_titlepage_entries:followers,comments,views,likes,dislikes
|
||||
|
||||
followers_label:Followers
|
||||
comments_label:Comments
|
||||
views_label:Views
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
|
||||
[finestories.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -735,6 +784,24 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[storiesonline.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,codes
|
||||
#extra_titlepage_entries:size,universe,codes
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
codes_label:Codes
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -777,6 +844,10 @@ extracategories:Highlander
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:In Death
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/public/style_emoticons/.*
|
||||
|
||||
[ksarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Star Trek
|
||||
@@ -1084,6 +1155,7 @@ context_label:Context
|
||||
type_label:Type of Couple
|
||||
|
||||
[www.fanfiction.net]
|
||||
user_agent:
|
||||
## fanfiction.net's 'cover' images are really just tiny thumbnails.
|
||||
## Change this to false to use them anyway.
|
||||
never_make_cover: true
|
||||
@@ -1092,6 +1164,17 @@ never_make_cover: true
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,favs,follows
|
||||
|
||||
## ffnet uses 'Pairings', not 'Relationship', stating they don't have
|
||||
## to be romantic pairings.
|
||||
ships_label:Pairings
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't have time.
|
||||
## See http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
#dateCreated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
|
||||
[www.fanfiktion.de]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1111,7 +1194,12 @@ extracategories:Harry Potter
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## fictionalley.org doesn't have a status metadatum. If uncommented,
|
||||
## this will be used for status.
|
||||
#default_value_status:Unknown
|
||||
|
||||
[www.fictionpress.com]
|
||||
user_agent:
|
||||
## Clear FanFiction from defaults, fictionpress.com is original fiction.
|
||||
extratags:
|
||||
|
||||
@@ -1133,12 +1221,13 @@ extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:likes,dislikes,views,total_views,short_description
|
||||
extra_valid_entries:likes,dislikes,views,total_views,short_description,groups
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
views_label:Highest Single Chapter Views
|
||||
total_views_label:Total Views
|
||||
short_description_label:Short Summary
|
||||
groups_label:Groups
|
||||
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -1150,6 +1239,13 @@ short_description_label:Short Summary
|
||||
## when a password is required rather than prompting every time.
|
||||
#fail_on_password: false
|
||||
|
||||
## fimfiction.net stories allow chapters to be added out of order. So
|
||||
## the newest chapter may not be the last one. FFDL update doesn't
|
||||
## like that. If do_update_hook is uncommented and set true, the
|
||||
## adapter will discard all existing chapters from the newest one on
|
||||
## when updating to enforce accurate chapters.
|
||||
#do_update_hook:false
|
||||
|
||||
[www.harrypotterfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
## like. Uncomment options by removing the '#' in front of them.
|
||||
|
||||
[defaults]
|
||||
## [defaults] section applies to all formats and sites but may be
|
||||
## overridden at several levels. Example:
|
||||
|
||||
## [defaults]
|
||||
## titlepage_entries: category,genre, status
|
||||
## [www.whofic.com]
|
||||
## # overrides defaults.
|
||||
## titlepage_entries: category,genre, status,dateUpdated,rating
|
||||
## [epub]
|
||||
## # overrides defaults & site section
|
||||
## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated
|
||||
## [www.whofic.com:epub]
|
||||
## # overrides defaults, site section & format section
|
||||
## titlepage_entries: category,genre, status,datePublished
|
||||
## [overrides]
|
||||
## # overrides all other sections
|
||||
## titlepage_entries: category
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. Uncomment by removing '#' in front of is_adult.
|
||||
#is_adult:true
|
||||
|
||||
+11
-1
@@ -51,7 +51,17 @@
|
||||
by {{ fic.author }} ({{ fic.format }})
|
||||
{% endif %}
|
||||
{% if fic.failure %}
|
||||
<span id='error'>{{ fic.failure }}</span>
|
||||
<h3>fanfiction.net / fimfiction.net</h3>
|
||||
<p>
|
||||
FYI, fanfiction.net appears to be blocking access from Google
|
||||
App Engine, which prevents this web service. There's
|
||||
nothing I can do about it. At the time of writing, the
|
||||
latest CLI and calibre plugin versions worked.
|
||||
</p>
|
||||
<p>It appears that FimFiction.net is also blocking access from Google
|
||||
App Engine now.
|
||||
</p>
|
||||
<span id='error'>{{ fic.failure }}</span>
|
||||
{% endif %}
|
||||
{% if not fic.completed and not fic.failure %}
|
||||
<p>Not done yet. This page will periodically poll to see if your story has finished.</p>
|
||||
|
||||
Reference in New Issue
Block a user