mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-14 11:14:10 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
872f00087e | ||
|
|
5433633888 | ||
|
|
2bc30444c8 | ||
|
|
4a2df36443 | ||
|
|
5dd2959eb0 | ||
|
|
3d07257f7c | ||
|
|
b7d2fdba81 | ||
|
|
b2fbe6ea09 | ||
|
|
0415f3bd6c | ||
|
|
bad9a0ea36 | ||
|
|
d51bb8373f | ||
|
|
138b751519 | ||
|
|
53e9030a61 | ||
|
|
3ab60a9808 | ||
|
|
bdf5d45a3c | ||
|
|
f91aa5ba3f | ||
|
|
4980dd8027 | ||
|
|
ca6bc7f88e | ||
|
|
53ca251a49 | ||
|
|
dda8360044 | ||
|
|
f51b21cfeb | ||
|
|
e07bd0653b | ||
|
|
b16aee27a1 | ||
|
|
17a631dc79 | ||
|
|
808b44cf9b | ||
|
|
62db815781 | ||
|
|
d2aa6c1774 | ||
|
|
48e1bc7aa4 | ||
|
|
f6b1529c27 | ||
|
|
84b0070d2c | ||
|
|
37e80cb63b | ||
|
|
c82a157bad | ||
|
|
555fa7678f | ||
|
|
eeccde995f | ||
|
|
e82a21e303 | ||
|
|
64d147910d | ||
|
|
04e0bde07b | ||
|
|
0a07ebc8b2 | ||
|
|
c4c4ff152f | ||
|
|
bc82d35c92 | ||
|
|
28b9fa511f | ||
|
|
9cf75c4774 | ||
|
|
2e1bb8139a | ||
|
|
1c1b6a25c4 | ||
|
|
6569c87060 | ||
|
|
d26298042c | ||
|
|
90baa68695 | ||
|
|
2b289c6c93 | ||
|
|
edf4039115 | ||
|
|
819dc06d8f |
@@ -10,8 +10,10 @@ FFDL, AKA fanficdownloader) project previously hosted as a
|
||||
This program is available as a calibre plugin, a command-line
|
||||
interface, and a web service.
|
||||
|
||||
*Currently, the project is the middle of being moved from code.google
|
||||
to GitHub and being renamed. For now, continue to use
|
||||
FanFictionDownLoader(FFDL).*
|
||||
FanFicFare has now been launched. New versions, features and updates
|
||||
will all be in FanFicFare.
|
||||
|
||||
[code.google project]: https://code.google.com/p/fanficdownloader/
|
||||
There's additional info in the project [wiki] pages.
|
||||
|
||||
[code.google project]: http://google-opensource.blogspot.com/2015/03/farewell-to-google-code.html
|
||||
[wiki]: https://github.com/JimmXinu/FanFicFare/wiki
|
||||
|
||||
@@ -42,7 +42,7 @@ class FanFicFareBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (2, 2, 0)
|
||||
version = (2, 2, 5)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<hr />
|
||||
|
||||
<p>Plugin created by Jim Miller, borrowing heavily from Grant Drake's
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134856">Reading List</a>',
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=126727">Extract ISBN</a>' and
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134000">Count Pages</a>'
|
||||
plugins.</p>
|
||||
|
||||
<p>
|
||||
Calibre officially distributes plugins from the mobileread.com forum site.
|
||||
The official distro channel for this plugin is there: <a href="http://www.mobileread.com/forums/showthread.php?t=259221">FanFicFare</a>
|
||||
</p>
|
||||
|
||||
<p> I also monitor the
|
||||
<a href="http://groups.google.com/group/fanfic-downloader">general users
|
||||
group</a> for the downloader. That covers the web application and CLI, too.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The source for this plugin is available at it's
|
||||
<a href="https://github.com/JimmXinu/FanFicFare">project home</a>.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
See the <a href="https://github.com/JimmXinu/FanFicFare/wiki/Supportedsites">list of supported sites</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Read the <a href="https://github.com/JimmXinu/FanFicFare/wiki/FAQs">FAQs</a>.
|
||||
</p>
|
||||
@@ -1,28 +0,0 @@
|
||||
<hr />
|
||||
|
||||
<p>Plugin created by Jim Miller, borrowing heavily from Grant Drake's
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134856">Reading List</a>',
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=126727">Extract ISBN</a>' and
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134000">Count Pages</a>'
|
||||
plugins.</p>
|
||||
|
||||
<p>
|
||||
Calibre officially distributes plugins from the mobileread.com forum site.
|
||||
The official distro channel for this plugin is there: <a href="http://www.mobileread.com/forums/showthread.php?t=163261">FanFictionDownLoader</a>
|
||||
</p>
|
||||
|
||||
<p> I also monitor the
|
||||
<a href="http://groups.google.com/group/fanfic-downloader">general users
|
||||
group</a> for the downloader. That covers the web application and CLI, too.
|
||||
</p>
|
||||
|
||||
The source for this plugin is available at it's
|
||||
<a href="https://github.com/JimmXinu/FanFicFare">project home</a>.
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
See the <a href="https://github.com/JimmXinu/FanFicFare/wiki/Supportedsites">list of supported sites</a>.
|
||||
</p>
|
||||
<p>
|
||||
Read the <a href="https://github.com/JimmXinu/FanFicFare/wiki/FAQs">FAQs</a>.
|
||||
</p>
|
||||
+185
-58
@@ -17,12 +17,12 @@ try:
|
||||
from PyQt5.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QLabel, QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
QCheckBox, QPushButton, QTabWidget, QScrollArea,
|
||||
QDialogButtonBox, QGroupBox, Qt )
|
||||
QDialogButtonBox, QGroupBox, QButtonGroup, QRadioButton, Qt)
|
||||
except ImportError as e:
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QLabel, QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
QCheckBox, QPushButton, QTabWidget, QScrollArea,
|
||||
QDialogButtonBox, QGroupBox, Qt )
|
||||
QDialogButtonBox, QGroupBox, QButtonGroup, QRadioButton, Qt)
|
||||
try:
|
||||
from calibre.gui2 import QVariant
|
||||
del QVariant
|
||||
@@ -39,9 +39,15 @@ else:
|
||||
return [convert_qvariant(i) for i in x.toList()]
|
||||
return x.toPyObject()
|
||||
|
||||
from calibre.gui2.ui import get_gui
|
||||
from calibre.gui2 import dynamic, info_dialog, question_dialog
|
||||
from calibre.constants import numeric_version as calibre_version
|
||||
from calibre.gui2.ui import get_gui
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
|
||||
try:
|
||||
from calibre.ebooks.covers import generate_cover as cal_generate_cover
|
||||
HAS_CALGC=True
|
||||
except:
|
||||
HAS_CALGC=False
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
@@ -54,7 +60,6 @@ except NameError:
|
||||
# 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',
|
||||
@@ -70,7 +75,10 @@ no_trans = { 'pini':'personal.ini',
|
||||
'p':'password',
|
||||
}
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.prefs import prefs, PREFS_NAMESPACE
|
||||
from calibre_plugins.fanficfare_plugin.prefs \
|
||||
import (prefs, PREFS_NAMESPACE, updatecalcover_order, calcover_save_options,
|
||||
gencalcover_order, SAVE_YES, SAVE_NO)
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.dialogs \
|
||||
import (UPDATE, UPDATEALWAYS, collision_order, save_collisions, RejectListDialog,
|
||||
EditTextDialog, IniTextDialog, RejectUrlEntry)
|
||||
@@ -84,8 +92,6 @@ from calibre_plugins.fanficfare_plugin.common_utils \
|
||||
from calibre_plugins.fanficfare_plugin.fff_util \
|
||||
import (test_config)
|
||||
|
||||
from calibre.gui2.complete2 import EditWithComplete #MultiCompleteLineEdit
|
||||
|
||||
class RejectURLList:
|
||||
def __init__(self,prefs):
|
||||
self.prefs = prefs
|
||||
@@ -205,10 +211,8 @@ class ConfigWidget(QWidget):
|
||||
if 'Reading List' not in plugin_action.gui.iactions:
|
||||
self.readinglist_tab.setEnabled(False)
|
||||
|
||||
self.generatecover_tab = GenerateCoverTab(self, plugin_action)
|
||||
tab_widget.addTab(self.generatecover_tab, 'Generate Cover')
|
||||
if 'Generate Cover' not in plugin_action.gui.iactions:
|
||||
self.generatecover_tab.setEnabled(False)
|
||||
self.calibrecover_tab = CalibreCoverTab(self, plugin_action)
|
||||
tab_widget.addTab(self.calibrecover_tab, _('Calibre Cover'))
|
||||
|
||||
self.countpages_tab = CountPagesTab(self, plugin_action)
|
||||
tab_widget.addTab(self.countpages_tab, 'Count Pages')
|
||||
@@ -234,7 +238,6 @@ class ConfigWidget(QWidget):
|
||||
prefs['fileform'] = unicode(self.basic_tab.fileform.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()
|
||||
@@ -247,7 +250,6 @@ class ConfigWidget(QWidget):
|
||||
prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked()
|
||||
prefs['deleteotherforms'] = self.basic_tab.deleteotherforms.isChecked()
|
||||
prefs['adddialogstaysontop'] = self.basic_tab.adddialogstaysontop.isChecked()
|
||||
prefs['includeimages'] = self.basic_tab.includeimages.isChecked()
|
||||
prefs['lookforurlinhtml'] = self.basic_tab.lookforurlinhtml.isChecked()
|
||||
prefs['checkforseriesurlid'] = self.basic_tab.checkforseriesurlid.isChecked()
|
||||
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
|
||||
@@ -273,17 +275,23 @@ class ConfigWidget(QWidget):
|
||||
# if they've removed everything, reset to default.
|
||||
prefs['personal.ini'] = get_resources('plugin-example.ini')
|
||||
|
||||
# Generate Covers tab
|
||||
prefs['gcnewonly'] = self.generatecover_tab.gcnewonly.isChecked()
|
||||
# Covers tab
|
||||
prefs['updatecalcover'] = calcover_save_options[unicode(self.calibrecover_tab.updatecalcover.currentText())]
|
||||
# for backward compatibility:
|
||||
prefs['updatecover'] = prefs['updatecalcover'] == SAVE_YES
|
||||
prefs['gencalcover'] = calcover_save_options[unicode(self.calibrecover_tab.gencalcover.currentText())]
|
||||
prefs['calibre_gen_cover'] = self.calibrecover_tab.calibre_gen_cover.isChecked()
|
||||
prefs['plugin_gen_cover'] = self.calibrecover_tab.plugin_gen_cover.isChecked()
|
||||
prefs['gcnewonly'] = self.calibrecover_tab.gcnewonly.isChecked()
|
||||
gc_site_settings = {}
|
||||
for (site,combo) in self.generatecover_tab.gc_dropdowns.iteritems():
|
||||
for (site,combo) in self.calibrecover_tab.gc_dropdowns.iteritems():
|
||||
val = unicode(convert_qvariant(combo.itemData(combo.currentIndex())))
|
||||
if val != 'none':
|
||||
gc_site_settings[site] = val
|
||||
#print("gc_site_settings[%s]:%s"%(site,gc_site_settings[site]))
|
||||
prefs['gc_site_settings'] = gc_site_settings
|
||||
prefs['allow_gc_from_ini'] = self.generatecover_tab.allow_gc_from_ini.isChecked()
|
||||
prefs['gc_polish_cover'] = self.generatecover_tab.gc_polish_cover.isChecked()
|
||||
prefs['allow_gc_from_ini'] = self.calibrecover_tab.allow_gc_from_ini.isChecked()
|
||||
prefs['gc_polish_cover'] = self.calibrecover_tab.gc_polish_cover.isChecked()
|
||||
|
||||
# Count Pages tab
|
||||
countpagesstats = []
|
||||
@@ -396,21 +404,17 @@ class BasicTab(QWidget):
|
||||
horz.addWidget(self.collision)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.updatemeta = QCheckBox(_('Default Update Calibre &Metadata?'),self)
|
||||
self.updatemeta.setToolTip(_("On each download, FanFicFare 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)
|
||||
horz.addWidget(self.updatemeta)
|
||||
|
||||
self.updateepubcover = QCheckBox(_('Default Update EPUB Cover when Updating EPUB?'),self)
|
||||
self.updateepubcover.setToolTip(_("On each download, FanFicFare 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.setChecked(prefs['smarten_punctuation'])
|
||||
if calibre_version >= (0, 9, 39):
|
||||
self.l.addWidget(self.smarten_punctuation)
|
||||
horz.addWidget(self.updateepubcover)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
cali_gb = groupbox = QGroupBox(_("Updating Calibre Options"))
|
||||
self.l = QVBoxLayout()
|
||||
@@ -421,11 +425,6 @@ class BasicTab(QWidget):
|
||||
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.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.\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'])
|
||||
@@ -466,16 +465,16 @@ class BasicTab(QWidget):
|
||||
self.showmarked.setChecked(prefs['showmarked'])
|
||||
self.l.addWidget(self.showmarked)
|
||||
|
||||
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'])
|
||||
self.l.addWidget(self.smarten_punctuation)
|
||||
|
||||
self.autoconvert = QCheckBox(_("Automatically Convert new/update books?"),self)
|
||||
self.autoconvert.setToolTip(_("Automatically call calibre's Convert for new/update books.\nConverts to the current output format as chosen in calibre's\nPreferences->Behavior settings."))
|
||||
self.autoconvert.setChecked(prefs['autoconvert'])
|
||||
self.l.addWidget(self.autoconvert)
|
||||
|
||||
self.show_est_time = QCheckBox(_("Show estimated time left?"),self)
|
||||
self.show_est_time.setToolTip(_("When a Progress Bar is shown, show a rough estimate of the time left."))
|
||||
self.show_est_time.setChecked(prefs['show_est_time'])
|
||||
self.l.addWidget(self.show_est_time)
|
||||
|
||||
gui_gb = groupbox = QGroupBox(_("GUI Options"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
@@ -495,16 +494,15 @@ class BasicTab(QWidget):
|
||||
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
|
||||
self.l.addWidget(self.adddialogstaysontop)
|
||||
|
||||
self.show_est_time = QCheckBox(_("Show estimated time left?"),self)
|
||||
self.show_est_time.setToolTip(_("When a Progress Bar is shown, show a rough estimate of the time left."))
|
||||
self.show_est_time.setChecked(prefs['show_est_time'])
|
||||
self.l.addWidget(self.show_est_time)
|
||||
|
||||
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:%(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 \nit appears on the FanFicFare title page(not cover)."))
|
||||
self.injectseries.setChecked(prefs['injectseries'])
|
||||
@@ -708,6 +706,7 @@ class ReadingListTab(QWidget):
|
||||
self.send_lists_box.update_items_cache(reading_lists)
|
||||
self.send_lists_box.setText(prefs['send_lists'])
|
||||
horz.addWidget(self.send_lists_box)
|
||||
self.send_lists_box.setCursorPosition(0)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.addtoreadlists = QCheckBox(_('Add new/updated stories to "To Read" Reading List(s).'),self)
|
||||
@@ -724,6 +723,7 @@ class ReadingListTab(QWidget):
|
||||
self.read_lists_box.update_items_cache(reading_lists)
|
||||
self.read_lists_box.setText(prefs['read_lists'])
|
||||
horz.addWidget(self.read_lists_box)
|
||||
self.read_lists_box.setCursorPosition(0)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.addtolistsonread = QCheckBox(_('Add stories back to "Send to Device" Reading List(s) when marked "Read".'),self)
|
||||
@@ -733,14 +733,19 @@ class ReadingListTab(QWidget):
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
class GenerateCoverTab(QWidget):
|
||||
class CalibreCoverTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.gencov_elements=[] ## used to disable/enable when gen
|
||||
## cover is off/on. This is more
|
||||
## about being a visual que than real
|
||||
## necessary function.
|
||||
|
||||
topl = self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
try:
|
||||
@@ -748,8 +753,116 @@ class GenerateCoverTab(QWidget):
|
||||
gc_settings = gc_plugin.get_saved_setting_names()
|
||||
except KeyError:
|
||||
gc_settings= []
|
||||
|
||||
|
||||
label = QLabel(_('The %(gc)s plugin can create cover images for books using various metadata and configurations. If you have GC installed, FanFicFare can run GC on new downloads and metadata updates. Pick a GC setting by site or Default.')%no_trans)
|
||||
label = QLabel(_("The Calibre cover image for a downloaded book can come"
|
||||
" from the story site(if EPUB and images are enabled), or"
|
||||
" from either Calibre's built-in random cover generator or"
|
||||
" the %(gc)s plugin.")%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
tooltip = _("Update Calibre book cover image from EPUB when Calibre metadata is updated.\n"
|
||||
"Doesn't go looking for new images on 'Update Calibre Metadata Only'.\n"
|
||||
"Cover in EPUB could be from site or previously injected into the EPUB.\n"
|
||||
"This comes before Generate Cover so %(gc)s(Plugin) use the image if configured to.")%no_trans
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('Update Calibre Cover (from EPUB):'))
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
self.updatecalcover = QComboBox(self)
|
||||
for i in updatecalcover_order:
|
||||
self.updatecalcover.addItem(i)
|
||||
# back compat. If has own value, use.
|
||||
if prefs['updatecalcover']:
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(calcover_save_options[prefs['updatecalcover']]))
|
||||
elif prefs['updatecover']: # doesn't have own val, set YES if old value set.
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(calcover_save_options[SAVE_YES]))
|
||||
else: # doesn't have own value, old value not set, NO.
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(calcover_save_options[SAVE_NO]))
|
||||
self.updatecalcover.setToolTip(tooltip)
|
||||
label.setBuddy(self.updatecalcover)
|
||||
horz.addWidget(self.updatecalcover)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
tooltip = _("Generate a Calibre book cover image when Calibre metadata is updated.<br />"
|
||||
"Defaults to 'Yes, Always' for backward compatibility and because %(gc)s(Plugin)"
|
||||
" will only run if configured for Default or site.")%no_trans
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('Generate Calibre Cover:'))
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
self.gencalcover = QComboBox(self)
|
||||
for i in gencalcover_order:
|
||||
self.gencalcover.addItem(i)
|
||||
# back compat. If has own value, use.
|
||||
# if prefs['gencalcover']:
|
||||
self.gencalcover.setCurrentIndex(self.gencalcover.findText(calcover_save_options[prefs['gencalcover']]))
|
||||
# elif prefs['gencover']: # doesn't have own val, set YES if old value set.
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(calcover_save_options[SAVE_YES]))
|
||||
# else: # doesn't have own value, old value not set, NO.
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(calcover_save_options[SAVE_NO]))
|
||||
|
||||
self.gencalcover.setToolTip(tooltip)
|
||||
label.setBuddy(self.gencalcover)
|
||||
horz.addWidget(self.gencalcover)
|
||||
self.l.addLayout(horz)
|
||||
self.gencalcover.currentIndexChanged.connect(self.endisable_elements)
|
||||
|
||||
# can't be local or it's destroyed when __init__ is done and
|
||||
# connected things don't fire.
|
||||
self.gencov_rdgrp = QButtonGroup()
|
||||
self.gencov_gb = QGroupBox()
|
||||
horz = QHBoxLayout()
|
||||
self.gencov_gb.setLayout(horz)
|
||||
|
||||
self.plugin_gen_cover = QRadioButton(_('Plugin %(gc)s')%no_trans,self)
|
||||
self.plugin_gen_cover.setToolTip(_("Use plugin to create covers. Additional settings are below."))
|
||||
self.gencov_rdgrp.addButton(self.plugin_gen_cover)
|
||||
# always, new only, when no cover from site, inject yes/no...
|
||||
self.plugin_gen_cover.setChecked(prefs['plugin_gen_cover'])
|
||||
horz.addWidget(self.plugin_gen_cover)
|
||||
self.gencov_elements.append(self.plugin_gen_cover)
|
||||
|
||||
self.calibre_gen_cover = QRadioButton(_('Calibre Generate Cover'),self)
|
||||
self.calibre_gen_cover.setToolTip(_("Call Calibre's Edit Metadata Generate cover"
|
||||
" feature to create a random cover each time"
|
||||
" a story is downloaded or updated.<br />"
|
||||
"Right click or long click the 'Generate cover'"
|
||||
" button in Calibre's Edit Metadata to customize."))
|
||||
self.gencov_rdgrp.addButton(self.calibre_gen_cover)
|
||||
# always, new only, when no cover from site, inject yes/no...
|
||||
self.calibre_gen_cover.setChecked(prefs['calibre_gen_cover'])
|
||||
horz.addWidget(self.calibre_gen_cover)
|
||||
self.gencov_elements.append(self.calibre_gen_cover)
|
||||
|
||||
#self.l.addLayout(horz)
|
||||
self.l.addWidget(self.gencov_gb)
|
||||
|
||||
self.gcnewonly = QCheckBox(_("Generate Covers Only for New Books")%no_trans,self)
|
||||
self.gcnewonly.setToolTip(_("Default is to generate a cover any time the calibre metadata is"
|
||||
" updated.<br \>Used for both Calibre and Plugin generated covers."))
|
||||
self.gcnewonly.setChecked(prefs['gcnewonly'])
|
||||
self.l.addWidget(self.gcnewonly)
|
||||
self.gencov_elements.append(self.gcnewonly)
|
||||
|
||||
self.gc_polish_cover = QCheckBox(_("Inject/update the cover inside EPUB"),self)
|
||||
self.gc_polish_cover.setToolTip(_("Calibre's Polish feature will be used to inject or update the generated"
|
||||
" cover into the EPUB ebook file.<br \>Used for both Calibre and Plugin generated covers."))
|
||||
self.gc_polish_cover.setChecked(prefs['gc_polish_cover'])
|
||||
self.l.addWidget(self.gc_polish_cover)
|
||||
self.gencov_elements.append(self.gc_polish_cover)
|
||||
|
||||
self.gcp_gb = QGroupBox(_("%(gc)s(Plugin) Settings")%no_trans)
|
||||
topl.addWidget(self.gcp_gb)
|
||||
self.l = QVBoxLayout()
|
||||
self.gcp_gb.setLayout(self.l)
|
||||
self.gencov_elements.append(self.gcp_gb)
|
||||
|
||||
self.gencov_rdgrp.buttonClicked.connect(self.endisable_elements)
|
||||
|
||||
label = QLabel(_('The %(gc)s plugin can create cover images for books using various metadata (including existing cover image). If you have %(gc)s installed, FanFicFare can run %(gc)s on new downloads and metadata updates. Pick a %(gc)s setting by site and/or one to use by Default.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
@@ -772,7 +885,7 @@ class GenerateCoverTab(QWidget):
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(site)
|
||||
if site == _("Default"):
|
||||
s = _("On Metadata update, run %(gc)s with this setting, if not selected for specific site.")%no_trans
|
||||
s = _("On Metadata update, run %(gc)s with this setting, if there isn't a more specific setting below.")%no_trans
|
||||
else:
|
||||
no_trans['site']=site # not ideal, but, meh.
|
||||
s = _("On Metadata update, run %(gc)s with this setting for %(site)s stories.")%no_trans
|
||||
@@ -798,20 +911,34 @@ class GenerateCoverTab(QWidget):
|
||||
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
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 %(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.setToolTip(_("The %(pini)s parameter %(gcset)s allows you to choose a %(gc)s 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)
|
||||
|
||||
self.gc_polish_cover = QCheckBox(_("Use calibre's Polish feature to inject/update the cover"),self)
|
||||
self.gc_polish_cover.setToolTip(_("Calibre's Polish feature will be used to inject or update the generated cover into the ebook, EPUB only."))
|
||||
self.gc_polish_cover.setChecked(prefs['gc_polish_cover'])
|
||||
self.l.addWidget(self.gc_polish_cover)
|
||||
|
||||
# keep at end.
|
||||
self.endisable_elements()
|
||||
|
||||
def endisable_elements(self,button=None):
|
||||
"Clearing house function for setting elements of Calibre"
|
||||
"Cover tab enabled/disabled depending on all factors."
|
||||
|
||||
## First, cover gen on/off
|
||||
for e in self.gencov_elements:
|
||||
e.setEnabled(calcover_save_options[unicode(self.gencalcover.currentText())] != SAVE_NO)
|
||||
|
||||
# next, disable plugin settings when using calibre gen cov.
|
||||
if not self.plugin_gen_cover.isChecked():
|
||||
self.gcp_gb.setEnabled(False)
|
||||
|
||||
# disable (but not enable) unsupported options.
|
||||
if not HAS_CALGC:
|
||||
self.calibre_gen_cover.setEnabled(False)
|
||||
if not 'Generate Cover' in self.plugin_action.gui.iactions:
|
||||
self.plugin_gen_cover.setEnabled(False)
|
||||
self.gcp_gb.setEnabled(False)
|
||||
|
||||
|
||||
class CountPagesTab(QWidget):
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ gpstyle='QGroupBox {border:0; padding-top:10px; padding-bottom:0px; margin-botto
|
||||
|
||||
class RejectUrlEntry:
|
||||
|
||||
matchpat=re.compile(r"^(?P<url>[^,]+)(,(?P<fullnote>(((?P<title>.+) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
|
||||
matchpat=re.compile(r"^(?P<url>[^,]+?)(,(?P<fullnote>(((?P<title>.+?) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
|
||||
|
||||
def __init__(self,url_or_line,note=None,title=None,auth=None,
|
||||
addreasontext=None,fromline=False,book_id=None):
|
||||
@@ -398,6 +398,9 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
if not self.merge:
|
||||
self.fileform.setCurrentIndex(self.fileform.findText(self.prefs['fileform']))
|
||||
else:
|
||||
# always epub on self.merge (anthology)
|
||||
self.fileform.setCurrentIndex(self.fileform.findText('epub'))
|
||||
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
@@ -994,6 +997,7 @@ class RejectListTableWidget(QTableWidget):
|
||||
note_cell.set_separator(None)
|
||||
note_cell.setToolTip(_('Select or Edit Reject Note.'))
|
||||
self.setCellWidget(row, 3, note_cell)
|
||||
note_cell.setCursorPosition(0)
|
||||
|
||||
def remove_selected_rows(self):
|
||||
self.setFocus()
|
||||
@@ -1069,6 +1073,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
label.setToolTip(_("This will be added to whatever note you've set for each URL above."))
|
||||
horz.addWidget(label)
|
||||
horz.addWidget(self.reason_edit)
|
||||
self.reason_edit.setCursorPosition(0)
|
||||
horz.insertStretch(-1)
|
||||
layout.addLayout(horz)
|
||||
|
||||
@@ -1168,6 +1173,7 @@ class EditTextDialog(SizePersistedDialog):
|
||||
self.l.addLayout(horz)
|
||||
else:
|
||||
self.l.addWidget(self.reason_edit)
|
||||
self.reason_edit.setCursorPosition(0)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
|
||||
+107
-76
@@ -48,6 +48,13 @@ try:
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
try:
|
||||
# should be present from cal2.3.0 on.
|
||||
from calibre.ebooks.covers import generate_cover as cal_generate_cover
|
||||
HAS_CALGC=True
|
||||
except:
|
||||
HAS_CALGC=False
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.common_utils import (set_plugin_icon_resources, get_icon,
|
||||
create_menu_action_unique, get_library_uuid)
|
||||
|
||||
@@ -57,7 +64,8 @@ from calibre_plugins.fanficfare_plugin.fanficfare.geturls import get_urls_from_p
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.fff_util import (get_fff_adapter, get_fff_config, get_fff_personalini)
|
||||
from calibre_plugins.fanficfare_plugin.config import (permitted_values, rejecturllist)
|
||||
from calibre_plugins.fanficfare_plugin.prefs import prefs
|
||||
from calibre_plugins.fanficfare_plugin.prefs import (prefs, SAVE_YES, SAVE_NO,
|
||||
SAVE_YES_IF_IMG, SAVE_YES_UNLESS_IMG)
|
||||
from calibre_plugins.fanficfare_plugin.dialogs import (
|
||||
AddNewDialog, UpdateExistingDialog,
|
||||
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog, RejectListDialog, EmailPassDialog,
|
||||
@@ -350,7 +358,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# get_resources will return a dictionary mapping names to bytes. Names that
|
||||
# are not found in the zip file will not be in the returned dictionary.
|
||||
|
||||
text = get_resources('about.txt')
|
||||
text = get_resources('about.html')
|
||||
AboutDialog(self.gui,self.qaction.icon(),self.version + text).exec_()
|
||||
|
||||
def create_menu_item_ex(self, parent_menu, menu_text, image=None, tooltip=None,
|
||||
@@ -1609,15 +1617,6 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if not oldmi.languages:
|
||||
mi.languages=['en']
|
||||
|
||||
if options['fileform'] == 'epub' and prefs['updatecover']:
|
||||
existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True)
|
||||
epubmi = get_metadata(existingepub,'EPUB')
|
||||
if epubmi.cover_data[1] is not None:
|
||||
try:
|
||||
db.set_cover(book_id, epubmi.cover_data[1])
|
||||
except:
|
||||
logger.info("Failed to set_cover, skipping")
|
||||
|
||||
# implement 'newonly' flags here by setting to the current
|
||||
# value again.
|
||||
if not book['added']:
|
||||
@@ -1757,73 +1756,105 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
db.commit()
|
||||
|
||||
if 'Generate Cover' in self.gui.iactions and (book['added'] or not prefs['gcnewonly']):
|
||||
|
||||
#logger.debug("Do Generate Cover added:%s gcnewonly:%s"%(book['added'],prefs['gcnewonly']))
|
||||
|
||||
# force a refresh if generating cover so complex composite
|
||||
# custom columns are current and correct
|
||||
db.refresh_ids([book_id])
|
||||
|
||||
gc_plugin = self.gui.iactions['Generate Cover']
|
||||
setting_name = None
|
||||
if prefs['allow_gc_from_ini']:
|
||||
if not configuration: # might already have it from allow_custcol_from_ini
|
||||
configuration = get_fff_config(book['url'],options['fileform'])
|
||||
|
||||
# template => regexp to match => GC Setting to use.
|
||||
# generate_cover_settings:
|
||||
# ${category} => Buffy:? the Vampire Slayer => Buffy
|
||||
# for line in configuration.getConfig('generate_cover_settings').splitlines():
|
||||
# if "=>" in line:
|
||||
# (template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
for (template,regexp,setting) in configuration.get_generate_cover_settings():
|
||||
value = Template(template).safe_substitute(book['all_metadata']).encode('utf8')
|
||||
# print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
# updating calibre cover from book.
|
||||
if options['fileform'] == 'epub' and (
|
||||
(prefs['updatecover'] and not prefs['updatecalcover']) ## backward compat
|
||||
or prefs['updatecalcover'] == SAVE_YES ## yes, always
|
||||
or (prefs['updatecalcover'] == SAVE_YES_IF_IMG ## yes, if image.
|
||||
and book['all_metadata']['cover_image'] )): # in ('specific','first','default','old')
|
||||
existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True)
|
||||
epubmi = get_metadata(existingepub,'EPUB')
|
||||
if epubmi.cover_data[1] is not None:
|
||||
try:
|
||||
db.set_cover(book_id, epubmi.cover_data[1])
|
||||
except:
|
||||
logger.info("Failed to set_cover, skipping")
|
||||
|
||||
# First, should cover generation happen at all?
|
||||
if (book['added'] or not prefs['gcnewonly']) and ( # skip if not new book and gcnewonly is True
|
||||
prefs['gencalcover'] == SAVE_YES ## yes, always
|
||||
or (prefs['gencalcover'] == SAVE_YES_UNLESS_IMG ## yes, unless image.
|
||||
and book['all_metadata']['cover_image'] not in ('specific','first','default')) ):
|
||||
|
||||
cover_generated = False # flag for polish below.
|
||||
# Yes, should do gencov. Which?
|
||||
if prefs['calibre_gen_cover'] and HAS_CALGC:
|
||||
# calibre's builtin, if available.
|
||||
cdata = cal_generate_cover(mi)
|
||||
db.set_cover(book_id, cdata)
|
||||
cover_generated = True
|
||||
elif prefs['plugin_gen_cover'] and 'Generate Cover' in self.gui.iactions:
|
||||
# plugin, if available.
|
||||
|
||||
#logger.debug("Do Generate Cover added:%s gcnewonly:%s"%(book['added'],prefs['gcnewonly']))
|
||||
|
||||
# force a refresh if generating cover so complex composite
|
||||
# custom columns are current and correct
|
||||
db.refresh_ids([book_id])
|
||||
|
||||
gc_plugin = self.gui.iactions['Generate Cover']
|
||||
setting_name = None
|
||||
if prefs['allow_gc_from_ini']:
|
||||
if not configuration: # might already have it from allow_custcol_from_ini
|
||||
configuration = get_fff_config(book['url'],options['fileform'])
|
||||
|
||||
# template => regexp to match => GC Setting to use.
|
||||
# generate_cover_settings:
|
||||
# ${category} => Buffy:? the Vampire Slayer => Buffy
|
||||
# for line in configuration.getConfig('generate_cover_settings').splitlines():
|
||||
# if "=>" in line:
|
||||
# (template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
for (template,regexp,setting) in configuration.get_generate_cover_settings():
|
||||
value = Template(template).safe_substitute(book['all_metadata']).encode('utf8')
|
||||
print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Generate Cover Setting from generate_cover_settings(%s)"%setting_name)
|
||||
if setting_name not in gc_plugin.get_saved_setting_names():
|
||||
logger.info("GC Name %s not found, discarding! (check personal.ini for typos)"%setting_name)
|
||||
setting_name = None
|
||||
|
||||
if not setting_name and book['all_metadata']['site'] in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings'][book['all_metadata']['site']]
|
||||
logger.debug("Generate Cover Setting from site(%s)"%setting_name)
|
||||
|
||||
if not setting_name and 'Default' in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings']['Default']
|
||||
logger.debug("Generate Cover Setting from Default(%s)"%setting_name)
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Generate Cover Setting from generate_cover_settings(%s)"%setting_name)
|
||||
if setting_name not in gc_plugin.get_saved_setting_names():
|
||||
logger.info("GC Name %s not found, discarding! (check personal.ini for typos)"%setting_name)
|
||||
setting_name = None
|
||||
|
||||
if not setting_name and book['all_metadata']['site'] in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings'][book['all_metadata']['site']]
|
||||
logger.debug("Generate Cover Setting from site(%s)"%setting_name)
|
||||
|
||||
if not setting_name and 'Default' in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings']['Default']
|
||||
logger.debug("Generate Cover Setting from Default(%s)"%setting_name)
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Running Generate Cover with settings %s."%setting_name)
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
|
||||
if prefs['gc_polish_cover'] and \
|
||||
options['fileform'] == "epub":
|
||||
# set cover inside epub from calibre's polish feature
|
||||
from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS
|
||||
from calibre.utils.logging import Log
|
||||
from collections import namedtuple
|
||||
|
||||
# Couldn't find a better way to get the cover path.
|
||||
cover_path = os.path.join(db.library_path, db.path(book_id, index_is_id=True), 'cover.jpg')
|
||||
data = {'cover':cover_path}
|
||||
#print("cover_path:%s"%cover_path)
|
||||
opts = ALL_OPTS.copy()
|
||||
opts.update(data)
|
||||
O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys()))
|
||||
opts = O(**opts)
|
||||
|
||||
log = Log(level=Log.DEBUG)
|
||||
outfile = db.format_abspath(book_id, formmapping[options['fileform']], index_is_id=True)
|
||||
#print("polish cover outfile:%s"%outfile)
|
||||
polish({outfile:outfile}, opts, log, logger.info)
|
||||
|
||||
logger.debug("Running Generate Cover with settings %s."%setting_name)
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
cover_generated = True
|
||||
|
||||
if cover_generated and prefs['gc_polish_cover'] and \
|
||||
options['fileform'] == "epub":
|
||||
# set cover inside epub from calibre's polish feature
|
||||
from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS
|
||||
from calibre.utils.logging import Log
|
||||
from collections import namedtuple
|
||||
|
||||
# Couldn't find a better way to get the cover path.
|
||||
cover_path = os.path.join(db.library_path,
|
||||
db.path(book_id, index_is_id=True),
|
||||
'cover.jpg')
|
||||
data = {'cover':cover_path}
|
||||
#print("cover_path:%s"%cover_path)
|
||||
opts = ALL_OPTS.copy()
|
||||
opts.update(data)
|
||||
O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys()))
|
||||
opts = O(**opts)
|
||||
|
||||
log = Log(level=Log.DEBUG)
|
||||
outfile = db.format_abspath(book_id,
|
||||
formmapping[options['fileform']],
|
||||
index_is_id=True)
|
||||
#print("polish cover outfile:%s"%outfile)
|
||||
polish({outfile:outfile}, opts, log, logger.info)
|
||||
|
||||
def get_clean_reading_lists(self,lists):
|
||||
if lists == None or lists.strip() == "" :
|
||||
|
||||
@@ -18,15 +18,7 @@ from calibre_plugins.fanficfare_plugin.fanficfare.configurable import Configurat
|
||||
from calibre_plugins.fanficfare_plugin.prefs import prefs
|
||||
|
||||
def get_fff_personalini():
|
||||
if prefs['includeimages']:
|
||||
# this is a cheat to make it easier for users.
|
||||
return '''[epub]
|
||||
include_images:true
|
||||
keep_summary_html:true
|
||||
make_firstimage_cover:true
|
||||
''' + prefs['personal.ini']
|
||||
else:
|
||||
return prefs['personal.ini']
|
||||
return prefs['personal.ini']
|
||||
|
||||
def get_fff_config(url,fileform="epub",personalini=None):
|
||||
if not personalini:
|
||||
|
||||
@@ -154,6 +154,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
if options['collision'] in (CALIBREONLY):
|
||||
logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
|
||||
book['comment'] = 'Metadata collected.'
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
|
||||
## checks were done earlier, it's new or not dup or newer--just write it.
|
||||
elif options['collision'] in (ADDNEW, SKIP, OVERWRITE, OVERWRITEALWAYS) or \
|
||||
@@ -178,6 +179,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
logger.info("write to %s"%outfile)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
|
||||
## checks were done earlier, just update it.
|
||||
elif 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS):
|
||||
@@ -197,6 +199,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
if options['collision'] == UPDATE:
|
||||
if chaptercount == urlchaptercount:
|
||||
book['comment']=_("Already contains %d chapters. Reuse as is.")%chaptercount
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
book['outfile'] = book['epub_for_update'] # for anthology merge ops.
|
||||
return book
|
||||
|
||||
@@ -215,6 +218,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
|
||||
book['comment'] = _('Update %s completed, added %s chapters for %s total.')%\
|
||||
(options['fileform'],(urlchaptercount-chaptercount),urlchaptercount)
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
|
||||
if options['smarten_punctuation'] and options['fileform'] == "epub" \
|
||||
and calibre_version >= (0, 9, 39):
|
||||
|
||||
@@ -85,9 +85,9 @@ authorId_label:Author ID
|
||||
## show up in Calibre as tags. Also carried into mobi when converted.
|
||||
extratags_label:Extra Tags
|
||||
## The version of fanficdownloader
|
||||
version_label:FFDL Version
|
||||
version_label:Downloader Version
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't have time.
|
||||
## Date formats used by FanFicFare. 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
|
||||
@@ -172,7 +172,7 @@ connect_timeout:60.0
|
||||
## Note that if metakey == conditionalkey the conditional is ignored.
|
||||
## You can use \s in the replacement to add explicit spaces. (The config parser
|
||||
## tends to discard trailing spaces.)
|
||||
## replace_metadata <entry>_LIST options: FFDL replace_metadata lines
|
||||
## replace_metadata <entry>_LIST options: FanFicFare replace_metadata lines
|
||||
## operate on individual list items for list entries. But if you
|
||||
## want to do a replacement on the joined string for the whole list,
|
||||
## you can by using <entry>_LIST. Example, if you added
|
||||
@@ -232,12 +232,21 @@ connect_timeout:60.0
|
||||
## Make sure to keep at least one space at the start of each line and
|
||||
## to escape % to %%, if used.
|
||||
## template => regexp to match => GC Setting to use.
|
||||
## To use this, make sure you go to the Generate Cover tab in FFDL
|
||||
## To use this, make sure you go to the Generate Cover tab in FanFicFare
|
||||
## config and check 'Allow generate_cover_settings from personal.ini
|
||||
## to override'
|
||||
## You can check for an existing cover image in the metadata
|
||||
## cover_image. cover_image can be:
|
||||
## specific -- The site has an image that is specifically this story's cover
|
||||
## first -- The first image in the story or story desc is used as cover
|
||||
## default -- A default_cover_image was used
|
||||
## old -- There was already a cover image in the epub.
|
||||
## This is useful because GC plugin can use the existing image.
|
||||
#generate_cover_settings:
|
||||
# ${category} => Buffy:? [tT]he Vampire Slayer => BuffyCover
|
||||
# ${category} => Star Trek => StarTrekCover
|
||||
# ${cover_image} => => CompleteCover
|
||||
# ${cover_image} => (specific|first|default) => CoverWithOrigImage
|
||||
|
||||
## If set false, the summary will have all html stripped.
|
||||
## Both this and include_images must be true to get images in the
|
||||
@@ -306,20 +315,20 @@ anthology_tags:Anthology
|
||||
## ships_LIST to change the / back to something else if you want.
|
||||
sort_ships:false
|
||||
|
||||
## join_string_<entry> options -- FFDL list entries are comma
|
||||
## join_string_<entry> options -- FanFicFare list entries are comma
|
||||
## separated by default. You can use this to change that. For example,
|
||||
## if you want authors separated with ' & ' instead, use
|
||||
## join_string_calibre_author:\s&\s. (\s == space)
|
||||
#join_string_author:,\s
|
||||
|
||||
## keep_in_order_<entry> options: FFDL sorts list entries by default
|
||||
## keep_in_order_<entry> options: FanFicFare sorts list entries by default
|
||||
## (except for author/authorUrl/authorId). But if you want to use an
|
||||
## extra entry derived from author, it ends up sorted. For example, if
|
||||
## you added calibre_author: keep_in_order_calibre_author:true
|
||||
#keep_in_order_author:true
|
||||
|
||||
## User-agent
|
||||
user_agent:FFDL/2.0
|
||||
user_agent:FFF/2.X
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
@@ -330,7 +339,7 @@ user_agent:FFDL/2.0
|
||||
## include_images is *not* available in the web service in any format.
|
||||
#include_images:false
|
||||
|
||||
## This switch prevents FFDL from doing any processing on the images.
|
||||
## This switch prevents FanFicFare from doing any processing on the images.
|
||||
## Usually they would be converted to jpg, resized and optionally made
|
||||
## grayscale.
|
||||
no_image_processing: true
|
||||
@@ -442,12 +451,12 @@ output_css:
|
||||
## include images from img tags in the body and summary of
|
||||
## stories. Images will be converted to jpg for size if possible.
|
||||
## include_images is *only* available in epub and html output format.
|
||||
#include_images:false
|
||||
include_images:true
|
||||
|
||||
## If set, the first image found will be made the cover image. If
|
||||
## keep_summary_html is true, any images in summary will be before any
|
||||
## in chapters.
|
||||
#make_firstimage_cover: false
|
||||
make_firstimage_cover: true
|
||||
|
||||
## If set, the epub will never have a cover, even include_images is on
|
||||
## and the site has specific cover images.
|
||||
@@ -479,6 +488,22 @@ image_max_size: 580, 725
|
||||
## space.
|
||||
#grayscale_images: false
|
||||
|
||||
## jpg or png
|
||||
## -- jpg produces smaller images, and may be supported by more
|
||||
## readers, but it's older and doesn't allow transparency.
|
||||
## Transparency removed as if remove_transparency: true
|
||||
## -- png is newer but does allow transparency, but only in CLI.
|
||||
## It doesn't work in calibre PI due to limitations of the API.
|
||||
convert_images_to: jpg
|
||||
|
||||
## Remove transparency and fill with background_color if true.
|
||||
remove_transparency: true
|
||||
|
||||
## This switch prevents FanFicFare from doing any processing on the images.
|
||||
## Usually they would be converted to jpg, resized and optionally made
|
||||
## grayscale.
|
||||
#no_image_processing: false
|
||||
|
||||
## 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. I doubt adding a div around the img
|
||||
@@ -549,7 +574,7 @@ extratags: FanFiction,Testing,HTML
|
||||
#is_adult:true
|
||||
|
||||
## archiveofourown.org stories allow chapters to be added out of
|
||||
## order. So the newest chapter may not be the last one. FFDL update
|
||||
## order. So the newest chapter may not be the last one. FanFicFare 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.
|
||||
@@ -729,7 +754,7 @@ extraships:Spike/Buffy
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -758,7 +783,7 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
|
||||
|
||||
## Some adapters collect additional meta information beyond the
|
||||
## standard ones. They need to be defined in extra_valid_entries to
|
||||
## tell the rest of the FFDL system about them. They can be used in
|
||||
## tell the rest of the FanFicFare system about them. They can be used in
|
||||
## include_subject_tags, titlepage_entries, extra_titlepage_entries,
|
||||
## logpage_entries, extra_logpage_entries, and include_in_* config
|
||||
## items. You can also add additional entries here to build up
|
||||
@@ -914,7 +939,7 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1054,6 +1079,23 @@ extracategories:In Death
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/public/style_emoticons/.*
|
||||
|
||||
[it-could-happen.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## RPF == http://en.wikipedia.org/wiki/Real_person_fiction
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Glee RPF
|
||||
extracharacters:Darren Criss, Chris Colfer
|
||||
extraships:Darren Criss/Chris Colfer
|
||||
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
|
||||
[ksarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Star Trek
|
||||
@@ -1073,7 +1115,7 @@ eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
|
||||
[lotrfanfiction.com]
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1319,6 +1361,19 @@ universe_as_series: true
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[tgstorytime.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Transgender
|
||||
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
|
||||
[thehexfiles.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -1338,7 +1393,7 @@ extraships:Harry Potter/Draco Malfoy
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1352,7 +1407,7 @@ extracategories:Criminal Minds
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1425,6 +1480,8 @@ extra_titlepage_entries:readings,awards
|
||||
awards_label:Awards
|
||||
readings_label:Readings
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:art/.*Awards.jpg
|
||||
|
||||
[voracity2.e-fic.com]
|
||||
@@ -1536,7 +1593,7 @@ extra_valid_entries:reviews,favs,follows
|
||||
## to be romantic pairings.
|
||||
ships_label:Pairings
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't usually have
|
||||
## Date formats used by FanFicFare. Published and Update don't usually have
|
||||
## time, but they do now on ffnet.
|
||||
## See http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
@@ -1590,7 +1647,7 @@ cover_exclusion_regexp:(/imageu/|d_60_90\.jpg)
|
||||
## reducing this sleep.
|
||||
slow_down_sleep_time:4
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't usually have
|
||||
## Date formats used by FanFicFare. Published and Update don't usually have
|
||||
## time, but they do now on ffnet.
|
||||
## See http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
@@ -1610,7 +1667,7 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
#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
|
||||
## the newest chapter may not be the last one. FanFicFare 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.
|
||||
@@ -1621,6 +1678,10 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## to on, but can be switched off if it is found to cause problems.
|
||||
fix_fimf_blockquotes:true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/images/emoticons/
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
@@ -1704,7 +1765,7 @@ extraships:InuYasha/Kagome
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1743,7 +1804,7 @@ extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
## phoenixsong.net, oddly, can have high rated chapters (login
|
||||
## required) in the middle of a lower rated story. Use this to force
|
||||
## FFDL to always login to phoenixsong.net so those stories download
|
||||
## FanFicFare to always login to phoenixsong.net so those stories download
|
||||
## correctly. If you have a login, this is recommended.
|
||||
#force_login:true
|
||||
|
||||
|
||||
+30
-4
@@ -15,9 +15,32 @@ import copy
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.gui2.ui import get_gui
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.dialogs import UPDATE
|
||||
from calibre_plugins.fanficfare_plugin.dialogs import SAVE_UPDATE
|
||||
from calibre_plugins.fanficfare_plugin.common_utils import get_library_uuid
|
||||
|
||||
# Show translated strings, but save the same string in prefs so your
|
||||
# prefs are the same in different languages.
|
||||
YES=_('Yes, Always')
|
||||
SAVE_YES='Yes'
|
||||
YES_IF_IMG=_('Yes, if EPUB has a cover image')
|
||||
SAVE_YES_IF_IMG='Yes, if img'
|
||||
YES_UNLESS_IMG=_('Yes, unless FanFicFare found a cover image')
|
||||
SAVE_YES_UNLESS_IMG='Yes, unless img'
|
||||
NO=_('No')
|
||||
SAVE_NO='No'
|
||||
calcover_save_options = {
|
||||
YES:SAVE_YES,
|
||||
SAVE_YES:YES,
|
||||
YES_IF_IMG:SAVE_YES_IF_IMG,
|
||||
SAVE_YES_IF_IMG:YES_IF_IMG,
|
||||
YES_UNLESS_IMG:SAVE_YES_UNLESS_IMG,
|
||||
SAVE_YES_UNLESS_IMG:YES_UNLESS_IMG,
|
||||
NO:SAVE_NO,
|
||||
SAVE_NO:NO,
|
||||
}
|
||||
updatecalcover_order=[YES,YES_IF_IMG,NO]
|
||||
gencalcover_order=[YES,YES_UNLESS_IMG,NO]
|
||||
|
||||
# if don't have any settings for FanFicFarePlugin, copy from
|
||||
# predecessor FanFictionDownLoaderPlugin.
|
||||
FFDL_PREFS_NAMESPACE = 'FanFictionDownLoaderPlugin'
|
||||
@@ -35,7 +58,6 @@ Dup from another site'''
|
||||
default_prefs['reject_always'] = False
|
||||
|
||||
default_prefs['updatemeta'] = True
|
||||
default_prefs['updatecover'] = False
|
||||
default_prefs['updateepubcover'] = False
|
||||
default_prefs['keeptags'] = False
|
||||
default_prefs['suppressauthorsort'] = False
|
||||
@@ -46,10 +68,9 @@ default_prefs['autoconvert'] = False
|
||||
default_prefs['urlsfromclip'] = True
|
||||
default_prefs['updatedefault'] = True
|
||||
default_prefs['fileform'] = 'epub'
|
||||
default_prefs['collision'] = UPDATE
|
||||
default_prefs['collision'] = SAVE_UPDATE
|
||||
default_prefs['deleteotherforms'] = False
|
||||
default_prefs['adddialogstaysontop'] = False
|
||||
default_prefs['includeimages'] = False
|
||||
default_prefs['lookforurlinhtml'] = False
|
||||
default_prefs['checkforseriesurlid'] = True
|
||||
default_prefs['checkforurlchange'] = True
|
||||
@@ -63,6 +84,11 @@ default_prefs['addtolists'] = False
|
||||
default_prefs['addtoreadlists'] = False
|
||||
default_prefs['addtolistsonread'] = False
|
||||
|
||||
default_prefs['updatecalcover'] = None
|
||||
default_prefs['gencalcover'] = SAVE_YES
|
||||
default_prefs['updatecover'] = False
|
||||
default_prefs['calibre_gen_cover'] = False
|
||||
default_prefs['plugin_gen_cover'] = True
|
||||
default_prefs['gcnewonly'] = False
|
||||
default_prefs['gc_site_settings'] = {}
|
||||
default_prefs['allow_gc_from_ini'] = True
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Translations
|
||||
|
||||
If you're interested in helping provide translations for this project,
|
||||
please use the
|
||||
[Transifex](https://www.transifex.com/projects/p/calibre-plugins/resources/)
|
||||
website to add translations to this, or other calibre plugins that support it.
|
||||
+725
-644
File diff suppressed because it is too large
Load Diff
+714
-632
File diff suppressed because it is too large
Load Diff
+705
-624
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+911
-548
File diff suppressed because it is too large
Load Diff
@@ -133,6 +133,8 @@ import adapter_lotrfanfictioncom
|
||||
import adapter_fhsarchivecom
|
||||
import adapter_fanfictionjunkiesde
|
||||
import adapter_devianthearts
|
||||
import adapter_tgstorytimecom
|
||||
import adapter_itcouldhappennet
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
@@ -195,6 +197,12 @@ def getAdapter(config,url,anyurl=False):
|
||||
def getConfigSections():
|
||||
return [cls.getConfigSection() for cls in __class_list]
|
||||
|
||||
def get_bulk_load_sites():
|
||||
# for now, all eFiction Base adapters are assumed to allow bulk_load.
|
||||
return [cls.getConfigSection().replace('www.','') for cls in
|
||||
filter( lambda x : issubclass(x,base_efiction_adapter.BaseEfictionAdapter),
|
||||
__class_list)]
|
||||
|
||||
def getSiteExamples():
|
||||
l=[]
|
||||
for cls in sorted(__class_list, key=lambda x : x.getConfigSection()):
|
||||
|
||||
@@ -32,21 +32,6 @@ class DeviantHeartsAdapter(BaseEfictionAdapter):
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
# def handleMetadataPair(self, key, value):
|
||||
# if key == 'Warnings':
|
||||
# for val in re.split("\s*,\s*", value):
|
||||
# if value == 'None':
|
||||
# return
|
||||
# else:
|
||||
# # toss numbers only.
|
||||
# self.story.addToList('warnings', filter(lambda x : not x.isdigit() , val))
|
||||
|
||||
# # elif 'Categories' in key:
|
||||
# # for val in re.split("\s*>\s*", value):
|
||||
# # self.story.addToList('category', val)
|
||||
# else:
|
||||
# super(FHSArchiveComAdapter, self).handleMetadataPair(key, value)
|
||||
|
||||
def getClass():
|
||||
return DeviantHeartsAdapter
|
||||
|
||||
@@ -135,7 +135,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('title', stripHTML(titleh4.a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('span',{'class':'author'}).find('a', href=re.compile(r"^/author/\d+"))
|
||||
a = soup.find('span',{'class':'author'}).find('a', href=re.compile(r"^/a/"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
@@ -176,7 +176,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if coverurl.startswith('//'): # fix for img urls missing 'http:'
|
||||
coverurl = "http:"+coverurl
|
||||
if get_cover:
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
# try setting from href, if fails, try using the img src
|
||||
if self.setCoverImage(self.url,coverurl)[0] == "failedtoload":
|
||||
coverurl = storyImage.find('img')['src']
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
|
||||
coverSource = storyImage.find('a', {'class':'source'})
|
||||
if coverSource:
|
||||
|
||||
@@ -176,7 +176,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
soup = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
@@ -217,11 +217,10 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
while value and not defaultGetattr(value,'class') == 'label' and '<span class="label">' not in unicode(value):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
@@ -271,7 +270,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
@@ -300,8 +299,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story1'})
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 FanFicFare 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import re
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class ItCouldHappenNetSiteAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'it-could-happen.net'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(seluuf):
|
||||
return 'ich'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%B %d, %Y"
|
||||
|
||||
def handleMetadataPair(self, key, value):
|
||||
# This site is all one 'category' as it's usually defined and
|
||||
# uses Category for what is usually genre.
|
||||
if key == 'Categories':
|
||||
for val in re.split("\s*,\s*", value):
|
||||
self.story.addToList('genre', val)
|
||||
else:
|
||||
super(ItCouldHappenNetSiteAdapter, self).handleMetadataPair(key, value)
|
||||
|
||||
def getClass():
|
||||
return ItCouldHappenNetSiteAdapter
|
||||
@@ -171,7 +171,8 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
status = soup.find('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
|
||||
self.story.setMetadata('status',status.string)
|
||||
if status: # apparently this site can have stories with neither In-Progress or Complete.
|
||||
self.story.setMetadata('status',status.string)
|
||||
|
||||
section = soup.findAll('span', {'class' : 'General'})[1]
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
#from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
@@ -40,22 +40,22 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
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.
|
||||
@@ -78,7 +78,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
or "Access to unlinked chapters requires" in data:
|
||||
self.needToLogin = True
|
||||
return self.needToLogin
|
||||
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
@@ -91,13 +91,13 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
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,usecache=False)
|
||||
|
||||
|
||||
if "My Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['theusername']))
|
||||
@@ -137,22 +137,22 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url+":i",usecache=False)
|
||||
|
||||
|
||||
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)
|
||||
soup = self.make_soup(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))
|
||||
|
||||
|
||||
notice = soup.find('div', {'class' : 'notice'})
|
||||
if notice:
|
||||
self.story.setMetadata('notice',unicode(notice))
|
||||
@@ -178,7 +178,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
page=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+str(page)))
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+str(page)))
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
@@ -196,8 +196,8 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
score = lc2.findNext('th', {'class' : 'num'}).text
|
||||
if score != '-':
|
||||
self.story.setMetadata('score', score)
|
||||
|
||||
self.story.setMetadata('score', score)
|
||||
|
||||
lc4 = lc2.findNext('td', {'class' : 'lc4'})
|
||||
desc = lc4.contents[0]
|
||||
|
||||
@@ -212,7 +212,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('seriesUrl',seriesUrl)
|
||||
series_name = stripHTML(a)
|
||||
logger.debug("Series name= %s" % series_name)
|
||||
series_soup = bs.BeautifulSoup(self._fetchUrl(seriesUrl))
|
||||
series_soup = self.make_soup(self._fetchUrl(seriesUrl))
|
||||
if series_soup:
|
||||
logger.debug("Retrieving Series - looking for name")
|
||||
series_name = series_soup.find('span', {'id' : 'ptitle'}).text.partition(' — ')[0]
|
||||
@@ -221,7 +221,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
desc = lc4.contents[2]
|
||||
# Check if series is in a universe
|
||||
universe_url = self.story.getList('authorUrl')[0] + "&type=uni"
|
||||
universes_soup = bs.BeautifulSoup(self._fetchUrl(universe_url) )
|
||||
universes_soup = self.make_soup(self._fetchUrl(universe_url) )
|
||||
logger.debug("Universe url='{0}'".format(universe_url))
|
||||
if universes_soup:
|
||||
universes = universes_soup.findAll('div', {'class' : 'ser-box'})
|
||||
@@ -255,7 +255,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
universe_name = stripHTML(a)
|
||||
universeUrl = 'http://'+self.host+a['href']
|
||||
logger.debug("Retrieving Universe - about to get page - universeUrl='{0}".format(universeUrl))
|
||||
universe_soup = bs.BeautifulSoup(self._fetchUrl(universeUrl))
|
||||
universe_soup = self.make_soup(self._fetchUrl(universeUrl))
|
||||
logger.debug("Retrieving Universe - have page")
|
||||
if universe_soup:
|
||||
logger.debug("Retrieving Universe - looking for name")
|
||||
@@ -274,7 +274,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
pass
|
||||
|
||||
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
|
||||
@@ -284,24 +284,24 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
value = b.nextSibling
|
||||
#logger.debug('label: "%s", value: "%s"' % (label, value))
|
||||
|
||||
|
||||
if 'Sex' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
|
||||
if 'Tags' in label or 'Codes' in label:
|
||||
for code in re.split(r'\s*,\s*', value.strip()):
|
||||
self.story.addToList('sitetags',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')
|
||||
@@ -312,111 +312,146 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
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'})
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
chaptertag = soup.find('div', {'id' : 'story'})
|
||||
if not chaptertag:
|
||||
logger.debug("div id=story not found, try article")
|
||||
chaptertag = soup.find('article', {'id' : 'story'})
|
||||
|
||||
# some big chapters are split over several pages
|
||||
pager = div.find('span', {'class' : 'pager'})
|
||||
pager = chaptertag.find('span', {'class' : 'pager'})
|
||||
|
||||
self.cleanPage(chaptertag)
|
||||
|
||||
if pager != None:
|
||||
a = pager.previousSibling
|
||||
while a != None:
|
||||
logger.debug("before pager: {0}".format(a))
|
||||
b = a.previousSibling
|
||||
a.extract()
|
||||
a = b
|
||||
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
logger.debug("pager urls:%s"%urls)
|
||||
pager.extract()
|
||||
div.contents = div.contents[2:]
|
||||
# logger.debug(div)
|
||||
|
||||
chaptertag.contents = chaptertag.contents[2:]
|
||||
|
||||
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'})
|
||||
|
||||
# Find the "Continues" marker on the current page and remove everything after that.
|
||||
continues = div.find('span', {'class' : 'conTag'})
|
||||
if continues != None:
|
||||
while continues != None:
|
||||
# logger.debug("removing end: {0}".format(continues))
|
||||
b = continues.nextSibling
|
||||
continues.extract()
|
||||
continues = b
|
||||
soup = self.make_soup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']))
|
||||
|
||||
# Find the "Continued" marker and delete everything before that
|
||||
continued = div1.find('span', {'class' : 'conTag'})
|
||||
if continued != None:
|
||||
a = continued.previousSibling
|
||||
while a != None:
|
||||
# logger.debug("before conTag: {0}".format(a))
|
||||
b = a.previousSibling
|
||||
a.extract()
|
||||
a = b
|
||||
# Remove the pager from the end if this is the last page
|
||||
endPager = div1.find('span', {'class' : 'pager'})
|
||||
if endPager != None:
|
||||
b = endPager.nextSibling
|
||||
while endPager != None:
|
||||
logger.debug("removing end: {0}".format(endPager))
|
||||
b = endPager.nextSibling
|
||||
endPager.extract()
|
||||
endPager = b
|
||||
div1.contents = div1.contents[:len(div1) - 2]
|
||||
# logger.debug("after removing pager: {0}".format(div1))
|
||||
for tag in div1.contents[2:]:
|
||||
div.append(tag)
|
||||
pagetag = soup.find('div', {'id' : 'story'})
|
||||
if not pagetag:
|
||||
logger.debug("div id=story not found, try article")
|
||||
pagetag = soup.find('article', {'id' : 'story'})
|
||||
|
||||
# If it is a chapter, there are dates at the start for when it was posted or modified. These plus
|
||||
# everything before them can be discarded.
|
||||
postedDates = div.findAll('div', {'class' : 'date'})
|
||||
self.cleanPage(pagetag)
|
||||
|
||||
for tag in pagetag.contents[2:]:
|
||||
chaptertag.append(tag)
|
||||
|
||||
|
||||
if None == chaptertag:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,chaptertag)
|
||||
|
||||
def cleanPage(self,pagetag):
|
||||
"Consolidate 'page' clean up code so it can be called."
|
||||
|
||||
# some big chapters are split over several pages
|
||||
# remove FIRST pager and everything before it.
|
||||
tag = pagetag.find('span', {'class' : 'pager'})
|
||||
while tag != None:
|
||||
# logger.debug("remove before pager: {0}".format(tag))
|
||||
prev = tag.previousSibling
|
||||
tag.extract()
|
||||
tag = prev
|
||||
|
||||
# Find the "Continues" marker on the current page and
|
||||
# remove everything after that. This is actually
|
||||
# effecting the *previous* 'page'. EXCEPT!--they are
|
||||
# putting a 'conTag' at the *top* now, too. So this
|
||||
# was nuking every page but the first and last. Now
|
||||
# only if 'Continues'
|
||||
for contag in pagetag.findAll('span', {'class' : 'conTag'}):
|
||||
# remove everything after continues...
|
||||
if 'Continuation' in contag.text:
|
||||
tag = contag
|
||||
while tag != None:
|
||||
# logger.debug("remove before Continuation: {0}".format(tag))
|
||||
prev = tag.previousSibling
|
||||
tag.extract()
|
||||
tag = prev
|
||||
elif 'Continues' in contag.text:
|
||||
tag = contag
|
||||
while tag != None:
|
||||
# logger.debug("remove after Continues: {0}".format(tag))
|
||||
nxt = tag.nextSibling
|
||||
tag.extract()
|
||||
tag = nxt
|
||||
|
||||
# some big chapters are split over several pages
|
||||
# remove LAST pager and everything before it.
|
||||
# Only needed on last page.
|
||||
tag = pagetag.find('span', {'class' : 'pager'})
|
||||
while tag != None:
|
||||
# logger.debug("remove after pager: {0}".format(tag))
|
||||
nxt = tag.nextSibling
|
||||
tag.extract()
|
||||
tag = nxt
|
||||
|
||||
# If it is a chapter, there are dates at the start for when it was posted or modified. These plus
|
||||
# everything before them can be discarded.
|
||||
postedDates = pagetag.findAll('div', {'class' : 'date'})
|
||||
if postedDates:
|
||||
a = postedDates[0].previousSibling
|
||||
while a != None:
|
||||
# logger.debug("before dates: {0}".format(a))
|
||||
# logger.debug("before dates: {0}".format(a))
|
||||
b = a.previousSibling
|
||||
a.extract()
|
||||
a = b
|
||||
for a in div.findAll('div', {'class' : 'date'}):
|
||||
for a in pagetag.findAll('div', {'class' : 'date'}):
|
||||
a.extract()
|
||||
|
||||
# For single chapter stories, there is a copyright statement. Remove this and everything
|
||||
# before it.
|
||||
copy = div.find('h4', {'class': 'copy'})
|
||||
copy = pagetag.find('h4', {'class': 'copy'})
|
||||
while copy != None:
|
||||
# logger.debug("before copyright: {0}".format(copy))
|
||||
# logger.debug("before copyright: {0}".format(copy))
|
||||
b = copy.previousSibling
|
||||
copy.extract()
|
||||
copy = b
|
||||
|
||||
# For a story or the last chapter, remove voting form and the in library box
|
||||
a = div.find('div', {'id' : 'vote-form'})
|
||||
a = pagetag.find('div', {'id' : 'vote-form'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
a = div.find('div', {'id' : 'b-man-div'})
|
||||
a = pagetag.find('div', {'id' : 'top-header'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
a = pagetag.find('div', {'id' : 'b-man-div'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
|
||||
# Kill the "The End" header and everything after it.
|
||||
a = div.find(['h2', 'h3'], {'class' : 'end'})
|
||||
logger.debug("Chapter end= '{0}'".format(a))
|
||||
# Kill the vote form and everything after it.
|
||||
a = pagetag.find('div', {'class' : 'vform'})
|
||||
# logger.debug("Chapter end= '{0}'".format(a))
|
||||
while a != None:
|
||||
b = a.nextSibling
|
||||
a.extract()
|
||||
a=b
|
||||
|
||||
# Kill the vote form and everything after it.
|
||||
a = pagetag.find('h3', {'class' : 'end'})
|
||||
# logger.debug("Chapter end= '{0}'".format(a))
|
||||
while a != None:
|
||||
b = a.nextSibling
|
||||
a.extract()
|
||||
a=b
|
||||
|
||||
foot = pagetag.find('footer')
|
||||
if foot != None:
|
||||
foot.extract()
|
||||
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 FanFicFare 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class TGStorytimeComAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'tgstorytime.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'tgstory'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
def getClass():
|
||||
return TGStorytimeComAdapter
|
||||
@@ -379,10 +379,12 @@ class BaseSiteAdapter(Configurable):
|
||||
self.getConfig('allow_unsafe_filename')),
|
||||
self._fetchUrlRaw,
|
||||
cover=True)
|
||||
self.story.setMetadata('cover_image','default')
|
||||
|
||||
# no new cover, set old cover, if there is one.
|
||||
if not self.story.cover and self.oldcover:
|
||||
self.story.oldcover = self.oldcover
|
||||
self.story.setMetadata('cover_image','old')
|
||||
|
||||
# cheesy way to carry calibre bookmark file forward across update.
|
||||
if self.calibrebookmark:
|
||||
@@ -479,8 +481,10 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def setCoverImage(self,storyurl,imgurl):
|
||||
if self.getConfig('include_images'):
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
return self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
else:
|
||||
return (None,None)
|
||||
|
||||
# bs3 & bs4 are different here.
|
||||
# will move to a bs3 vs bs4 block if there's lots of changes.
|
||||
|
||||
@@ -89,7 +89,6 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
"""
|
||||
return ["Windows-1252", "utf8"]
|
||||
|
||||
|
||||
@classmethod
|
||||
def getPathToArchive(cls):
|
||||
"""
|
||||
@@ -325,6 +324,8 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
|
||||
## Handle warnings and login checks
|
||||
errorDiv = soup.find("div", "errortext")
|
||||
if errorDiv is None:
|
||||
errorDiv = soup.find("div", "errormsg") # sometimes different class.
|
||||
while errorDiv is not None:
|
||||
if self.getMessageRegisteredUsersOnly() in errorDiv.prettify():
|
||||
if not self.triedLoggingIn:
|
||||
|
||||
+49
-8
@@ -17,6 +17,7 @@
|
||||
|
||||
from optparse import OptionParser
|
||||
from os.path import expanduser, isfile, join, dirname
|
||||
from os import access, R_OK
|
||||
from subprocess import call
|
||||
import ConfigParser
|
||||
import getpass
|
||||
@@ -92,6 +93,10 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
parser.add_option('--force',
|
||||
action='store_true', dest='force',
|
||||
help='Force overwrite of an existing epub, download and overwrite all chapters.', )
|
||||
parser.add_option('-i', '--infile',
|
||||
help='Give a filename to read for URLs (and/or existing EPUB files with -u for updates).',
|
||||
dest='infile', default=None,
|
||||
metavar='INFILE')
|
||||
parser.add_option('-l', '--list',
|
||||
action='store_true', dest='list',
|
||||
help='Get list of valid story URLs from page given.', )
|
||||
@@ -111,7 +116,7 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
logger = logging.getLogger('fanficfare')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if not options.siteslist and len(args) != 1:
|
||||
if not (options.siteslist or options.infile) and len(args) != 1:
|
||||
parser.error('incorrect number of arguments')
|
||||
|
||||
if options.siteslist:
|
||||
@@ -124,24 +129,60 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
if options.update and options.format != 'epub':
|
||||
parser.error('-u/--update-epub only works with epub')
|
||||
|
||||
# for passing in a file list
|
||||
if options.infile:
|
||||
urls=[]
|
||||
with open(options.infile,"r") as infile:
|
||||
#print "File exists and is readable"
|
||||
|
||||
#fileurls = [line.strip() for line in infile]
|
||||
for url in infile:
|
||||
url = url[:url.find('#')].strip()
|
||||
if len(url) > 0:
|
||||
#print "URL: (%s)"%url
|
||||
urls.append(url)
|
||||
else:
|
||||
urls = args
|
||||
|
||||
if len(urls) > 1:
|
||||
for url in urls:
|
||||
try:
|
||||
do_download(url,
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini)
|
||||
except Exception, e:
|
||||
print "URL(%s) Failed: Exception (%s). Run URL individually for more detail."%(url,e)
|
||||
else:
|
||||
do_download(urls[0],
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini)
|
||||
|
||||
# make rest a function and loop on it.
|
||||
def do_download(arg,
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini):
|
||||
|
||||
# Attempt to update an existing epub.
|
||||
chaptercount = None
|
||||
output_filename = None
|
||||
if options.update:
|
||||
try:
|
||||
url, chaptercount = get_dcsource_chaptercount(args[0])
|
||||
url, chaptercount = get_dcsource_chaptercount(arg)
|
||||
if not url:
|
||||
print 'No story URL found in epub to update.'
|
||||
return
|
||||
print 'Updating %s, URL: %s' % (args[0], url)
|
||||
output_filename = args[0]
|
||||
print 'Updating %s, URL: %s' % (arg, url)
|
||||
output_filename = arg
|
||||
except Exception:
|
||||
# if there's an error reading the update file, maybe it's a URL?
|
||||
# we'll look for an existing outputfile down below.
|
||||
url = args[0]
|
||||
url = arg
|
||||
else:
|
||||
url = args[0]
|
||||
|
||||
url = arg
|
||||
|
||||
try:
|
||||
configuration = Configuration(adapters.getConfigSectionFor(url), options.format)
|
||||
except exceptions.UnknownSite, e:
|
||||
@@ -210,7 +251,7 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
configuration.set('overrides', var, val)
|
||||
|
||||
if options.list or options.normalize:
|
||||
retlist = get_urls_from_page(args[0], configuration, normalize=options.normalize)
|
||||
retlist = get_urls_from_page(arg, configuration, normalize=options.normalize)
|
||||
print '\n'.join(retlist)
|
||||
return
|
||||
|
||||
|
||||
@@ -118,15 +118,10 @@ def get_valid_set_options():
|
||||
'universe_as_series':(['storiesonline.net'],None,boollist),
|
||||
'strip_text_links':(['bloodshedverse.com'],None,boollist),
|
||||
|
||||
# eFiction Base
|
||||
'bulk_load':(['fannation.shades-of-moonlight.com',
|
||||
'fhsarchive.com',
|
||||
'lotrfanfiction.com',
|
||||
'themaplebookshelf.com',
|
||||
'devianthearts.com',
|
||||
'thehookupzone.net',
|
||||
'libraryofmoria.com',
|
||||
],None,boollist),
|
||||
# eFiction Base adapters allow bulk_load
|
||||
# kept forgetting to add them, so now it's automatic.
|
||||
'bulk_load':(adapters.get_bulk_load_sites(),
|
||||
None,boollist),
|
||||
|
||||
'include_logpage':(None,['epub'],boollist+['smart']),
|
||||
|
||||
@@ -136,6 +131,7 @@ def get_valid_set_options():
|
||||
'grayscale_images':(None,['epub','html'],boollist),
|
||||
'no_image_processing':(None,['epub','html'],boollist),
|
||||
}
|
||||
|
||||
return dict(valdict)
|
||||
|
||||
def get_valid_scalar_entries():
|
||||
@@ -163,6 +159,7 @@ def get_valid_scalar_entries():
|
||||
'seriesHTML',
|
||||
'langcode',
|
||||
'output_css',
|
||||
'cover_image',
|
||||
])
|
||||
|
||||
def get_valid_entries():
|
||||
|
||||
+58
-19
@@ -85,9 +85,9 @@ authorId_label:Author ID
|
||||
## show up in Calibre as tags. Also carried into mobi when converted.
|
||||
extratags_label:Extra Tags
|
||||
## The version of fanficdownloader
|
||||
version_label:FFDL Version
|
||||
version_label:Downloader Version
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't have time.
|
||||
## Date formats used by FanFicFare. 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
|
||||
@@ -205,7 +205,7 @@ connect_timeout:60.0
|
||||
## Note that if metakey == conditionalkey the conditional is ignored.
|
||||
## You can use \s in the replacement to add explicit spaces. (The config parser
|
||||
## tends to discard trailing spaces.)
|
||||
## replace_metadata <entry>_LIST options: FFDL replace_metadata lines
|
||||
## replace_metadata <entry>_LIST options: FanFicFare replace_metadata lines
|
||||
## operate on individual list items for list entries. But if you
|
||||
## want to do a replacement on the joined string for the whole list,
|
||||
## you can by using <entry>_LIST. Example, if you added
|
||||
@@ -312,20 +312,20 @@ chapter_title_add_pattern:${index}. ${title}
|
||||
## ships_LIST to change the / back to something else if you want.
|
||||
sort_ships:false
|
||||
|
||||
## join_string_<entry> options -- FFDL list entries are comma
|
||||
## join_string_<entry> options -- FanFicFare list entries are comma
|
||||
## separated by default. You can use this to change that. For example,
|
||||
## if you want authors separated with ' & ' instead, use
|
||||
## join_string_calibre_author:\s&\s. (\s == space)
|
||||
#join_string_author:,\s
|
||||
|
||||
## keep_in_order_<entry> options: FFDL sorts list entries by default
|
||||
## keep_in_order_<entry> options: FanFicFare sorts list entries by default
|
||||
## (except for author/authorUrl/authorId). But if you want to use an
|
||||
## extra entry derived from author, it ends up sorted. For example, if
|
||||
## you added calibre_author: keep_in_order_calibre_author:true
|
||||
#keep_in_order_author:true
|
||||
|
||||
## User-agent
|
||||
user_agent:FFDL/2.0
|
||||
user_agent:FFF/2.X
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
@@ -341,7 +341,7 @@ user_agent:FFDL/2.0
|
||||
#output_filename: books/${author}/${title}/${title}-${siteabbrev}_${authorId}_${storyId}${formatext}
|
||||
#zip_output: false
|
||||
|
||||
## This switch prevents FFDL from doing any processing on the images.
|
||||
## This switch prevents FanFicFare from doing any processing on the images.
|
||||
## Usually they would be converted to jpg, resized and optionally made
|
||||
## grayscale.
|
||||
no_image_processing: true
|
||||
@@ -503,6 +503,11 @@ convert_images_to: jpg
|
||||
## Remove transparency and fill with background_color if true.
|
||||
remove_transparency: true
|
||||
|
||||
## This switch prevents FanFicFare from doing any processing on the images.
|
||||
## Usually they would be converted to jpg, resized and optionally made
|
||||
## grayscale.
|
||||
#no_image_processing: false
|
||||
|
||||
## 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. I doubt adding a div around the img
|
||||
@@ -573,7 +578,7 @@ extratags: FanFiction,Testing,HTML
|
||||
#is_adult:true
|
||||
|
||||
## archiveofourown.org stories allow chapters to be added out of
|
||||
## order. So the newest chapter may not be the last one. FFDL update
|
||||
## order. So the newest chapter may not be the last one. FanFicFare 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.
|
||||
@@ -753,7 +758,7 @@ extraships:Spike/Buffy
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -782,7 +787,7 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
|
||||
|
||||
## Some adapters collect additional meta information beyond the
|
||||
## standard ones. They need to be defined in extra_valid_entries to
|
||||
## tell the rest of the FFDL system about them. They can be used in
|
||||
## tell the rest of the FanFicFare system about them. They can be used in
|
||||
## include_subject_tags, titlepage_entries, extra_titlepage_entries,
|
||||
## logpage_entries, extra_logpage_entries, and include_in_* config
|
||||
## items. You can also add additional entries here to build up
|
||||
@@ -920,7 +925,7 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1060,6 +1065,23 @@ extracategories:In Death
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/public/style_emoticons/.*
|
||||
|
||||
[it-could-happen.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## RPF == http://en.wikipedia.org/wiki/Real_person_fiction
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Glee RPF
|
||||
extracharacters:Darren Criss, Chris Colfer
|
||||
extraships:Darren Criss/Chris Colfer
|
||||
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
|
||||
[ksarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Star Trek
|
||||
@@ -1079,7 +1101,7 @@ eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
|
||||
[lotrfanfiction.com]
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1325,6 +1347,19 @@ universe_as_series: true
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[tgstorytime.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Transgender
|
||||
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
|
||||
[thehexfiles.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -1344,7 +1379,7 @@ extraships:Harry Potter/Draco Malfoy
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1358,7 +1393,7 @@ extracategories:Criminal Minds
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1533,7 +1568,7 @@ extra_valid_entries:reviews,favs,follows
|
||||
## to be romantic pairings.
|
||||
ships_label:Pairings
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't usually have
|
||||
## Date formats used by FanFicFare. Published and Update don't usually have
|
||||
## time, but they do now on ffnet.
|
||||
## See http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
@@ -1590,7 +1625,7 @@ cover_exclusion_regexp:(/imageu/|d_60_90\.jpg)
|
||||
## reducing this sleep.
|
||||
slow_down_sleep_time:4
|
||||
|
||||
## Date formats used by FFDL. Published and Update don't usually have
|
||||
## Date formats used by FanFicFare. Published and Update don't usually have
|
||||
## time, but they do now on ffnet.
|
||||
## See http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
@@ -1610,7 +1645,7 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
#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
|
||||
## the newest chapter may not be the last one. FanFicFare 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.
|
||||
@@ -1621,6 +1656,10 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## to on, but can be switched off if it is found to cause problems.
|
||||
fix_fimf_blockquotes:true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/images/emoticons/
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
@@ -1704,7 +1743,7 @@ extraships:InuYasha/Kagome
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
|
||||
## Virtually all eFiction-based sites allow downloading the whole story in
|
||||
## Virtually all eFiction Base adapters allow downloading the whole story in
|
||||
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
|
||||
## metadata and chapters can be loaded in one step
|
||||
bulk_load:true
|
||||
@@ -1743,7 +1782,7 @@ extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
## phoenixsong.net, oddly, can have high rated chapters (login
|
||||
## required) in the middle of a lower rated story. Use this to force
|
||||
## FFDL to always login to phoenixsong.net so those stories download
|
||||
## FanFicFare to always login to phoenixsong.net so those stories download
|
||||
## correctly. If you have a login, this is recommended.
|
||||
#force_login:true
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ def get_update_data(inputio,
|
||||
if getfilecount:
|
||||
# spin through the manifest--only place there are item tags.
|
||||
for item in contentdom.getElementsByTagName("item"):
|
||||
# First, count the 'chapter' files. FFDL uses file0000.xhtml,
|
||||
# First, count the 'chapter' files. FFF uses file0000.xhtml,
|
||||
# but can also update epubs downloaded from Twisting the
|
||||
# Hellmouth, which uses chapter0.html.
|
||||
if( item.getAttribute("media-type") == "application/xhtml+xml" ):
|
||||
@@ -175,7 +175,7 @@ def get_story_url_from_html(inputio,_is_good_url=None):
|
||||
|
||||
# spin through the manifest--only place there are item tags.
|
||||
for item in contentdom.getElementsByTagName("item"):
|
||||
# First, count the 'chapter' files. FFDL uses file0000.xhtml,
|
||||
# First, count the 'chapter' files. FFF uses file0000.xhtml,
|
||||
# but can also update epubs downloaded from Twisting the
|
||||
# Hellmouth, which uses chapter0.html.
|
||||
#print("---- item:%s"%item)
|
||||
|
||||
@@ -24,6 +24,13 @@ class FailedToDownload(Exception):
|
||||
def __str__(self):
|
||||
return self.error
|
||||
|
||||
class RejectImage(Exception):
|
||||
def __init__(self,error):
|
||||
self.error=error
|
||||
|
||||
def __str__(self):
|
||||
return self.error
|
||||
|
||||
class InvalidStoryURL(Exception):
|
||||
def __init__(self,url,domain,example):
|
||||
self.url=url
|
||||
|
||||
+21
-13
@@ -44,6 +44,8 @@ imagetypes = {
|
||||
|
||||
try:
|
||||
from calibre.utils.magick import Image
|
||||
from StringIO import StringIO
|
||||
from gif import GifInfo, CHECK_IS_ANIMATED
|
||||
convtype = {'jpg':'JPG', 'png':'PNG'}
|
||||
|
||||
def convert_image(url,data,sizes,grayscale,
|
||||
@@ -55,6 +57,10 @@ try:
|
||||
owidth, oheight = img.size
|
||||
nwidth, nheight = sizes
|
||||
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
|
||||
|
||||
if normalize_format_name(img.format)=="gif" and GifInfo(StringIO(data),CHECK_IS_ANIMATED).frameCount > 1:
|
||||
raise exceptions.RejectImage("Animated gifs come out purely--not going to use it.")
|
||||
|
||||
if scaled:
|
||||
img.size = (nwidth, nheight)
|
||||
export = True
|
||||
@@ -224,7 +230,7 @@ langs = {
|
||||
"Devanagari":"hi",
|
||||
|
||||
## These are from/for AO3:
|
||||
|
||||
|
||||
u'العربية':'ar',
|
||||
u'беларуская':'be',
|
||||
u'Български език':'bg',
|
||||
@@ -325,7 +331,7 @@ class InExMatch:
|
||||
else:
|
||||
retval = self.match == value
|
||||
#print(">>>>>>>>>>>>>%s==%s r: %s,%s=%s"%(self.match,value,self.negate,retval, self.negate != retval))
|
||||
|
||||
|
||||
return self.negate != retval
|
||||
|
||||
def __str__(self):
|
||||
@@ -338,7 +344,7 @@ class InExMatch:
|
||||
else:
|
||||
s='='
|
||||
return u'InExMatch(%s %s%s %s)'%(self.keys,f,s,self.match)
|
||||
|
||||
|
||||
## metakey[,metakey]=~pattern
|
||||
## metakey[,metakey]==string
|
||||
## *for* part lines. Effect only when trailing conditional key=~regexp matches
|
||||
@@ -358,7 +364,7 @@ def set_in_ex_clude(setting):
|
||||
match = InExMatch(line)
|
||||
dest.append([match,condmatch])
|
||||
return dest
|
||||
|
||||
|
||||
## Two or three part lines. Two part effect everything.
|
||||
## Three part effect only those key(s) lists.
|
||||
## pattern=>replacement
|
||||
@@ -433,7 +439,7 @@ class Story(Configurable):
|
||||
|
||||
def join_list(self, key, vallist):
|
||||
return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist))
|
||||
|
||||
|
||||
def setMetadata(self, key, value, condremoveentities=True):
|
||||
|
||||
# keep as list type, but set as only value.
|
||||
@@ -445,20 +451,20 @@ class Story(Configurable):
|
||||
self.metadata[key]=conditionalRemoveEntities(value)
|
||||
else:
|
||||
self.metadata[key]=value
|
||||
|
||||
|
||||
if key == "language":
|
||||
try:
|
||||
# getMetadata not just self.metadata[] to do replace_metadata.
|
||||
self.setMetadata('langcode',langs[self.getMetadata(key)])
|
||||
except:
|
||||
self.setMetadata('langcode','en')
|
||||
|
||||
|
||||
if key == 'dateUpdated' and value:
|
||||
# Last Update tags for Bill.
|
||||
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"))
|
||||
self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d"))
|
||||
|
||||
|
||||
|
||||
def do_in_ex_clude(self,which,value,key):
|
||||
if value and which in self.in_ex_cludes:
|
||||
include = 'include' in which
|
||||
@@ -487,7 +493,7 @@ class Story(Configurable):
|
||||
if include and keyfound and not found:
|
||||
value = None
|
||||
return value
|
||||
|
||||
|
||||
|
||||
def doReplacements(self,value,key,return_list=False,seen_list=[]):
|
||||
value = self.do_in_ex_clude('include_metadata_pre',value,key)
|
||||
@@ -526,7 +532,7 @@ class Story(Configurable):
|
||||
# print("replacement,value:%s,%s->%s"%(replacement,value,regexp.sub(replacement,value)))
|
||||
value = regexp.sub(replacement,value)
|
||||
retlist = [value]
|
||||
|
||||
|
||||
for val in retlist:
|
||||
retlist = map(partial(self.do_in_ex_clude,'include_metadata_post',key=key),retlist)
|
||||
retlist = map(partial(self.do_in_ex_clude,'exclude_metadata_post',key=key),retlist)
|
||||
@@ -610,7 +616,7 @@ class Story(Configurable):
|
||||
self.getMetadata('author', removeallentities, doreplacements)))
|
||||
|
||||
self.extendList("extratags",self.getConfigList("extratags"))
|
||||
|
||||
|
||||
if self.getMetadataRaw('seriesUrl'):
|
||||
self.setMetadata('seriesHTML',linkhtml%('series',self.getMetadata('seriesUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('series', removeallentities, doreplacements)))
|
||||
@@ -703,10 +709,10 @@ class Story(Configurable):
|
||||
for val in retlist:
|
||||
newretlist.extend(self.doReplacements(val,listname,return_list=True))
|
||||
retlist = newretlist
|
||||
|
||||
|
||||
if removeallentities:
|
||||
retlist = map(removeAllEntities,retlist)
|
||||
|
||||
|
||||
retlist = filter( lambda x : x!=None and x!='' ,retlist)
|
||||
|
||||
# reorder ships so b/a and c/b/a become a/b and a/b/c. Only on '/',
|
||||
@@ -874,6 +880,7 @@ class Story(Configurable):
|
||||
self.imgurls.insert(0,imgurl)
|
||||
newsrc = "images/cover.%s"%ext
|
||||
self.cover=newsrc
|
||||
self.setMetadata('cover_image','specific')
|
||||
self.imgtuples.insert(0,{'newsrc':newsrc,'mime':mime,'data':data})
|
||||
else:
|
||||
self.imgurls.append(imgurl)
|
||||
@@ -888,6 +895,7 @@ class Story(Configurable):
|
||||
not (coverexclusion and re.search(coverexclusion,imgurl)):
|
||||
newsrc = "images/cover.%s"%ext
|
||||
self.cover=newsrc
|
||||
self.setMetadata('cover_image','first')
|
||||
self.imgtuples.append({'newsrc':newsrc,'mime':mime,'data':data})
|
||||
self.imgurls.append(imgurl)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class BaseStoryWriter(Configurable):
|
||||
'formatname':'File Format',
|
||||
'formatext':'File Extension',
|
||||
'siteabbrev':'Site Abbrev',
|
||||
'version':'FFDL Version'
|
||||
'version':'Downloader Version'
|
||||
}
|
||||
self.story.setMetadata('formatname',self.getFormatName())
|
||||
self.story.setMetadata('formatext',self.getFormatExt())
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
A pure Python GIF metadata extractor.
|
||||
Supports adjustable detail to fine-tune performance.
|
||||
|
||||
Example code and full epydoc docstrings included.
|
||||
|
||||
Uses:
|
||||
- Identifying whether a GIF is static or animated.
|
||||
- Extracting the dimensions, pixel aspect ratio, number of frames, loop count,
|
||||
global palette or palette size, and background color.
|
||||
- Extracting comments and other plaintext.
|
||||
- Testing for various structural errors.
|
||||
|
||||
TODO:
|
||||
- Provide basic support for XMP Metadata extraction
|
||||
- http://en.wikipedia.org/wiki/Extensible_Metadata_Platform#Location_in_file_types
|
||||
- http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
- Generate test GIF with http://code.google.com/p/python-xmp-toolkit/
|
||||
|
||||
Changelog:
|
||||
- 0.2.2: Audited the code and made some corrections.
|
||||
- 0.2.1: 40% speed improvement (went from 15 to 9 seconds for 1000 images)
|
||||
- 0.2.0: Feature-complete
|
||||
- 0.1.0: Initial release
|
||||
"""
|
||||
|
||||
__appname__ = "gif.py"
|
||||
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
|
||||
__version__ = "0.2.2"
|
||||
__license__ = "PSF License 2.4 or higher (The Python License)"
|
||||
|
||||
#{ Check Types (enum, numerical ordering is significant)
|
||||
CHECK_IS_GIF_FILE = 0 #: Just check for a valid GIF header.
|
||||
CHECK_IS_ANIMATED = 1 #: Check whether the file has more than one frame.
|
||||
CHECK_COUNT_FRAMES = 2 #: Count the number of frames in the file.
|
||||
CHECK_PARSE_PALETTE = 3 #: Parse the palette and resolve the background color.
|
||||
CHECK_READ_COMMENTS = 4 #: Load comments (can sometimes be large) into the L{GifInfo} object.
|
||||
CHECK_READ_ALL_TEXT = 5 #: Also load the contents of Plain Text extension blocks.
|
||||
|
||||
CHECK_ALL = CHECK_READ_ALL_TEXT #: alias to allow for future modifications
|
||||
|
||||
#{ Warning Codes (bitfield)
|
||||
WARN_NONE = 0 #: No warnings
|
||||
WARN_BAD_IMG = 1 #: Corruption (of the [sub]block size field(s)) or truncation detected in an image block
|
||||
WARN_BAD_EXT = 2 #: Corruption (of the [sub]block size field(s)) or truncation detected in an extension block
|
||||
WARN_BAD_SIZE = 4 #: An image block specifies dimensions exceeding the global canvas size
|
||||
WARN_BAD_BGCOLOR = 8 #: The background color index specified is greater than the palette size
|
||||
WARN_EOF = 16 #: File is missing it's trailer. (Corrupt elsewhere, truncated, or breaking spec by using EOF as the terminator.)
|
||||
WARN_TRUNC = 32 #: File is definitively either truncated or corrupt. (An EOF was encountered part-way through a structure.)
|
||||
WARN_LOOP_POS = 64 #: Netscape Application Extension block (animation-control) was present but not first in the file.
|
||||
#}
|
||||
|
||||
import struct
|
||||
|
||||
#{ Structures used by GifInfo
|
||||
gifHeaderStruct = struct.Struct('<xxxxxxHHBBB') #: File header
|
||||
gifImageStruct = struct.Struct('<HHHHB') #: Image block header
|
||||
gifExtenStruct = struct.Struct('<BB') #: Top-level extension block header
|
||||
gifNetscapeStruct = struct.Struct('<BBH') #: NETSCAPE Loop-control sub-block
|
||||
gifColorTripleStruct = struct.Struct('<BBB') #: RGB palette element
|
||||
gifPlaintextStruct = struct.Struct('<BHHHHBBBB') #: Plain Text block header
|
||||
#}
|
||||
|
||||
class BadHeaderException(Exception):
|
||||
"""Raised when no valid GIF header is found"""
|
||||
|
||||
class GifInfo(object):
|
||||
"""A class for loading and storing metadata from GIF files.
|
||||
|
||||
Accepts paths and file-like objects.
|
||||
|
||||
When using L{CHECK_ALL}, this can also be used to walk past a valid
|
||||
GIF file in an un-delimited byte stream in order to identify the point at
|
||||
which the following file starts. (It doesn't C{fh.seek(0)} or C{fh.close()} after
|
||||
use)
|
||||
"""
|
||||
warnFlags = WARN_NONE #: A bit field of C{WARN_*} flags set by L{__init__}
|
||||
checkLevel = CHECK_ALL #: Default C{CHECK_*} level used by L{__init__}
|
||||
|
||||
#{ Pre-defined "unset" values for GIF Metadata
|
||||
path = None #: The path to the file, if one was passed to L{__init__}
|
||||
version = None #: C{87a} or C{89a}
|
||||
width = None
|
||||
height = None
|
||||
loopCount = None
|
||||
pixelAspect = None
|
||||
paletteSize = None #: Always calculated if a global palette is present
|
||||
palette = None #: The palette as a list of integer RGB tuples. Requires L{CHECK_PARSE_PALETTE}.
|
||||
bgColor = None #: Global background color as an RGB tuple. Requires L{CHECK_PARSE_PALETTE}.
|
||||
comments = None #: Text in Comment (0xFE) extension blocks as a list of strings. Requires L{CHECK_READ_COMMENTS}
|
||||
otherText = None #: Text in "Plain Text" (0x01) extension blocks as a list of strings. Requires L{CHECK_READ_ALL_TEXT}
|
||||
frameCount = 0
|
||||
#}
|
||||
|
||||
def __init__(self, fh, checkLevel=checkLevel):
|
||||
"""
|
||||
@param fh: A path or file-like object for a GIF file.
|
||||
@param checkLevel: A C{CHECK_*} constant.
|
||||
|
||||
@raises BadHeaderException: The given file lacks a valid GIF header.
|
||||
@raises IOError: The underlying C{open()} system call failed.
|
||||
"""
|
||||
self.checkLevel = checkLevel
|
||||
if isinstance(fh, basestring):
|
||||
self.path = fh
|
||||
fh = open(fh, 'rb')
|
||||
|
||||
header = fh.read(gifHeaderStruct.size)
|
||||
if len(header) < gifHeaderStruct.size:
|
||||
raise BadHeaderException("File is too small to be a GIF")
|
||||
|
||||
self.version = header[3:6]
|
||||
if header[0:3] != 'GIF' or self.version not in ['87a', '89a']:
|
||||
raise BadHeaderException("File does not have a recognizable GIF header")
|
||||
elif self.checkLevel <= CHECK_IS_GIF_FILE:
|
||||
return
|
||||
|
||||
self.width, self.height, GCTF_Byte, bgColor, self.pixelAspect = gifHeaderStruct.unpack(header)
|
||||
|
||||
if self.pixelAspect:
|
||||
self.pixelAspect = (self.pixelAspect + 15) / 64.0
|
||||
|
||||
rawPalette = self._getPalette(fh, GCTF_Byte)
|
||||
self.paletteSize = int(len(rawPalette) / 3)
|
||||
if self.checkLevel >= CHECK_PARSE_PALETTE:
|
||||
self.palette = []
|
||||
for pos in range(0, self.paletteSize):
|
||||
self.palette.append(gifColorTripleStruct.unpack_from(rawPalette, pos * 3))
|
||||
|
||||
if self.paletteSize and bgColor > self.paletteSize:
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_BGCOLOR
|
||||
elif self.palette:
|
||||
self.bgColor = self.palette[bgColor]
|
||||
|
||||
# Iterate blocks
|
||||
self.firstBlock = True
|
||||
blocktype = self._read(fh, 1)
|
||||
while not blocktype == chr(0x3B) and not self.warnFlags & WARN_EOF:
|
||||
self._blockHandlers.get(blocktype, lambda x, y:'')(self, fh)
|
||||
if self.checkLevel <= CHECK_IS_ANIMATED and self.frameCount > 1:
|
||||
return
|
||||
|
||||
self.firstBlock = False
|
||||
blocktype = self._read(fh, 1)
|
||||
|
||||
del self.firstBlock
|
||||
|
||||
def _handleImageBlock(self, fh):
|
||||
""""""
|
||||
self.frameCount += 1
|
||||
try:
|
||||
x, y, w, h, LCTF_Byte = gifImageStruct.unpack(self._read(fh, gifImageStruct.size))
|
||||
except:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF | WARN_TRUNC
|
||||
return
|
||||
|
||||
if x + w > self.width or y + h > self.height:
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_SIZE
|
||||
|
||||
self._getPalette(fh, LCTF_Byte) # Skip the local color table if present
|
||||
fh.read(1) # Skip the LZW minimum code size.
|
||||
|
||||
# Skip content and test for the block terminator
|
||||
if not self._skipSubBlocks(fh): # For example, if it's a zero-length string like EOF would return.
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_IMG
|
||||
|
||||
def _handleGenericExtensionBlock(self, fh):
|
||||
"""@todo: Rewrite this so extension block types have method handlers."""
|
||||
try:
|
||||
extType, blkSize = gifExtenStruct.unpack(self._read(fh, gifExtenStruct.size))
|
||||
except:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF | WARN_TRUNC
|
||||
return
|
||||
startOffset = fh.tell()
|
||||
|
||||
if extType == 0x01 and self.checkLevel >= CHECK_READ_ALL_TEXT: # Plain Text Block
|
||||
self._read(fh, gifPlaintextStruct.size)
|
||||
self.otherText = self.otherText or []
|
||||
blkSize = self._read(fh, 1)
|
||||
while blkSize and blkSize != '\x00':
|
||||
self.otherText.append(self._read(fh, ord(blkSize)))
|
||||
blkSize = self._read(fh, 1)
|
||||
elif extType == 0xFE and self.checkLevel >= CHECK_READ_COMMENTS: # Comment Block
|
||||
self.comments = self.comments or []
|
||||
blkSize = self._read(fh, 1)
|
||||
while blkSize and blkSize != '\x00':
|
||||
self.comments.append(self._read(fh, ord(blkSize)))
|
||||
blkSize = self._read(fh, 1)
|
||||
elif extType == 0xFF: # Application Block
|
||||
if blkSize == 0x0B and self._read(fh, blkSize) == "NETSCAPE2.0":
|
||||
try:
|
||||
a, b, self.loopCount = gifNetscapeStruct.unpack(self._read(fh, gifNetscapeStruct.size))
|
||||
except:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF | WARN_TRUNC
|
||||
return
|
||||
|
||||
if a != 3 and b != 1:
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_EXT
|
||||
|
||||
if not self.firstBlock:
|
||||
self.warnFlags = self.warnFlags | WARN_LOOP_POS
|
||||
else:
|
||||
fh.seek( startOffset + blkSize ) # Skip the contents
|
||||
|
||||
# Test for the block terminator
|
||||
if not self._skipSubBlocks(fh):
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_EXT
|
||||
|
||||
def _getPalette(self, handle, bitfield):
|
||||
"""Using the size value from C{bitfield},
|
||||
load the palette at C{handle}'s current file pointer position."""
|
||||
if bitfield & int("10000000", 2):
|
||||
nBits = bitfield & int("00000111", 2)
|
||||
tableSize = 3 * 2**( nBits + 1 )
|
||||
return handle.read(tableSize)
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _read(self, handle, size):
|
||||
"""Attempt to read the specified number of bytes. Set L{WARN_EOF} if
|
||||
fewer are received."""
|
||||
content = handle.read(size)
|
||||
if len(content) < size:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF
|
||||
return content
|
||||
|
||||
def _skipSubBlocks(self, handle):
|
||||
"""Skip sub-blocks beginning at the current file pointer position
|
||||
using fseek."""
|
||||
offset = handle.tell()
|
||||
blkSize = handle.read(1)
|
||||
while blkSize and blkSize != '\x00':
|
||||
offset += ord(blkSize) + 1
|
||||
handle.seek(offset)
|
||||
blkSize = handle.read(1)
|
||||
return blkSize
|
||||
|
||||
_blockHandlers = {
|
||||
chr(0x2C) : _handleImageBlock,
|
||||
chr(0x21) : _handleGenericExtensionBlock,
|
||||
}
|
||||
|
||||
def gif_is_animated(path):
|
||||
"""A simple convenience function for testing whether a GIF is animated.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return GifInfo(file(path,'rb'), CHECK_IS_ANIMATED).frameCount > 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
from optparse import OptionParser
|
||||
parser = OptionParser(description=__doc__.split('\n\n')[0],
|
||||
version="%%prog v%s" % __version__, usage="%prog <path> ...")
|
||||
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
if args:
|
||||
for fpath in args:
|
||||
try:
|
||||
info = GifInfo(fpath, CHECK_COUNT_FRAMES)
|
||||
warnFlags = (
|
||||
(info.warnFlags & WARN_BAD_IMG and 'I' or ' ') +
|
||||
(info.warnFlags & WARN_BAD_EXT and 'X' or ' ') +
|
||||
(info.warnFlags & WARN_BAD_SIZE and 'C' or ' ') +
|
||||
(info.warnFlags & WARN_BAD_BGCOLOR and 'B' or ' ') +
|
||||
(info.warnFlags & WARN_EOF and 'E' or ' ') +
|
||||
(info.warnFlags & WARN_TRUNC and 'T' or ' ') +
|
||||
(info.warnFlags & WARN_LOOP_POS and 'L' or ' ')
|
||||
)
|
||||
print "[%s](%3s Frames): %s" % (warnFlags, info.frameCount, info.path)
|
||||
except BadHeaderException, err:
|
||||
print "%s: %s" % (str(err), fpath)
|
||||
print "\nWarning Flags:"
|
||||
print " I = Image Chunk Corruption/Truncation"
|
||||
print " X = Extension Chunk Corruption/Truncation"
|
||||
print " C = Image Chunk Dimensions Exceed Global Canvas"
|
||||
print " B = Bad Background Color (Index Exceeds Palette Size)"
|
||||
print " E = Unexpected EOF Encountered (Missing Image Terminator)"
|
||||
print " T = EOF Encountered Within A Block Header (Corrupt or Truncated File)"
|
||||
print " L = Loop-control block misplaced within the file"
|
||||
print
|
||||
print "Note: A nearly-threefold speed-up can be had by using CHECK_IS_ANIMATED rather than CHECK_COUNT_FRAMES"
|
||||
+2
-2
@@ -25,7 +25,7 @@ if __name__=="__main__":
|
||||
exclude=['*.pyc','*~','*.xcf','*[0-9].png','*.po','*.pot','*default.mo','*Thumbs.db']
|
||||
|
||||
os.chdir('calibre-plugin')
|
||||
files=['plugin-defaults.ini','plugin-example.ini','about.txt',
|
||||
files=['plugin-defaults.ini','plugin-example.ini','about.html',
|
||||
'images','translations']
|
||||
files.extend(glob('*.py'))
|
||||
files.extend(glob('plugin-import-name-*.txt'))
|
||||
@@ -36,7 +36,7 @@ if __name__=="__main__":
|
||||
|
||||
os.chdir('../included_dependencies')
|
||||
# 'a' for append
|
||||
files=['six.py','bs4','html5lib','chardet']
|
||||
files=['gif.py','six.py','bs4','html5lib','chardet']
|
||||
createZipFile("../"+filename,"a",
|
||||
files,
|
||||
exclude=exclude)
|
||||
|
||||
@@ -25,7 +25,7 @@ setup(
|
||||
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||
# the version across setup.py and the project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version="2.2.0.b1",
|
||||
version="2.2.5",
|
||||
|
||||
description='A tool for downloading fanfiction to eBook formats',
|
||||
long_description=long_description,
|
||||
@@ -46,7 +46,7 @@ setup(
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 4 - Beta',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
|
||||
'Environment :: Console',
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader fanficfare
|
||||
application: fanficfare
|
||||
version: 2-2-0a1
|
||||
version: 2-2-5
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
+15
-14
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="/css/index.css" rel="stylesheet" type="text/css">
|
||||
<title>FanFicFare - read Fanfiction from twilighted.net, fanfiction.net, fictionpress.com, fictionalley.org, ficwad.com, potionsandsnitches.net, harrypotterfanfiction.com, mediaminer.org on Kindle, Nook, Sony Reader, iPad, iPhone, Android, Aldiko, Stanza</title>
|
||||
<title>FanFicFare - read Fanfiction from fanfiction.net, archiveofourown.org, fimfiction.net, fictionpress.com, fictionalley.org, ficwad.com, potionsandsnitches.net, harrypotterfanfiction.com, mediaminer.org and many others on Kindle, Nook, Sony Reader, iPad, iPhone, Android, Aldiko, Stanza, etc</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag -->
|
||||
@@ -27,28 +27,29 @@
|
||||
much easier. </p>
|
||||
</div>
|
||||
|
||||
<h3>Changes:</h3>
|
||||
<h3>New Name and URL</h3>
|
||||
<p>
|
||||
This version changes the name of the project from
|
||||
FanFictionDownLoader(FFDL) to FanFicFare(FFF). Along with
|
||||
FanFictionDownLoader(FFDL) to FanFicFare. Along with
|
||||
the name change, there are changes to the internal
|
||||
structuring of the project code, but not to the functionality.
|
||||
</p>
|
||||
<p>
|
||||
If you had saved configuration (including your
|
||||
username/password for fic sites) in FFDL, you will need to
|
||||
copy it over to FFF. Visit
|
||||
copy it over to FanFicFare. Visit
|
||||
the <a href="http://fanfictiondownloader.appspot.com/editconfig">FFDL
|
||||
User Config</a> page, copy your settings and then paste
|
||||
them on
|
||||
the <a href="http://fanficfare.appspot.com/editconfig">FFF
|
||||
the <a href="http://fanficfare.appspot.com/editconfig">FanFicFare
|
||||
User Config</a> page.
|
||||
</p>
|
||||
<!-- ul>
|
||||
<li>Add devianthearts.com as a Base eFiction site.</li>
|
||||
<li>Allow https URLs for fictionmania.tv.</li>
|
||||
<li>Allow storiesofarda.com/chapterAllview.asp?SID=999 urls.</li>
|
||||
</ul -->
|
||||
<h3>Changes:</h3>
|
||||
<ul>
|
||||
<li>Fix ficwad.com changing author URLs.</li>
|
||||
<li>Fix for ponyfictionarchive.net stories with neither In-Progress or Complete flag.</li>
|
||||
<li>Fix for grangerenchanted.com bad HTML summary causing crash.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="https://github.com/JimmXinu/FanFicFare/wiki/FAQs">FAQs</a>.
|
||||
@@ -57,8 +58,8 @@
|
||||
If you have any problems with this application, please
|
||||
report them in
|
||||
the <a href="http://groups.google.com/group/fanfic-downloader">FanFicFare Google Group</a>. The
|
||||
<a href="http://fanfictiondownloader.appspot.com">FanFictionDownLoader(FFDL)
|
||||
Version</a> is also available for you to use if necessary.
|
||||
<a href="http://2-2-4.fanficfare.appspot.com">previous version
|
||||
</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
@@ -119,7 +120,7 @@
|
||||
Once you have calibre installed and running, inside
|
||||
calibre, you can go to 'Get plugins to enhance calibre' or
|
||||
'Get new plugins' and
|
||||
install <a href="http://www.mobileread.com/forums/showthread.php?t=163261">FanFicFare</a>.
|
||||
install <a href="http://www.mobileread.com/forums/showthread.php?t=259221">FanFicFare</a>.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
@@ -127,7 +128,7 @@
|
||||
<h3>Supported sites:</h3>
|
||||
<p>
|
||||
There's a
|
||||
<a href="https://github.com/JimmXinu/FanFicFare/Supportedsites">Supported
|
||||
<a href="https://github.com/JimmXinu/FanFicFare/wiki/Supportedsites">Supported
|
||||
Sites</a> page in our wiki. If you have a site you'd like
|
||||
to see supported, please check there first.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user