mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-14 11:14:10 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b4332da8 | ||
|
|
a1bd9c8379 | ||
|
|
e02c969371 | ||
|
|
b18b2177b0 | ||
|
|
5ca33837d4 | ||
|
|
eade66f513 | ||
|
|
28c4557d22 | ||
|
|
d6eda82767 | ||
|
|
28eff8ac12 | ||
|
|
7ae40e539d | ||
|
|
ab6436ca0b | ||
|
|
b5a04b0f97 | ||
|
|
c7bbb765b2 | ||
|
|
8a84043d29 | ||
|
|
f115d68e52 | ||
|
|
e2707f5459 | ||
|
|
aa85efd6d9 | ||
|
|
9e2c0a3563 | ||
|
|
d4f3fee053 | ||
|
|
e87c7b7009 | ||
|
|
5b6228166c | ||
|
|
f91092d9d8 | ||
|
|
a40383bada | ||
|
|
c9205dd6bc | ||
|
|
a0acbb8893 | ||
|
|
7d66d93b70 | ||
|
|
277b1ef92d | ||
|
|
72217423c7 | ||
|
|
99e4ea9a59 | ||
|
|
f1bb729b33 | ||
|
|
26fe5f42ef | ||
|
|
5f8e75d4c1 | ||
|
|
6dcfad0832 | ||
|
|
b11e06e697 | ||
|
|
63515d1366 | ||
|
|
cfcba87c32 | ||
|
|
709da65ea2 | ||
|
|
a21b555255 | ||
|
|
f25b46d268 | ||
|
|
6e2635a110 | ||
|
|
ab44185cfa | ||
|
|
c21ad04a9c | ||
|
|
314baf038f | ||
|
|
860da1208e | ||
|
|
fc477034e5 | ||
|
|
51542d4e31 | ||
|
|
fbabf92861 | ||
|
|
a00becc3f8 | ||
|
|
d503d4914b | ||
|
|
b6bd88c4b2 | ||
|
|
c4153ac59b | ||
|
|
dce0b5a335 | ||
|
|
d4291f2cd6 | ||
|
|
43637fa67b |
@@ -48,7 +48,7 @@ class FanFicFareBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (2, 3, 6)
|
||||
version = (2, 5, 3)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
@@ -116,22 +116,23 @@ class FanFicFareBase(InterfaceActionBase):
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.cli import main as fff_main
|
||||
from calibre_plugins.fanficfare_plugin.prefs import PrefsFacade
|
||||
from calibre.utils.config import prefs as calibre_prefs
|
||||
from optparse import OptionParser
|
||||
|
||||
from optparse import OptionParser
|
||||
|
||||
parser = OptionParser('%prog --run-plugin '+self.name+' -- [options] <storyurl>')
|
||||
parser.add_option('--library-path', '--with-library', default=None, help=_('Path to the calibre library. Default is to use the path stored in the settings.'))
|
||||
# parser.add_option('--dont-notify-gui', default=False, action='store_true',
|
||||
# help=_('Do not notify the running calibre GUI (if any) that the database has'
|
||||
# ' changed. Use with care, as it can lead to database corruption!'))
|
||||
|
||||
|
||||
pargs = [x for x in argv if x.startswith('--with-library') or x.startswith('--library-path')
|
||||
or not x.startswith('-')]
|
||||
opts, args = parser.parse_args(pargs)
|
||||
|
||||
|
||||
fff_prefs = PrefsFacade(db(path=opts.library_path,
|
||||
read_only=True))
|
||||
|
||||
fff_main(argv[1:],
|
||||
parser=parser,
|
||||
passed_defaultsini=StringIO(get_resources("fanficfare/defaults.ini")),
|
||||
passed_personalini=StringIO(fff_prefs["personal.ini"]))
|
||||
parser=parser,
|
||||
passed_defaultsini=StringIO(get_resources("fanficfare/defaults.ini")),
|
||||
passed_personalini=StringIO(fff_prefs["personal.ini"]),
|
||||
)
|
||||
|
||||
+137
-119
@@ -14,11 +14,13 @@ import traceback, copy, threading, re
|
||||
from collections import OrderedDict
|
||||
|
||||
try:
|
||||
from PyQt5 import QtWidgets as QtGui
|
||||
from PyQt5.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QLabel, QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
QCheckBox, QPushButton, QTabWidget, QScrollArea,
|
||||
QDialogButtonBox, QGroupBox, QButtonGroup, QRadioButton, Qt)
|
||||
except ImportError as e:
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QLabel, QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
QCheckBox, QPushButton, QTabWidget, QScrollArea,
|
||||
@@ -87,7 +89,7 @@ from calibre_plugins.fanficfare_plugin.prefs \
|
||||
from calibre_plugins.fanficfare_plugin.dialogs \
|
||||
import (UPDATE, UPDATEALWAYS, collision_order, save_collisions, RejectListDialog,
|
||||
EditTextDialog, IniTextDialog, RejectUrlEntry)
|
||||
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.adapters \
|
||||
import getSiteSections
|
||||
|
||||
@@ -112,7 +114,7 @@ class RejectURLList:
|
||||
#print("rue.url:%s"%rue.url)
|
||||
if rue.valid:
|
||||
cache[rue.url] = rue
|
||||
return cache
|
||||
return cache
|
||||
|
||||
def _get_listcache(self):
|
||||
if self.listcache == None:
|
||||
@@ -124,7 +126,7 @@ class RejectURLList:
|
||||
self.prefs['rejecturls'] = '\n'.join([x.to_line() for x in listcache.values()])
|
||||
self.prefs.save_to_db()
|
||||
self.listcache = None
|
||||
|
||||
|
||||
def clear_cache(self):
|
||||
self.listcache = None
|
||||
|
||||
@@ -133,7 +135,7 @@ class RejectURLList:
|
||||
with self.sync_lock:
|
||||
listcache = self._get_listcache()
|
||||
return url in listcache
|
||||
|
||||
|
||||
def get_note(self,url):
|
||||
with self.sync_lock:
|
||||
listcache = self._get_listcache()
|
||||
@@ -159,7 +161,7 @@ class RejectURLList:
|
||||
|
||||
def add_text(self,rejecttext,addreasontext):
|
||||
self.add(self._read_list_from_text(rejecttext,addreasontext).values())
|
||||
|
||||
|
||||
def add(self,rejectlist,clear=False):
|
||||
with self.sync_lock:
|
||||
if clear:
|
||||
@@ -172,7 +174,7 @@ class RejectURLList:
|
||||
|
||||
def get_list(self):
|
||||
return self._get_listcache().values()
|
||||
|
||||
|
||||
def get_reject_reasons(self):
|
||||
return self.prefs['rejectreasons'].splitlines()
|
||||
|
||||
@@ -183,7 +185,7 @@ class ConfigWidget(QWidget):
|
||||
def __init__(self, plugin_action):
|
||||
QWidget.__init__(self)
|
||||
self.plugin_action = plugin_action
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -192,7 +194,7 @@ class ConfigWidget(QWidget):
|
||||
+_('List of Supported Sites')+'</a> -- <a href="'\
|
||||
+'https://github.com/JimmXinu/FanFicFare/wiki/FAQs">'\
|
||||
+_('FAQs')+'</a>')
|
||||
|
||||
|
||||
label.setOpenExternalLinks(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
@@ -204,13 +206,13 @@ class ConfigWidget(QWidget):
|
||||
|
||||
tab_widget = QTabWidget(self)
|
||||
self.scroll_area.setWidget(tab_widget)
|
||||
|
||||
|
||||
self.basic_tab = BasicTab(self, plugin_action)
|
||||
tab_widget.addTab(self.basic_tab, _('Basic'))
|
||||
|
||||
self.personalini_tab = PersonalIniTab(self, plugin_action)
|
||||
tab_widget.addTab(self.personalini_tab, 'personal.ini')
|
||||
|
||||
|
||||
self.readinglist_tab = ReadingListTab(self, plugin_action)
|
||||
tab_widget.addTab(self.readinglist_tab, 'Reading Lists')
|
||||
if 'Reading List' not in plugin_action.gui.iactions:
|
||||
@@ -258,6 +260,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['adddialogstaysontop'] = self.basic_tab.adddialogstaysontop.isChecked()
|
||||
prefs['lookforurlinhtml'] = self.basic_tab.lookforurlinhtml.isChecked()
|
||||
prefs['checkforseriesurlid'] = self.basic_tab.checkforseriesurlid.isChecked()
|
||||
prefs['auto_reject_seriesurlid'] = self.basic_tab.auto_reject_seriesurlid.isChecked()
|
||||
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
|
||||
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
|
||||
prefs['matchtitleauth'] = self.basic_tab.matchtitleauth.isChecked()
|
||||
@@ -285,7 +288,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['personal.ini'] = get_resources('plugin-example.ini')
|
||||
|
||||
prefs['cal_cols_pass_in'] = self.personalini_tab.cal_cols_pass_in.isChecked()
|
||||
|
||||
|
||||
# Covers tab
|
||||
prefs['updatecalcover'] = prefs_save_options[unicode(self.calibrecover_tab.updatecalcover.currentText())]
|
||||
# for backward compatibility:
|
||||
@@ -306,7 +309,7 @@ class ConfigWidget(QWidget):
|
||||
|
||||
# Count Pages tab
|
||||
countpagesstats = []
|
||||
|
||||
|
||||
if self.countpages_tab.pagecount.isChecked():
|
||||
countpagesstats.append('PageCount')
|
||||
if self.countpages_tab.wordcount.isChecked():
|
||||
@@ -317,10 +320,10 @@ class ConfigWidget(QWidget):
|
||||
countpagesstats.append('FleschGrade')
|
||||
if self.countpages_tab.gunningfog.isChecked():
|
||||
countpagesstats.append('GunningFog')
|
||||
|
||||
|
||||
prefs['countpagesstats'] = countpagesstats
|
||||
prefs['wordcountmissing'] = self.countpages_tab.wordcount.isChecked() and self.countpages_tab.wordcountmissing.isChecked()
|
||||
|
||||
|
||||
# Standard Columns tab
|
||||
colsnewonly = {}
|
||||
for (col,checkbox) in self.std_columns_tab.stdcol_newonlycheck.iteritems():
|
||||
@@ -352,7 +355,7 @@ class ConfigWidget(QWidget):
|
||||
for (col,checkbox) in self.cust_columns_tab.custcol_newonlycheck.iteritems():
|
||||
colsnewonly[col] = checkbox.isChecked()
|
||||
prefs['custom_cols_newonly'] = colsnewonly
|
||||
|
||||
|
||||
prefs['allow_custcol_from_ini'] = self.cust_columns_tab.allow_custcol_from_ini.isChecked()
|
||||
|
||||
prefs['imapserver'] = unicode(self.imap_tab.imapserver.text()).strip()
|
||||
@@ -362,8 +365,9 @@ class ConfigWidget(QWidget):
|
||||
prefs['imapmarkread'] = self.imap_tab.imapmarkread.isChecked()
|
||||
prefs['imapsessionpass'] = self.imap_tab.imapsessionpass.isChecked()
|
||||
prefs['auto_reject_from_email'] = self.imap_tab.auto_reject_from_email.isChecked()
|
||||
prefs['update_existing_only_from_email'] = self.imap_tab.update_existing_only_from_email.isChecked()
|
||||
prefs['download_from_email_immediately'] = self.imap_tab.download_from_email_immediately.isChecked()
|
||||
|
||||
|
||||
prefs.save_to_db()
|
||||
|
||||
def edit_shortcuts(self):
|
||||
@@ -380,7 +384,7 @@ class BasicTab(QWidget):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
topl = QVBoxLayout()
|
||||
self.setLayout(topl)
|
||||
|
||||
@@ -435,7 +439,7 @@ class BasicTab(QWidget):
|
||||
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'])
|
||||
horz.addWidget(self.updateepubcover)
|
||||
|
||||
|
||||
self.bgmeta = QCheckBox(_('Default Background Metadata?'),self)
|
||||
self.bgmeta.setToolTip(_("On each download, FanFicFare offers an option to Collect Metadata from sites in a Background process.<br />This returns control to you quicker while updating, but you won't be asked for username/passwords or if you are an adult--stories that need those will just fail.<br />Only available for Update/Overwrite of existing books in case URL given isn't canonical or matches to existing book by Title/Author."))
|
||||
self.bgmeta.setChecked(prefs['bgmeta'])
|
||||
@@ -451,7 +455,7 @@ class BasicTab(QWidget):
|
||||
self.deleteotherforms.setToolTip(_('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.'))
|
||||
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
|
||||
self.l.addWidget(self.deleteotherforms)
|
||||
|
||||
|
||||
self.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'])
|
||||
@@ -468,10 +472,18 @@ class BasicTab(QWidget):
|
||||
self.l.addWidget(self.suppresstitlesort)
|
||||
|
||||
self.checkforseriesurlid = QCheckBox(_("Check for existing Series Anthology books?"),self)
|
||||
self.checkforseriesurlid.setToolTip(_("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.\nDoesn't work when Collect Metadata in Background is selected."))
|
||||
self.checkforseriesurlid.setToolTip(_("Check for existing Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.\nDoesn't work when Collect Metadata in Background is selected."))
|
||||
self.checkforseriesurlid.setChecked(prefs['checkforseriesurlid'])
|
||||
self.l.addWidget(self.checkforseriesurlid)
|
||||
|
||||
self.auto_reject_seriesurlid = QCheckBox(_("Reject Without Confirmation?"),self)
|
||||
self.auto_reject_seriesurlid.setToolTip(_("Automatically reject storys with existing Series Anthology books.\nOnly works if 'Check for existing Series Anthology books' is on.\nDoesn't work when Collect Metadata in Background is selected."))
|
||||
self.auto_reject_seriesurlid.setChecked(prefs['auto_reject_seriesurlid'])
|
||||
horz = QHBoxLayout()
|
||||
horz.addItem(QtGui.QSpacerItem(20, 1))
|
||||
horz.addWidget(self.auto_reject_seriesurlid)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.checkforurlchange = QCheckBox(_("Check for changed Story URL?"),self)
|
||||
self.checkforurlchange.setToolTip(_("Warn you if an update will change the URL of an existing book.\nfanfiction.net URLs will change from http to https silently."))
|
||||
self.checkforurlchange.setChecked(prefs['checkforurlchange'])
|
||||
@@ -501,7 +513,7 @@ class BasicTab(QWidget):
|
||||
self.smarten_punctuation.setChecked(prefs['smarten_punctuation'])
|
||||
self.l.addWidget(self.smarten_punctuation)
|
||||
|
||||
|
||||
|
||||
tooltip = _("Calculate Word Counts using Calibre internal methods.\n"
|
||||
"Many sites include Word Count, but many do not.\n"
|
||||
"This will count the words in each book and include it as if it came from the site.")
|
||||
@@ -518,7 +530,7 @@ class BasicTab(QWidget):
|
||||
horz.addWidget(self.do_wordcount)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
|
||||
|
||||
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'])
|
||||
@@ -570,12 +582,12 @@ class BasicTab(QWidget):
|
||||
self.rejectlist.setToolTip(_("Edit list of URLs FanFicFare will automatically Reject."))
|
||||
self.rejectlist.clicked.connect(self.show_rejectlist)
|
||||
self.l.addWidget(self.rejectlist)
|
||||
|
||||
|
||||
self.reject_urls = QPushButton(_('Add Reject URLs'), self)
|
||||
self.reject_urls.setToolTip(_("Add additional URLs to Reject as text."))
|
||||
self.reject_urls.clicked.connect(self.add_reject_urls)
|
||||
self.l.addWidget(self.reject_urls)
|
||||
|
||||
|
||||
self.reject_reasons = QPushButton(_('Edit Reject Reasons List'), self)
|
||||
self.reject_reasons.setToolTip(_("Customize the Reasons presented when Rejecting URLs"))
|
||||
self.reject_reasons.clicked.connect(self.show_reject_reasons)
|
||||
@@ -598,13 +610,13 @@ class BasicTab(QWidget):
|
||||
vertright.addWidget(gui_gb)
|
||||
vertright.addWidget(misc_gb)
|
||||
vertright.addWidget(rej_gb)
|
||||
|
||||
|
||||
horz.addLayout(vertleft)
|
||||
horz.addLayout(vertright)
|
||||
|
||||
|
||||
topl.addLayout(horz)
|
||||
topl.insertStretch(-1)
|
||||
|
||||
|
||||
def set_collisions(self):
|
||||
prev=self.collision.currentText()
|
||||
self.collision.clear()
|
||||
@@ -614,7 +626,7 @@ class BasicTab(QWidget):
|
||||
i = self.collision.findText(prev)
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
|
||||
def show_rejectlist(self):
|
||||
d = RejectListDialog(self,
|
||||
rejecturllist.get_list(),
|
||||
@@ -623,12 +635,12 @@ class BasicTab(QWidget):
|
||||
show_delete=False,
|
||||
show_all_reasons=False)
|
||||
d.exec_()
|
||||
|
||||
|
||||
if d.result() != d.Accepted:
|
||||
return
|
||||
|
||||
|
||||
rejecturllist.add(d.get_reject_list(),clear=True)
|
||||
|
||||
|
||||
def show_reject_reasons(self):
|
||||
d = EditTextDialog(self,
|
||||
prefs['rejectreasons'],
|
||||
@@ -640,7 +652,7 @@ class BasicTab(QWidget):
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
prefs['rejectreasons'] = d.get_plain_text()
|
||||
|
||||
|
||||
def add_reject_urls(self):
|
||||
d = EditTextDialog(self,
|
||||
"http://example.com/story.php?sid=5,"+_("Reason why I rejected it")+"\nhttp://example.com/story.php?sid=6,"+_("Title by Author")+" - "+_("Reason why I rejected it"),
|
||||
@@ -654,14 +666,14 @@ class BasicTab(QWidget):
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
rejecturllist.add_text(d.get_plain_text(),d.get_reason_text())
|
||||
|
||||
|
||||
class PersonalIniTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -678,16 +690,16 @@ class PersonalIniTab(QWidget):
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
vert.addLayout(horz)
|
||||
self.ini_button = QPushButton(_('Edit personal.ini'), self)
|
||||
#self.ini_button.setToolTip(_("Edit personal.ini file."))
|
||||
self.ini_button.clicked.connect(self.add_ini_button)
|
||||
horz.addWidget(self.ini_button)
|
||||
|
||||
|
||||
label = QLabel(_("FanFicFare now includes find, color coding, and error checking for personal.ini editing. Red generally indicates errors."))
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
|
||||
vert.addSpacing(5)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
@@ -696,18 +708,18 @@ class PersonalIniTab(QWidget):
|
||||
#self.ini_button.setToolTip(_("Edit personal.ini file."))
|
||||
self.ini_button.clicked.connect(self.safe_ini_button)
|
||||
horz.addWidget(self.ini_button)
|
||||
|
||||
|
||||
label = QLabel(_("View your personal.ini with usernames and passwords removed. For safely sharing your personal.ini settings with others."))
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
groupbox = QGroupBox(_("defaults.ini"))
|
||||
horz = QHBoxLayout()
|
||||
groupbox.setLayout(horz)
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
|
||||
view_label = _("View all of the plugin's configurable settings\nand their default settings.")
|
||||
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', self)
|
||||
self.defaults.setToolTip(view_label)
|
||||
@@ -717,7 +729,7 @@ class PersonalIniTab(QWidget):
|
||||
label = QLabel(view_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
|
||||
self.l.addSpacing(5)
|
||||
|
||||
groupbox = QGroupBox(_("Calibre Columns"))
|
||||
@@ -732,13 +744,13 @@ class PersonalIniTab(QWidget):
|
||||
self.cal_cols_pass_in.setToolTip(pass_label)
|
||||
self.cal_cols_pass_in.setChecked(prefs['cal_cols_pass_in'])
|
||||
horz.addWidget(self.cal_cols_pass_in)
|
||||
|
||||
|
||||
label = QLabel(pass_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
vert.addSpacing(5)
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
col_label = _("FanFicFare can pass the Calibre Columns into the download/update process.<br>This will show you the columns available by name.")
|
||||
@@ -756,7 +768,7 @@ class PersonalIniTab(QWidget):
|
||||
self.l.addWidget(label)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
|
||||
def show_defaults(self):
|
||||
IniTextDialog(self,
|
||||
get_resources('plugin-defaults.ini'),
|
||||
@@ -766,10 +778,10 @@ class PersonalIniTab(QWidget):
|
||||
use_find=True,
|
||||
read_only=True,
|
||||
save_size_name='fff:defaults.ini').exec_()
|
||||
|
||||
|
||||
def safe_ini_button(self):
|
||||
personalini = re.sub(r'((username|password) *[=:]).*$',r'\1XXXXXXXX',self.personalini,flags=re.MULTILINE)
|
||||
|
||||
|
||||
d = EditTextDialog(self,
|
||||
personalini,
|
||||
icon=self.windowIcon(),
|
||||
@@ -778,7 +790,7 @@ class PersonalIniTab(QWidget):
|
||||
save_size_name='fff:safe personal.ini',
|
||||
read_only=True)
|
||||
d.exec_()
|
||||
|
||||
|
||||
def add_ini_button(self):
|
||||
d = IniTextDialog(self,
|
||||
self.personalini,
|
||||
@@ -790,13 +802,13 @@ class PersonalIniTab(QWidget):
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
self.personalini = d.get_plain_text()
|
||||
|
||||
|
||||
def show_showcalcols(self):
|
||||
lines=[]#[('calibre_std_user_categories',_('User Categories'))]
|
||||
for k,f in field_metadata.iteritems():
|
||||
if f['name'] and k not in STD_COLS_SKIP: # only if it has a human readable name.
|
||||
lines.append(('calibre_std_'+k,f['name']))
|
||||
|
||||
|
||||
for k, column in self.plugin_action.gui.library_view.model().custom_columns.iteritems():
|
||||
if k != prefs['savemetacol']:
|
||||
# custom always have name.
|
||||
@@ -811,14 +823,14 @@ class PersonalIniTab(QWidget):
|
||||
label=_('Label (entry_name)'),
|
||||
read_only=True,
|
||||
save_size_name='fff:showcalcols').exec_()
|
||||
|
||||
|
||||
class ReadingListTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -827,21 +839,21 @@ class ReadingListTab(QWidget):
|
||||
reading_lists = rl_plugin.get_list_names()
|
||||
except KeyError:
|
||||
reading_lists= []
|
||||
|
||||
|
||||
label = QLabel(_('These settings provide integration with the %(rl)s Plugin. %(rl)s can automatically send to devices and change custom columns. You have to create and configure the lists in %(rl)s to be useful.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
self.addtolists = QCheckBox(_('Add new/updated stories to "Send to Device" Reading List(s).'),self)
|
||||
self.addtolists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.')%no_trans)
|
||||
self.addtolists.setChecked(prefs['addtolists'])
|
||||
self.l.addWidget(self.addtolists)
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('"Send to Device" Reading Lists'))
|
||||
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
horz.addWidget(label)
|
||||
horz.addWidget(label)
|
||||
self.send_lists_box = EditWithComplete(self)
|
||||
self.send_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
self.send_lists_box.update_items_cache(reading_lists)
|
||||
@@ -849,16 +861,16 @@ class ReadingListTab(QWidget):
|
||||
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)
|
||||
self.addtoreadlists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.\nAlso offers menu option to remove stories from the "To Read" lists.')%no_trans)
|
||||
self.addtoreadlists.setChecked(prefs['addtoreadlists'])
|
||||
self.l.addWidget(self.addtoreadlists)
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('"To Read" Reading Lists'))
|
||||
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
horz.addWidget(label)
|
||||
horz.addWidget(label)
|
||||
self.read_lists_box = EditWithComplete(self)
|
||||
self.read_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
self.read_lists_box.update_items_cache(reading_lists)
|
||||
@@ -866,31 +878,31 @@ class ReadingListTab(QWidget):
|
||||
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)
|
||||
self.addtolistsonread.setToolTip(_('Menu option to remove from "To Read" lists will also add stories back to "Send to Device" Reading List(s)'))
|
||||
self.addtolistsonread.setChecked(prefs['addtolistsonread'])
|
||||
self.l.addWidget(self.addtolistsonread)
|
||||
|
||||
|
||||
self.autounnew = QCheckBox(_('Automatically run Remove "New" Chapter Marks when marking books "Read".'),self)
|
||||
self.autounnew.setToolTip(_('Menu option to remove from "To Read" lists will also remove "(new)" chapter marks created by personal.ini <i>mark_new_chapters</i> setting.'))
|
||||
self.autounnew.setChecked(prefs['autounnew'])
|
||||
self.l.addWidget(self.autounnew)
|
||||
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
|
||||
class CalibreCoverTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -900,7 +912,7 @@ class CalibreCoverTab(QWidget):
|
||||
except KeyError:
|
||||
gc_settings= []
|
||||
|
||||
|
||||
|
||||
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"
|
||||
@@ -908,7 +920,7 @@ class CalibreCoverTab(QWidget):
|
||||
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"
|
||||
@@ -949,7 +961,7 @@ class CalibreCoverTab(QWidget):
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[SAVE_YES]))
|
||||
# else: # doesn't have own value, old value not set, NO.
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[SAVE_NO]))
|
||||
|
||||
|
||||
self.gencalcover.setToolTip(tooltip)
|
||||
label.setBuddy(self.gencalcover)
|
||||
horz.addWidget(self.gencalcover)
|
||||
@@ -962,7 +974,7 @@ class CalibreCoverTab(QWidget):
|
||||
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)
|
||||
@@ -1007,7 +1019,7 @@ class CalibreCoverTab(QWidget):
|
||||
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)
|
||||
@@ -1021,7 +1033,7 @@ class CalibreCoverTab(QWidget):
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
|
||||
self.gc_dropdowns = {}
|
||||
|
||||
sitelist = getSiteSections()
|
||||
@@ -1054,9 +1066,9 @@ class CalibreCoverTab(QWidget):
|
||||
|
||||
horz.addWidget(dropdown)
|
||||
self.sl.addLayout(horz)
|
||||
|
||||
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
|
||||
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)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)
|
||||
@@ -1070,7 +1082,7 @@ class CalibreCoverTab(QWidget):
|
||||
"Clearing house function for setting elements of Calibre"
|
||||
"Cover tab enabled/disabled depending on all factors."
|
||||
|
||||
## First, cover gen on/off
|
||||
## First, cover gen on/off
|
||||
for e in self.gencov_elements:
|
||||
e.setEnabled(prefs_save_options[unicode(self.gencalcover.currentText())] != SAVE_NO)
|
||||
|
||||
@@ -1084,15 +1096,15 @@ class CalibreCoverTab(QWidget):
|
||||
if not 'Generate Cover' in self.plugin_action.gui.iactions:
|
||||
self.plugin_gen_cover.setEnabled(False)
|
||||
self.gcp_gb.setEnabled(False)
|
||||
|
||||
|
||||
|
||||
|
||||
class CountPagesTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -1115,7 +1127,7 @@ class CountPagesTab(QWidget):
|
||||
self.l.addWidget(self.pagecount)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
|
||||
|
||||
self.wordcount = QCheckBox('Word Count',self)
|
||||
self.wordcount.setToolTip(tooltip+"\n"+_('Will overwrite word count from FanFicFare metadata if set to update the same custom column.'))
|
||||
self.wordcount.setChecked('WordCount' in prefs['countpagesstats'])
|
||||
@@ -1128,33 +1140,33 @@ class CountPagesTab(QWidget):
|
||||
horz.addWidget(self.wordcountmissing)
|
||||
|
||||
self.wordcount.stateChanged.connect(lambda x : self.wordcountmissing.setEnabled(self.wordcount.isChecked()))
|
||||
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.fleschreading = QCheckBox('Flesch Reading Ease',self)
|
||||
self.fleschreading.setToolTip(tooltip)
|
||||
self.fleschreading.setChecked('FleschReading' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.fleschreading)
|
||||
|
||||
|
||||
self.fleschgrade = QCheckBox('Flesch-Kincaid Grade Level',self)
|
||||
self.fleschgrade.setToolTip(tooltip)
|
||||
self.fleschgrade.setChecked('FleschGrade' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.fleschgrade)
|
||||
|
||||
|
||||
self.gunningfog = QCheckBox('Gunning Fog Index',self)
|
||||
self.gunningfog.setToolTip(tooltip)
|
||||
self.gunningfog.setChecked('GunningFog' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.gunningfog)
|
||||
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
|
||||
class OtherTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -1162,7 +1174,7 @@ class OtherTab(QWidget):
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
keyboard_shortcuts_button = QPushButton(_('Keyboard shortcuts...'), self)
|
||||
keyboard_shortcuts_button.setToolTip(_('Edit the keyboard shortcuts associated with this plugin'))
|
||||
keyboard_shortcuts_button.clicked.connect(parent_dialog.edit_shortcuts)
|
||||
@@ -1172,14 +1184,14 @@ class OtherTab(QWidget):
|
||||
reset_confirmation_button.setToolTip(_('Reset all show me again dialogs for the FanFicFare plugin'))
|
||||
reset_confirmation_button.clicked.connect(self.reset_dialogs)
|
||||
self.l.addWidget(reset_confirmation_button)
|
||||
|
||||
|
||||
view_prefs_button = QPushButton(_('&View library preferences...'), self)
|
||||
view_prefs_button.setToolTip(_('View data stored in the library database for this plugin'))
|
||||
view_prefs_button.clicked.connect(self.view_prefs)
|
||||
self.l.addWidget(view_prefs_button)
|
||||
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
|
||||
def reset_dialogs(self):
|
||||
for key in dynamic.keys():
|
||||
if key.startswith('fanfictiondownloader_') and key.endswith('_again') \
|
||||
@@ -1189,7 +1201,7 @@ class OtherTab(QWidget):
|
||||
_('Confirmation dialogs have all been reset'),
|
||||
show=True,
|
||||
show_copy_button=False)
|
||||
|
||||
|
||||
def view_prefs(self):
|
||||
d = PrefsViewerDialog(self.plugin_action.gui, PREFS_NAMESPACE)
|
||||
d.exec_()
|
||||
@@ -1271,7 +1283,7 @@ class CustomColumnsTab(QWidget):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
custom_columns = self.plugin_action.gui.library_view.model().custom_columns
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
@@ -1281,7 +1293,7 @@ class CustomColumnsTab(QWidget):
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
self.custcol_dropdowns = {}
|
||||
self.custcol_newonlycheck = {}
|
||||
|
||||
@@ -1293,7 +1305,7 @@ class CustomColumnsTab(QWidget):
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
|
||||
for key, column in custom_columns.iteritems():
|
||||
|
||||
if column['datatype'] in permitted_values:
|
||||
@@ -1323,9 +1335,9 @@ class CustomColumnsTab(QWidget):
|
||||
if key in prefs['custom_cols_newonly']:
|
||||
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
|
||||
horz.addWidget(newonlycheck)
|
||||
|
||||
|
||||
self.sl.addLayout(horz)
|
||||
|
||||
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
@@ -1333,11 +1345,11 @@ class CustomColumnsTab(QWidget):
|
||||
self.allow_custcol_from_ini.setToolTip(_("The %(pini)s parameter %(ccset)s allows you to set custom columns to site specific values that aren't common to all sites.<br />%(ccset)s is ignored when this is off.")%no_trans)
|
||||
self.allow_custcol_from_ini.setChecked(prefs['allow_custcol_from_ini'])
|
||||
self.l.addWidget(self.allow_custcol_from_ini)
|
||||
|
||||
|
||||
label = QLabel(_("Special column:"))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_("Update/Overwrite Error Column:"))
|
||||
tooltip=_("When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)")
|
||||
@@ -1352,7 +1364,7 @@ class CustomColumnsTab(QWidget):
|
||||
self.errorcol.addItem(column['name'],key)
|
||||
self.errorcol.setCurrentIndex(self.errorcol.findData(prefs['errorcol']))
|
||||
horz.addWidget(self.errorcol)
|
||||
|
||||
|
||||
self.save_all_errors = QCheckBox(_('Save All Errors'),self)
|
||||
self.save_all_errors.setToolTip(_('If unchecked, these errors will not be saved:%s')%(
|
||||
'\n'+
|
||||
@@ -1360,9 +1372,9 @@ class CustomColumnsTab(QWidget):
|
||||
_("Already contains %d chapters.").replace('%d','X')))))
|
||||
self.save_all_errors.setChecked(prefs['save_all_errors'])
|
||||
horz.addWidget(self.save_all_errors)
|
||||
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_("Saved Metadata Column:"))
|
||||
tooltip=_("If set, FanFicFare will save a copy of all its metadata in this column when the book is downloaded or updated.<br/>The metadata from this column can later be used to update custom columns without having to request the metadata from the server again.<br/>(Long Text columns only.)")
|
||||
@@ -1379,7 +1391,7 @@ class CustomColumnsTab(QWidget):
|
||||
|
||||
label = QLabel('')
|
||||
horz.addWidget(label) # empty spacer for alignment with error column line.
|
||||
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
#print("prefs['custom_cols'] %s"%prefs['custom_cols'])
|
||||
@@ -1393,7 +1405,7 @@ class StandardColumnsTab(QWidget):
|
||||
QWidget.__init__(self)
|
||||
|
||||
columns=OrderedDict()
|
||||
|
||||
|
||||
columns["title"]=_("Title")
|
||||
columns["authors"]=_("Author(s)")
|
||||
columns["publisher"]=_("Publisher")
|
||||
@@ -1412,7 +1424,7 @@ class StandardColumnsTab(QWidget):
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
self.stdcol_newonlycheck = {}
|
||||
|
||||
for key, column in columns.iteritems():
|
||||
@@ -1427,9 +1439,9 @@ class StandardColumnsTab(QWidget):
|
||||
if key in prefs['std_cols_newonly']:
|
||||
newonlycheck.setChecked(prefs['std_cols_newonly'][key])
|
||||
horz.addWidget(newonlycheck)
|
||||
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
|
||||
self.l.addSpacing(5)
|
||||
label = QLabel(_("Other Standard Column Options"))
|
||||
label.setWordWrap(True)
|
||||
@@ -1451,7 +1463,7 @@ Default is a list of included titles only.'''))
|
||||
self.anth_comments_newonly.setToolTip(_("Comments will only be set for New Anthologies, not updates.\nThat way comments you set manually are retained."))
|
||||
self.anth_comments_newonly.setChecked(prefs['anth_comments_newonly'])
|
||||
self.l.addWidget(self.anth_comments_newonly)
|
||||
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
class ImapTab(QWidget):
|
||||
@@ -1460,11 +1472,11 @@ class ImapTab(QWidget):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
self.l = QGridLayout()
|
||||
self.setLayout(self.l)
|
||||
row=0
|
||||
|
||||
|
||||
label = QLabel(_('These settings will allow FanFicFare to fetch story URLs from your email account. It will only look for story URLs in unread emails in the folder specified below.'))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label,row,0,1,-1)
|
||||
@@ -1479,21 +1491,21 @@ class ImapTab(QWidget):
|
||||
self.imapserver.setText(prefs['imapserver'])
|
||||
self.l.addWidget(self.imapserver,row,1)
|
||||
row+=1
|
||||
|
||||
|
||||
label = QLabel(_('IMAP User Name'))
|
||||
tooltip = _("Name of IMAP user. Eg: yourname@gmail.com\nNote that Gmail accounts need to have IMAP enabled in Gmail Settings first.")
|
||||
label.setToolTip(tooltip)
|
||||
self.l.addWidget(label,row,0)
|
||||
self.l.addWidget(label,row,0)
|
||||
self.imapuser = QLineEdit(self)
|
||||
self.imapuser.setToolTip(tooltip)
|
||||
self.imapuser.setText(prefs['imapuser'])
|
||||
self.l.addWidget(self.imapuser,row,1)
|
||||
row+=1
|
||||
|
||||
|
||||
label = QLabel(_('IMAP User Password'))
|
||||
tooltip = _("IMAP password. If left empty, FanFicFare will ask you for your password when you use the feature.")
|
||||
label.setToolTip(tooltip)
|
||||
self.l.addWidget(label,row,0)
|
||||
self.l.addWidget(label,row,0)
|
||||
self.imappass = QLineEdit(self)
|
||||
self.imappass.setToolTip(tooltip)
|
||||
self.imappass.setEchoMode(QLineEdit.Password)
|
||||
@@ -1506,35 +1518,41 @@ class ImapTab(QWidget):
|
||||
self.imapsessionpass.setChecked(prefs['imapsessionpass'])
|
||||
self.l.addWidget(self.imapsessionpass,row,0,1,-1)
|
||||
row+=1
|
||||
|
||||
|
||||
label = QLabel(_('IMAP Folder Name'))
|
||||
tooltip = _("Name of IMAP folder to search for new emails. The folder (or label) has to already exist. Use INBOX for your default inbox.")
|
||||
label.setToolTip(tooltip)
|
||||
self.l.addWidget(label,row,0)
|
||||
self.l.addWidget(label,row,0)
|
||||
self.imapfolder = QLineEdit(self)
|
||||
self.imapfolder.setToolTip(tooltip)
|
||||
self.imapfolder.setText(prefs['imapfolder'])
|
||||
self.l.addWidget(self.imapfolder,row,1)
|
||||
row+=1
|
||||
|
||||
|
||||
self.imapmarkread = QCheckBox(_('Mark Emails Read'),self)
|
||||
self.imapmarkread.setToolTip(_('If checked, emails will be marked as having been read if they contain any story URLs.'))
|
||||
self.imapmarkread.setChecked(prefs['imapmarkread'])
|
||||
self.l.addWidget(self.imapmarkread,row,0,1,-1)
|
||||
row+=1
|
||||
|
||||
|
||||
self.auto_reject_from_email = QCheckBox(_('Discard URLs on Reject List'),self)
|
||||
self.auto_reject_from_email.setToolTip(_('If checked, FanFicFare will silently discard story URLs from emails that are on your Reject URL List.<br>Otherwise they will appear and you will see the normal Reject URL dialog.<br>The Emails will still be marked Read if configured to.'))
|
||||
self.auto_reject_from_email.setChecked(prefs['auto_reject_from_email'])
|
||||
self.l.addWidget(self.auto_reject_from_email,row,0,1,-1)
|
||||
row+=1
|
||||
|
||||
|
||||
self.update_existing_only_from_email = QCheckBox(_('Update Existing Books Only'),self)
|
||||
self.update_existing_only_from_email.setToolTip(_('If checked, FanFicFare will silently discard story URLs from emails that are not already in your library.<br>Otherwise all story URLs, new and existing, will be used.<br>The Emails will still be marked Read if configured to.'))
|
||||
self.update_existing_only_from_email.setChecked(prefs['update_existing_only_from_email'])
|
||||
self.l.addWidget(self.update_existing_only_from_email,row,0,1,-1)
|
||||
row+=1
|
||||
|
||||
self.download_from_email_immediately = QCheckBox(_('Download from Email Immediately'),self)
|
||||
self.download_from_email_immediately.setToolTip(_('If checked, FanFicFare will start downloading story URLs from emails immediately.<br>Otherwise the usual Download from URLs dialog will appear.'))
|
||||
self.download_from_email_immediately.setChecked(prefs['download_from_email_immediately'])
|
||||
self.l.addWidget(self.download_from_email_immediately,row,0,1,-1)
|
||||
row+=1
|
||||
|
||||
|
||||
label = QLabel(_("<b>It's safest if you create a separate email account that you use only "
|
||||
"for your story update notices. FanFicFare and calibre cannot guarantee that "
|
||||
"malicious code cannot get your email password once you've entered it. "
|
||||
@@ -1543,5 +1561,5 @@ class ImapTab(QWidget):
|
||||
self.l.addWidget(label,row,0,1,-1,Qt.AlignTop)
|
||||
self.l.setRowStretch(row,1)
|
||||
row+=1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -484,9 +484,16 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
reject_list=set()
|
||||
if prefs['auto_reject_from_email']:
|
||||
# need to normalize for reject list.
|
||||
reject_list = set([x for x in url_list if rejecturllist.check(adapters.getNormalStoryURLSite(x)[0])])
|
||||
reject_list = set([x for x in url_list if rejecturllist.check(adapters.getNormalStoryURL(x))])
|
||||
url_list = url_list - reject_list
|
||||
|
||||
## feature for update-only - check url_list with
|
||||
## self.do_id_search(url)
|
||||
notupdate_list = set()
|
||||
if prefs['update_existing_only_from_email']:
|
||||
notupdate_list = set([x for x in url_list if not self.do_id_search(adapters.getNormalStoryURL(x))])
|
||||
url_list = url_list - notupdate_list
|
||||
|
||||
self.gui.status_bar.show_message(_('No Valid Story URLs Found in Unread Emails.'),3000)
|
||||
self.restore_cursor()
|
||||
|
||||
@@ -512,6 +519,8 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
msg = _('No Valid Story URLs Found in Unread Emails.')
|
||||
if reject_list:
|
||||
msg = msg + '<p>'+(_('(%d Story URLs Skipped, on Rejected URL List)')%len(reject_list))+'</p>'
|
||||
if notupdate_list:
|
||||
msg = msg + '<p>'+(_("(%d Story URLs Skipped, no Existing Book in Library)")%len(notupdate_list))+'</p>'
|
||||
info_dialog(self.gui, _('Get Story URLs from Email'),
|
||||
msg,
|
||||
show=True,
|
||||
@@ -1155,7 +1164,8 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
|
||||
identicalbooks = self.do_id_search(story.getMetadata('seriesUrl'))
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
if len(identicalbooks) > 0 and question_dialog(self.gui, _('Skip Story?'),'''
|
||||
if len(identicalbooks) > 0 and (prefs['auto_reject_seriesurlid'] or \
|
||||
question_dialog(self.gui, _('Skip Story?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>%s</p>
|
||||
@@ -1165,7 +1175,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
_('"<b>%s</b>" is in series "<b><a href="%s">%s</a></b>" that you have an anthology book for.')%(story.getMetadata('title'),story.getMetadata('seriesUrl'),series[:series.index(' [')]),
|
||||
_("Click '<b>Yes</b>' to Skip."),
|
||||
_("Click '<b>No</b>' to download anyway.")),
|
||||
show_copy_button=False):
|
||||
show_copy_button=False)):
|
||||
book['comment'] = _("Story in Series Anthology(%s).")%series
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
|
||||
@@ -161,7 +161,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
adapter.set_pagecache(options['pagecache'])
|
||||
|
||||
story = adapter.getStoryMetadataOnly()
|
||||
if 'calibre_series' in book:
|
||||
if not story.getMetadata("series") and 'calibre_series' in book:
|
||||
adapter.setSeries(book['calibre_series'][0],book['calibre_series'][1])
|
||||
|
||||
# set PI version instead of default.
|
||||
|
||||
@@ -150,7 +150,7 @@ extratags: FanFiction
|
||||
## Can also be used for other metadata values
|
||||
#default_value_category:FanFiction
|
||||
|
||||
## number of seconds to sleep between calls to the story site. May by
|
||||
## number of seconds to sleep between calls to the story site. May be
|
||||
## useful if pulling large numbers of stories or if the site is slow.
|
||||
#slow_down_sleep_time:0.5
|
||||
|
||||
@@ -720,6 +720,18 @@ remove_transparency: true
|
||||
## true--replace_br_with_p also fixes the problem.
|
||||
nook_img_fix:true
|
||||
|
||||
## Apply adapter's normalize_chapterurl() to all links in chapter
|
||||
## texts, if they match chapter URLs. Currently only implemented by
|
||||
## base_xenforoforum adapters.
|
||||
#normalize_text_links:false
|
||||
|
||||
## Search all links in chapter texts and, if they match any included
|
||||
## chapter URLs, replace them with links to the chapter in the
|
||||
## download. Only works with epub and html output formats.
|
||||
## base_xenforoforum adapters should also use normalize_text_links
|
||||
## with this.
|
||||
#internalize_text_links:false
|
||||
|
||||
[mobi]
|
||||
## mobi TOC cannot be turned off right now.
|
||||
#include_tocpage: true
|
||||
@@ -1177,6 +1189,21 @@ romance_label: Romance
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[fictionhunt.com]
|
||||
## Archive only site for ffnet HP stories.
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
extra_valid_entries: origin,originUrl,originHTML,reviews
|
||||
originHTML_label:Original Story URL
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:origin
|
||||
|
||||
add_to_extra_titlepage_entries:originHTML
|
||||
|
||||
[fictionmania.tv]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
@@ -1548,6 +1575,11 @@ comments_label:Comments
|
||||
|
||||
include_in_category:category,searchtags
|
||||
|
||||
[royalroadl.com]
|
||||
extra_valid_entries:stars
|
||||
|
||||
#add_to_extra_titlepage_entries:,stars
|
||||
|
||||
[samandjack.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -2319,8 +2351,9 @@ extracharacters:Wolverine,Rogue
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: Atlantis
|
||||
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
##site stopped showing reviews ~ Oct 2016
|
||||
#extra_valid_entries:reviews
|
||||
#reviews_label:Reviews
|
||||
|
||||
[buffygiles.velocitygrass.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
|
||||
@@ -130,6 +130,7 @@ default_prefs['deleteotherforms'] = False
|
||||
default_prefs['adddialogstaysontop'] = False
|
||||
default_prefs['lookforurlinhtml'] = False
|
||||
default_prefs['checkforseriesurlid'] = True
|
||||
default_prefs['auto_reject_seriesurlid'] = False
|
||||
default_prefs['checkforurlchange'] = True
|
||||
default_prefs['injectseries'] = False
|
||||
default_prefs['matchtitleauth'] = True
|
||||
@@ -176,6 +177,7 @@ default_prefs['imapsessionpass'] = False
|
||||
default_prefs['imapfolder'] = 'INBOX'
|
||||
default_prefs['imapmarkread'] = True
|
||||
default_prefs['auto_reject_from_email'] = False
|
||||
default_prefs['update_existing_only_from_email'] = False
|
||||
default_prefs['download_from_email_immediately'] = False
|
||||
|
||||
def set_library_config(library_config,db):
|
||||
|
||||
+425
-403
File diff suppressed because it is too large
Load Diff
+421
-399
File diff suppressed because it is too large
Load Diff
+422
-400
File diff suppressed because it is too large
Load Diff
+419
-397
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+419
-397
File diff suppressed because it is too large
Load Diff
+422
-399
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+419
-397
File diff suppressed because it is too large
Load Diff
+432
-409
File diff suppressed because it is too large
Load Diff
+418
-396
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2015 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
try:
|
||||
# just a way to switch between web service and CLI/PI
|
||||
import google.appengine.api
|
||||
import google.appengine.api
|
||||
except:
|
||||
try: # just a way to switch between CLI and PI
|
||||
import calibre.constants
|
||||
@@ -31,4 +31,3 @@ except:
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -143,6 +143,8 @@ import adapter_haremlucifaelcom
|
||||
import adapter_kiarepositorymujajinet
|
||||
import adapter_fanfictionlucifaelcom
|
||||
import adapter_adultfanfictionorg
|
||||
import adapter_fictionhuntcom
|
||||
import adapter_royalroadl
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,14 +15,12 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
from urllib import unquote_plus
|
||||
import time
|
||||
|
||||
from .. import exceptions as exceptions
|
||||
from ..htmlcleanup import stripHTML
|
||||
@@ -206,7 +204,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# b.extract()
|
||||
metatext = stripHTML(grayspan).replace('Hurt/Comfort','Hurt-Comfort')
|
||||
#logger.debug("metatext:(%s)"%metatext)
|
||||
|
||||
|
||||
if 'Status: Complete' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
@@ -282,8 +280,8 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
l = chars_ships_text
|
||||
while '[' in l:
|
||||
self.story.addToList('ships',l[l.index('[')+1:l.index(']')].replace(', ','/'))
|
||||
l = l[l.index(']')+1:]
|
||||
|
||||
l = l[l.index(']')+1:]
|
||||
|
||||
if get_cover:
|
||||
# Try the larger image first.
|
||||
cover_url = ""
|
||||
@@ -331,7 +329,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# Find the chapter selector
|
||||
select = soup.find('select', { 'name' : 'chapter' } )
|
||||
|
||||
|
||||
if select is None:
|
||||
# no selector found, so it's a one-chapter story.
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
@@ -351,36 +349,17 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
# time.sleep(4.0) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
# ## more if hit too fast. This is in
|
||||
# ## additional to what ever the
|
||||
# ## slow_down_sleep_time setting is.
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
## ffnet(and, I assume, fpcom) tends to fail more if hit too
|
||||
## fast. This is in additional to what ever the
|
||||
## slow_down_sleep_time setting is.
|
||||
data = self._fetchUrl(url,extrasleep=4.0)
|
||||
|
||||
if "Please email this error message in full to <a href='mailto:support@fanfiction.com'>support@fanfiction.com</a>" in data:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! FanFiction.net Site Error!" % url)
|
||||
|
||||
# some ancient stories have body tags inside them that cause
|
||||
# soup parsing to discard the content. For story text we
|
||||
# don't care about anything before "<div role='main'" and
|
||||
# this kills any body tags.
|
||||
# XXX needed with new BS? -- No, doesn't look like it
|
||||
# divstr = "<div role='main'"
|
||||
# if divstr not in data:
|
||||
# raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
# else:
|
||||
# data = data[data.index(divstr):]
|
||||
# data = data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
## Remove the 'share' button.
|
||||
## No longer appears in the story text.
|
||||
# sharediv = soup.find('div', {'class' : 'a2a_kit a2a_default_style'})
|
||||
# if sharediv:
|
||||
# sharediv.extract()
|
||||
|
||||
div = soup.find('div', {'id' : 'storytextp'})
|
||||
|
||||
if None == div:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2016 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import exceptions as exceptions
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
class FictionHuntComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
self.story.setMetadata('siteabbrev','fichunt')
|
||||
|
||||
# get storyId from url--url validation guarantees second part is storyId
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
+"/read/"+self.story.getMetadata('storyId')+"/1")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d-%m-%Y"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'fictionhunt.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://fictionhunt.com/read/1234/1"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www.)?fictionhunt.com/read/\d+(/\d+)?(/|/[^/]+)?/?$"
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
adapters that will work with the page cache need to implement
|
||||
this and change it to True.
|
||||
'''
|
||||
return True
|
||||
|
||||
def doExtractChapterUrlsAndMetadata(self,get_cover=True):
|
||||
|
||||
# fetch the chapter. From that we will get almost all the
|
||||
# metadata and chapter list
|
||||
|
||||
url = self.url
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.meta)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
self.story.setMetadata('title',stripHTML(soup.find('div',{'class':'title'})).strip())
|
||||
|
||||
self.setDescription(url,'<i>(Story descriptions not available on fictionhunt.com)</i>')
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
# fictionhunt doesn't have author pages, use ffnet original author link.
|
||||
a = soup.find('a', href=re.compile(r"fanfiction.net/u/\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[-1])
|
||||
self.story.setMetadata('authorUrl','https://www.fanfiction.net/u/'+self.story.getMetadata('authorId'))
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find original ffnet URL
|
||||
a = soup.find('a', href=re.compile(r"fanfiction.net/s/\d+"))
|
||||
self.story.setMetadata('origin',stripHTML(a))
|
||||
self.story.setMetadata('originUrl',a['href'])
|
||||
|
||||
# Fleur D. & Harry P. & Hermione G. & Susan B. - Words: 42,848 - Rated: M - English - None - Chapters: 9 - Reviews: 248 - Updated: 21-09-2016 - Published: 16-05-2015 - by Elven Sorcerer (FFN)
|
||||
# None - Words: 13,087 - Rated: M - English - Romance & Supernatural - Chapters: 3 - Reviews: 5 - Updated: 21-09-2016 - Published: 20-09-2016
|
||||
# Harry P. & OC - Words: 10,910 - Rated: M - English - None - Chapters: 5 - Reviews: 6 - Updated: 21-09-2016 - Published: 11-09-2016
|
||||
# Dudley D. & Harry P. & Nagini & Vernon D. - Words: 4,328 - Rated: K+ - English - None - Chapters: 2 - Updated: 21-09-2016 - Published: 20-09-2016 -
|
||||
details = soup.find('div',{'class':'details'})
|
||||
|
||||
detail_re = \
|
||||
r'(?P<characters>.+) - Words: (?P<numWords>[0-9,]+) - Rated: (?P<rating>[a-zA-Z\\+]+) - (?P<language>.+) - (?P<genre>.+)'+ \
|
||||
r' - Chapters: (?P<numChapters>[0-9,]+)( - Reviews: (?P<reviews>[0-9,]+))? - Updated: (?P<dateUpdated>[0-9-]+)'+ \
|
||||
r' - Published: (?P<datePublished>[0-9-]+)(?P<completed> - Complete)?'
|
||||
|
||||
details_dict = re.match(detail_re,stripHTML(details)).groupdict()
|
||||
|
||||
# lists
|
||||
for meta in ('characters','genre'):
|
||||
if details_dict[meta] != 'None':
|
||||
self.story.extendList(meta,details_dict[meta].split(' & '))
|
||||
|
||||
# scalars
|
||||
for meta in ('numWords','numChapters','rating','language','reviews'):
|
||||
self.story.setMetadata(meta,details_dict[meta])
|
||||
|
||||
# dates
|
||||
for meta in ('datePublished','dateUpdated'):
|
||||
self.story.setMetadata(meta, makeDate(details_dict[meta], self.dateformat))
|
||||
|
||||
# status
|
||||
if details_dict['completed']:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
# It's assumed that the number of chapters is correct.
|
||||
# There's no complete list of chapters, so the only
|
||||
# alternative is to get the num of chaps from the last
|
||||
# indiated chapter list instead.
|
||||
for i in range(1,1+int(self.story.getMetadata('numChapters'))):
|
||||
self.chapterUrls.append(("Chapter "+unicode(i),"http://"+self.getSiteDomain()\
|
||||
+"/read/"+self.story.getMetadata('storyId')+"/%s"%i))
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
div = soup.find('div', {'class' : 'text'})
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
def getClass():
|
||||
return FictionHuntComSiteAdapter
|
||||
@@ -184,7 +184,6 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
@@ -195,7 +194,13 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
svalue = ""
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
# poor HTML(unclosed <p> for one) can cause run on
|
||||
# over the next label.
|
||||
if '<span class="label">' in svalue:
|
||||
svalue = svalue[0:svalue.find('<span class="label">')]
|
||||
break
|
||||
else:
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2016 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.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
from datetime import datetime
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return RoyalRoadAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class RoyalRoadAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8",
|
||||
"Windows-1252"
|
||||
] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only fiction/1234
|
||||
self.story.setMetadata('storyId',re.match('/fiction/(\d+)(:/.+)?$',self.parsedUrl.path).groups()[0])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fiction/'+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','rylrdl')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = '%d/%m/%Y %H:%M:%S %p'
|
||||
|
||||
def make_date(self, parenttag):
|
||||
# locale dates differ but the timestamp is easily converted
|
||||
ts = parenttag.find('time')['unixtime']
|
||||
return datetime.fromtimestamp(float(ts))
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'royalroadl.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['royalroadl.com','www.royalroadl.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "https://royalroadl.com/fiction/3056"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return "https?"+re.escape("://")+r"(www\.|)royalroadl\.com/fiction/\d+$"
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
adapters that will work with the page cache need to implement
|
||||
this and change it to True.
|
||||
'''
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
|
||||
## Title
|
||||
title=soup.h2.text
|
||||
self.story.setMetadata('title',title)
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
author = soup.find('',{'class':'mt-card-social'})
|
||||
author_link = author.findAll('li')[-1]
|
||||
if author_link:
|
||||
authorId = author_link.a['href'].split('=')[-1]
|
||||
self.story.setMetadata('authorId', authorId)
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/member.php?action=profile&uid='+authorId)
|
||||
self.story.setMetadata('author',soup.find(attrs=dict(property="books:author"))['content'])
|
||||
|
||||
|
||||
chapters = soup.find('table',{'id':'chapters'}).find('tbody')
|
||||
tds = [tr.findAll('td')[0] for tr in chapters.findAll('tr')]
|
||||
for td in tds:
|
||||
chapterUrl = 'http://' + self.getSiteDomain() + td.a['href']
|
||||
self.chapterUrls.append((stripHTML(td.text), chapterUrl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# this is forum based so it's a bit ugly
|
||||
description = soup.find('div', {'property': 'description', 'class': 'hidden-content'})
|
||||
self.setDescription(url,description)
|
||||
|
||||
dates = [tr.findAll('td')[1] for tr in chapters.findAll('tr')]
|
||||
self.story.setMetadata('dateUpdated', self.make_date(dates[-1]))
|
||||
self.story.setMetadata('datePublished', self.make_date(dates[0]))
|
||||
|
||||
genre=[tag.text for tag in soup.find('input',{'property':'genre'}).parent.findChildren('span')]
|
||||
if not "Unspecified" in genre:
|
||||
for tag in genre:
|
||||
self.story.addToList('genre',tag)
|
||||
|
||||
# 'rating' in FFF speak means G, PG, Teen, Restricted, etc.
|
||||
# 'stars' is used instead for RR's 1-5 stars rating.
|
||||
stars=soup.find(attrs=dict(property="books:rating:value"))['content']
|
||||
self.story.setMetadata('stars',stars)
|
||||
logger.debug(self.story.getMetadata('stars'))
|
||||
|
||||
warning = soup.find('strong',text='Warning')
|
||||
if warning != None:
|
||||
warnings=[c.text for c in warning.parent.children if getattr(c,'text',None)][1:]
|
||||
for warntag in warnings:
|
||||
self.story.addToList('warnings',warntag)
|
||||
|
||||
# get cover
|
||||
img = soup.find('',{'class':'row fic-header'}).find('img')
|
||||
if img:
|
||||
cover_url = img['src']
|
||||
self.setCoverImage(url,cover_url)
|
||||
# some content is show as tables, this will preserve them
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div',{'class':"chapter-inner chapter-content"})
|
||||
|
||||
# TODO: these stories often have tables in, but these wont render correctly
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -171,7 +171,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('author',stripHTML(a).replace("'s Page",""))
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.findAll('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId')+":\d+$"))
|
||||
chapters = soup.findAll('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId')+":\d+(/.*)?$"))
|
||||
if len(chapters) != 0:
|
||||
for chapter in chapters:
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
|
||||
@@ -125,9 +125,10 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
rating=pt.text.split('[')[1].split(']')[0]
|
||||
self.story.setMetadata('rating', rating)
|
||||
|
||||
st = soup.find('div', {'class' : 'storytitle'})
|
||||
a = st.findAll('a', href=re.compile(r'reviews.php\?type=ST&item='+self.story.getMetadata('storyId')+"$"))[1] # second one.
|
||||
self.story.setMetadata('reviews',stripHTML(a))
|
||||
# site stopped showing reviews ~ Oct 2016
|
||||
# st = soup.find('div', {'class' : 'storytitle'})
|
||||
# a = st.findAll('a', href=re.compile(r'reviews.php\?type=ST&item='+self.story.getMetadata('storyId')+"$"))[1] # second one.
|
||||
# self.story.setMetadata('reviews',stripHTML(a))
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
|
||||
@@ -84,7 +84,7 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def __init__(self, configuration, url):
|
||||
Configurable.__init__(self, configuration)
|
||||
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
@@ -113,7 +113,7 @@ class BaseSiteAdapter(Configurable):
|
||||
self.logfile = None
|
||||
|
||||
self.pagecache = self.get_empty_pagecache()
|
||||
|
||||
|
||||
## order of preference for decoding.
|
||||
self.decode = ["utf8",
|
||||
"Windows-1252"] # 1252 is a superset of
|
||||
@@ -135,17 +135,17 @@ class BaseSiteAdapter(Configurable):
|
||||
saveheaders = self.opener.addheaders
|
||||
self.opener = u2.build_opener(u2.HTTPCookieProcessor(self.cookiejar),GZipProcessor())
|
||||
self.opener.addheaders = saveheaders
|
||||
|
||||
|
||||
def load_cookiejar(self,filename):
|
||||
'''
|
||||
Needs to be called after adapter create, but before any fetchs
|
||||
are done. Takes file *name*.
|
||||
'''
|
||||
self.get_cookiejar().load(filename, ignore_discard=True, ignore_expires=True)
|
||||
|
||||
|
||||
def get_pagecache(self):
|
||||
return self.pagecache
|
||||
|
||||
|
||||
def set_pagecache(self,d):
|
||||
self.pagecache=d
|
||||
|
||||
@@ -159,7 +159,7 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def _has_cachekey(self,cachekey):
|
||||
return self.use_pagecache() and cachekey in self.get_pagecache()
|
||||
|
||||
|
||||
def _get_from_pagecache(self,cachekey):
|
||||
if self.use_pagecache():
|
||||
return self.get_pagecache().get(cachekey)
|
||||
@@ -176,18 +176,18 @@ class BaseSiteAdapter(Configurable):
|
||||
this and change it to True.
|
||||
'''
|
||||
return False
|
||||
|
||||
|
||||
# def story_load(self,filename):
|
||||
# d = pickle.load(self.story.metadata,filename)
|
||||
# self.story.metadata = d['metadata']
|
||||
# self.chapterUrls = d['chapterlist']
|
||||
# self.story.metadataDone = True
|
||||
|
||||
|
||||
def _setURL(self,url):
|
||||
self.url = url
|
||||
self.parsedUrl = up.urlparse(url)
|
||||
self.host = self.parsedUrl.netloc
|
||||
self.path = self.parsedUrl.path
|
||||
self.path = self.parsedUrl.path
|
||||
self.story.setMetadata('storyUrl',self.url,condremoveentities=False)
|
||||
|
||||
## website encoding(s)--in theory, each website reports the character
|
||||
@@ -201,7 +201,7 @@ class BaseSiteAdapter(Configurable):
|
||||
decode = self.getConfigList('website_encodings')
|
||||
else:
|
||||
decode = self.decode
|
||||
|
||||
|
||||
for code in decode:
|
||||
try:
|
||||
#print code
|
||||
@@ -230,7 +230,7 @@ class BaseSiteAdapter(Configurable):
|
||||
usecache=True):
|
||||
'''
|
||||
When should cache be cleared or not used? logins...
|
||||
|
||||
|
||||
extrasleep is primarily for ffnet adapter which has extra
|
||||
sleeps. Passed into fetchs so it can be bypassed when
|
||||
cache hits.
|
||||
@@ -240,7 +240,7 @@ class BaseSiteAdapter(Configurable):
|
||||
logger.debug("#####################################\npagecache HIT: %s"%safe_url(cachekey))
|
||||
data,redirecturl = self._get_from_pagecache(cachekey)
|
||||
return data
|
||||
|
||||
|
||||
logger.debug("#####################################\npagecache MISS: %s"%safe_url(cachekey))
|
||||
self.do_sleep(extrasleep)
|
||||
|
||||
@@ -261,19 +261,19 @@ class BaseSiteAdapter(Configurable):
|
||||
parameters=None,
|
||||
extrasleep=None,
|
||||
usecache=True):
|
||||
|
||||
|
||||
return self._fetchUrlRawOpened(url,
|
||||
parameters,
|
||||
extrasleep,
|
||||
usecache)[0]
|
||||
|
||||
|
||||
def _fetchUrlRawOpened(self, url,
|
||||
parameters=None,
|
||||
extrasleep=None,
|
||||
usecache=True):
|
||||
'''
|
||||
When should cache be cleared or not used? logins...
|
||||
|
||||
|
||||
extrasleep is primarily for ffnet adapter which has extra
|
||||
sleeps. Passed into fetchs so it can be bypassed when
|
||||
cache hits.
|
||||
@@ -289,7 +289,7 @@ class BaseSiteAdapter(Configurable):
|
||||
def geturl(self): return self.url
|
||||
def read(self): return self.data
|
||||
return (data,FakeOpened(data,redirecturl))
|
||||
|
||||
|
||||
logger.debug("#####################################\npagecache MISS: %s"%safe_url(cachekey))
|
||||
self.do_sleep(extrasleep)
|
||||
if parameters != None:
|
||||
@@ -298,13 +298,13 @@ class BaseSiteAdapter(Configurable):
|
||||
opened = self.opener.open(url.replace(' ','%20'),None,float(self.getConfig('connect_timeout',30.0)))
|
||||
data = opened.read()
|
||||
self._set_to_pagecache(cachekey,data,opened.url)
|
||||
|
||||
|
||||
return (data,opened)
|
||||
|
||||
def set_sleep(self,val):
|
||||
logger.debug("\n===========\n set sleep time %s\n==========="%val)
|
||||
self.override_sleep = val
|
||||
|
||||
|
||||
def do_sleep(self,extrasleep=None):
|
||||
if extrasleep:
|
||||
time.sleep(float(extrasleep))
|
||||
@@ -312,7 +312,7 @@ class BaseSiteAdapter(Configurable):
|
||||
time.sleep(float(self.override_sleep))
|
||||
elif self.getConfig('slow_down_sleep_time'):
|
||||
time.sleep(float(self.getConfig('slow_down_sleep_time')))
|
||||
|
||||
|
||||
def _fetchUrl(self, url,
|
||||
parameters=None,
|
||||
usecache=True,
|
||||
@@ -330,7 +330,7 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
excpt=None
|
||||
for sleeptime in [0, 0.5, 4, 9]:
|
||||
time.sleep(sleeptime)
|
||||
time.sleep(sleeptime)
|
||||
try:
|
||||
(data,opened)=self._fetchUrlRawOpened(url,
|
||||
parameters=parameters,
|
||||
@@ -345,7 +345,7 @@ class BaseSiteAdapter(Configurable):
|
||||
except Exception, e:
|
||||
excpt=e
|
||||
logger.warn("Caught an exception reading URL: %s sleeptime(%s) Exception %s."%(unicode(safe_url(url)),sleeptime,unicode(e)))
|
||||
|
||||
|
||||
logger.error("Giving up on %s" %safe_url(url))
|
||||
logger.debug(excpt, exc_info=True)
|
||||
raise(excpt)
|
||||
@@ -357,12 +357,16 @@ class BaseSiteAdapter(Configurable):
|
||||
if last:
|
||||
self.chapterLast=int(last)-1
|
||||
self.story.set_chapters_range(first,last)
|
||||
|
||||
|
||||
# Does the download the first time it's called.
|
||||
def getStory(self):
|
||||
if not self.storyDone:
|
||||
self.getStoryMetadataOnly(get_cover=True)
|
||||
|
||||
## one-off step to normalize old chapter URLs if present.
|
||||
if self.oldchaptersmap:
|
||||
self.oldchaptersmap = dict((self.normalize_chapterurl(key), value) for (key, value) in self.oldchaptersmap.items())
|
||||
|
||||
for index, (title,url) in enumerate(self.chapterUrls):
|
||||
newchap = False
|
||||
if (self.chapterFirst!=None and index < self.chapterFirst) or \
|
||||
@@ -388,7 +392,7 @@ class BaseSiteAdapter(Configurable):
|
||||
url in self.oldchaptersdata and (
|
||||
self.oldchaptersdata[url]['chapterorigtitle'] !=
|
||||
self.oldchaptersdata[url]['chaptertitle']) )
|
||||
|
||||
|
||||
if not data:
|
||||
data = self.getChapterText(url)
|
||||
# if had to fetch and has existing chapters
|
||||
@@ -400,13 +404,13 @@ class BaseSiteAdapter(Configurable):
|
||||
# anyway--only if it's replaced during an
|
||||
# update.
|
||||
newchap = False
|
||||
|
||||
|
||||
self.story.addChapter(url,
|
||||
removeEntities(title),
|
||||
removeEntities(data),
|
||||
newchap)
|
||||
self.storyDone = True
|
||||
|
||||
|
||||
# include image, but no cover from story, add default_cover_image cover.
|
||||
if self.getConfig('include_images') and \
|
||||
not self.story.cover and \
|
||||
@@ -423,26 +427,30 @@ class BaseSiteAdapter(Configurable):
|
||||
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:
|
||||
self.story.calibrebookmark = self.calibrebookmark
|
||||
if self.logfile:
|
||||
self.story.logfile = self.logfile
|
||||
|
||||
|
||||
return self.story
|
||||
|
||||
def getStoryMetadataOnly(self,get_cover=True):
|
||||
if not self.metadataDone:
|
||||
self.doExtractChapterUrlsAndMetadata(get_cover=get_cover)
|
||||
|
||||
|
||||
if not self.story.getMetadataRaw('dateUpdated'):
|
||||
if self.story.getMetadataRaw('datePublished'):
|
||||
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
|
||||
else:
|
||||
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
|
||||
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
|
||||
|
||||
self.metadataDone = True
|
||||
# normalize chapter urls.
|
||||
for index, (title,url) in enumerate(self.chapterUrls):
|
||||
self.chapterUrls[index] = (title,self.normalize_chapterurl(url))
|
||||
|
||||
return self.story
|
||||
|
||||
def setStoryMetadata(self,metahtml):
|
||||
@@ -453,36 +461,36 @@ class BaseSiteAdapter(Configurable):
|
||||
if self.story.getMetadataRaw('datePublished'):
|
||||
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
|
||||
else:
|
||||
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
|
||||
|
||||
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
|
||||
|
||||
def hookForUpdates(self,chaptercount):
|
||||
"Usually not needed."
|
||||
return chaptercount
|
||||
|
||||
###############################
|
||||
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
"Needs to be overriden in each adapter class."
|
||||
return 'no such domain'
|
||||
|
||||
|
||||
@classmethod
|
||||
def getConfigSection(cls):
|
||||
"Only needs to be overriden if != site domain."
|
||||
return cls.getSiteDomain()
|
||||
|
||||
|
||||
@classmethod
|
||||
def getConfigSections(cls):
|
||||
"Only needs to be overriden if has additional ini sections."
|
||||
return [cls.getConfigSection()]
|
||||
|
||||
|
||||
@classmethod
|
||||
def stripURLParameters(cls,url):
|
||||
"Only needs to be overriden if URL contains more than one parameter"
|
||||
## remove any trailing '&' parameters--?sid=999 will be left.
|
||||
## that's all that any of the current adapters need or want.
|
||||
return re.sub(r"&.*$","",url)
|
||||
|
||||
|
||||
## URL pattern validation is done *after* picking an adaptor based
|
||||
## on domain instead of *as* the adaptor selector so we can offer
|
||||
## the user example(s) for that particular site.
|
||||
@@ -490,7 +498,7 @@ class BaseSiteAdapter(Configurable):
|
||||
def getSiteURLPattern(self):
|
||||
"Used to validate URL. Should be override in each adapter class."
|
||||
return '^http://'+re.escape(self.getSiteDomain())
|
||||
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
"""
|
||||
@@ -500,7 +508,7 @@ class BaseSiteAdapter(Configurable):
|
||||
validateURL method.
|
||||
"""
|
||||
return 'no such example'
|
||||
|
||||
|
||||
def doExtractChapterUrlsAndMetadata(self,get_cover=True):
|
||||
'''
|
||||
There are a handful of adapters that fetch a cover image while
|
||||
@@ -509,7 +517,7 @@ class BaseSiteAdapter(Configurable):
|
||||
this instead of extractChapterUrlsAndMetadata()
|
||||
'''
|
||||
return self.extractChapterUrlsAndMetadata()
|
||||
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
"Needs to be overriden in each adapter class. Populates self.story metadata and self.chapterUrls"
|
||||
pass
|
||||
@@ -561,7 +569,7 @@ class BaseSiteAdapter(Configurable):
|
||||
# bs4
|
||||
return soup.attrs.keys()
|
||||
return []
|
||||
|
||||
|
||||
# This gives us a unicode object, not just a string containing bytes.
|
||||
# (I gave soup a unicode string, you'd think it could give it back...)
|
||||
# Now also does a bunch of other common processing for us.
|
||||
@@ -570,12 +578,12 @@ class BaseSiteAdapter(Configurable):
|
||||
fetch=self._fetchUrlRaw
|
||||
|
||||
acceptable_attributes = self.getConfigList('keep_html_attrs',['href','name','class','id'])
|
||||
|
||||
|
||||
if self.getConfig("keep_style_attr"):
|
||||
acceptable_attributes.append('style')
|
||||
if self.getConfig("keep_title_attr"):
|
||||
acceptable_attributes.append('title')
|
||||
|
||||
|
||||
#print("include_images:"+self.getConfig('include_images'))
|
||||
if self.getConfig('include_images'):
|
||||
acceptable_attributes.extend(('src','alt','longdesc'))
|
||||
@@ -592,6 +600,19 @@ class BaseSiteAdapter(Configurable):
|
||||
if attr not in acceptable_attributes:
|
||||
del soup[attr] ## strip all tag attributes except href and name
|
||||
|
||||
## apply adapter's normalize_chapterurls to all links in
|
||||
## chapter texts, if they match chapter URLs. While this will
|
||||
## be occasionally helpful by itself, it's really for the next
|
||||
## feature: internal text links.
|
||||
if self.getConfig('normalize_text_links'):
|
||||
for alink in soup.find_all('a'):
|
||||
# try:
|
||||
if alink.has_attr('href'):
|
||||
# logger.debug("normalize_text_links %s -> %s"%(alink['href'],self.normalize_chapterurl(alink['href'])))
|
||||
alink['href'] = self.normalize_chapterurl(alink['href'])
|
||||
# except AttributeError as ae:
|
||||
# logger.info("Parsing for normalize_text_links failed...")
|
||||
|
||||
try:
|
||||
# as a generator, each tag will be returned even if there's a
|
||||
# mismatch at the end.
|
||||
@@ -599,8 +620,8 @@ class BaseSiteAdapter(Configurable):
|
||||
for attr in self.get_attr_keys(t):
|
||||
if attr not in acceptable_attributes:
|
||||
del t[attr] ## strip all tag attributes except acceptable_attributes
|
||||
|
||||
# these are not acceptable strict XHTML. But we do already have
|
||||
|
||||
# these are not acceptable strict XHTML. But we do already have
|
||||
# CSS classes of the same names defined
|
||||
if t and hasattr(t,'name') and t.name is not None:
|
||||
if t.name in self.getConfigList('replace_tags_with_spans',['u']):
|
||||
@@ -616,11 +637,11 @@ class BaseSiteAdapter(Configurable):
|
||||
# remove script tags cross the board.
|
||||
if t.name=='script':
|
||||
t.extract()
|
||||
|
||||
|
||||
except AttributeError, ae:
|
||||
if "%s"%ae != "'NoneType' object has no attribute 'next_element'":
|
||||
logger.error("Error parsing HTML, probably poor input HTML. %s"%ae)
|
||||
|
||||
|
||||
retval = unicode(soup)
|
||||
|
||||
if self.getConfig('nook_img_fix') and not self.getConfig('replace_br_with_p'):
|
||||
@@ -629,16 +650,16 @@ class BaseSiteAdapter(Configurable):
|
||||
# that under the text for the rest of the chapter.
|
||||
retval = re.sub(r"(?!<(div|p)>)\s*(?P<imgtag><img[^>]+>)\s*(?!</(div|p)>)",
|
||||
"<div>\g<imgtag></div>",retval)
|
||||
|
||||
|
||||
# Don't want html, head or body tags in chapter html--writers add them.
|
||||
# This is primarily for epub updates.
|
||||
retval = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",retval)
|
||||
|
||||
|
||||
if self.getConfig("replace_br_with_p") and allow_replace_br_with_p:
|
||||
# Apply heuristic processing to replace <br> paragraph
|
||||
# breaks with <p> tags.
|
||||
retval = replace_br_with_p(retval)
|
||||
|
||||
|
||||
if self.getConfig('replace_hr'):
|
||||
# replacing a self-closing tag with a container tag in the
|
||||
# soup is more difficult than it first appears. So cheat.
|
||||
@@ -648,31 +669,35 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def make_soup(self,data):
|
||||
'''
|
||||
Convenience method for getting a bs4 soup. Older and
|
||||
non-updated adapters call the included bs3 library themselves.
|
||||
Convenience method for getting a bs4 soup. bs3 has been removed.
|
||||
'''
|
||||
|
||||
|
||||
## html5lib handles <noscript> oddly. See:
|
||||
## https://bugs.launchpad.net/beautifulsoup/+bug/1277464
|
||||
## This should 'hide' and restore <noscript> tags.
|
||||
data = data.replace("noscript>","fff_hide_noscript>")
|
||||
|
||||
|
||||
## soup and re-soup because BS4/html5lib is more forgiving of
|
||||
## incorrectly nested tags that way.
|
||||
soup = bs4.BeautifulSoup(data,'html5lib')
|
||||
soup = bs4.BeautifulSoup(unicode(soup),'html5lib')
|
||||
|
||||
|
||||
for ns in soup.find_all('fff_hide_noscript'):
|
||||
ns.name = 'noscript'
|
||||
|
||||
|
||||
return soup
|
||||
|
||||
|
||||
## For adapters, especially base_xenforoforum to override. Make
|
||||
## sure to return unchanged URL if it's NOT a chapter URL...
|
||||
def normalize_chapterurl(self,url):
|
||||
return url
|
||||
|
||||
def cachedfetch(realfetch,cache,url):
|
||||
if url in cache:
|
||||
return cache[url]
|
||||
else:
|
||||
return realfetch(url)
|
||||
|
||||
|
||||
fullmon = {u"January":u"01", u"February":u"02", u"March":u"03", u"April":u"04", u"May":u"05",
|
||||
u"June":u"06","July":u"07", u"August":u"08", u"September":u"09", u"October":u"10",
|
||||
u"November":u"11", u"December":u"12" }
|
||||
@@ -687,7 +712,7 @@ def makeDate(string,dateform):
|
||||
# lie. It has to do something even more complicated to get
|
||||
# Russian month names correct everywhere.
|
||||
do_abbrev = "%b" in dateform
|
||||
|
||||
|
||||
if u"%B" in dateform or do_abbrev:
|
||||
dateform = dateform.replace(u"%B",u"%m").replace(u"%b",u"%m")
|
||||
for (name,num) in fullmon.items():
|
||||
@@ -708,10 +733,10 @@ def makeDate(string,dateform):
|
||||
string = string.replace(u"AM",u"").replace(u"PM",u"").replace(u"am",u"").replace(u"pm",u"")
|
||||
|
||||
date = datetime.strptime(string.encode('utf-8'),dateform.encode('utf-8'))
|
||||
|
||||
|
||||
if add_hours:
|
||||
date += timedelta(hours=12)
|
||||
|
||||
|
||||
return date
|
||||
|
||||
# .? for AO3's ']' in param names.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 FanFicFare team
|
||||
# Copyright 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -85,6 +85,62 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?[^#]*?(#post-(?P<anchorpost>\d+))?$"
|
||||
|
||||
## For adapters, especially base_xenforoforum to override. Make
|
||||
## sure to return unchanged URL if it's NOT a chapter URL. This
|
||||
## is most helpful for xenforoforum because threadmarks use
|
||||
## thread-name URLs--which can change if the thread name changes.
|
||||
def normalize_chapterurl(self,url):
|
||||
(is_chapter_url,normalized_url) = self._is_normalize_chapterurl(url)
|
||||
if is_chapter_url:
|
||||
return normalized_url
|
||||
else:
|
||||
return url
|
||||
|
||||
## returns (is_chapter_url,normalized_url)
|
||||
def _is_normalize_chapterurl(self,url):
|
||||
is_chapter_url = False
|
||||
|
||||
## moved from extract metadata to share with normalize_chapterurl.
|
||||
if not url.startswith('http'):
|
||||
url = self.getURLPrefix()+'/'+url
|
||||
|
||||
if ( url.startswith(self.getURLPrefix()) or
|
||||
url.startswith('http://'+self.getSiteDomain()) or
|
||||
url.startswith('https://'+self.getSiteDomain()) ) and \
|
||||
( '/posts/' in url or '/threads/' in url or 'showpost.php' in url or 'goto/post' in url):
|
||||
# brute force way to deal with SB's http->https change when hardcoded http urls.
|
||||
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
|
||||
|
||||
# http://forums.spacebattles.com/showpost.php?p=4755532&postcount=9
|
||||
url = re.sub(r'showpost\.php\?p=([0-9]+)(&postcount=[0-9]+)?',r'/posts/\1/',url)
|
||||
|
||||
# http://forums.spacebattles.com/goto/post?id=15222406#post-15222406
|
||||
url = re.sub(r'/goto/post\?id=([0-9]+)(#post-[0-9]+)?',r'/posts/\1/',url)
|
||||
|
||||
url = re.sub(r'(^[\'"]+|[\'"]+$)','',url) # strip leading or trailing '" from incorrect quoting.
|
||||
url = re.sub(r'like$','',url) # strip 'like' if incorrect 'like' link instead of proper post URL.
|
||||
|
||||
#### moved from getChapterText()
|
||||
## there's some history of stories with links to the wrong
|
||||
## page. This changes page#post URLs to perma-link URLs.
|
||||
## Which will be redirected back to page#posts, but the
|
||||
## *correct* ones.
|
||||
# http://forums.sufficientvelocity.com/threads/harry-potter-and-the-not-fatal-at-all-cultural-exchange-program.330/page-4#post-39915
|
||||
# https://forums.sufficientvelocity.com/posts/39915/
|
||||
if '#post-' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('#post-')[1]+'/'
|
||||
|
||||
## Same as above except for for case where author mistakenly
|
||||
## used the reply link instead of normal link to post.
|
||||
# "http://forums.spacebattles.com/threads/manager-worm-story-thread-iv.301602/reply?quote=15962513"
|
||||
# https://forums.spacebattles.com/posts/
|
||||
if 'reply?quote=' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('reply?quote=')[1]+'/'
|
||||
|
||||
is_chapter_url = True
|
||||
return (is_chapter_url,url)
|
||||
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
adapters that will work with the page cache need to implement
|
||||
@@ -119,7 +175,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
# params[soup.find('input', {'id':'password'})['name']] = params['password']
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
|
||||
if "Log Out" not in d :
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['login']))
|
||||
@@ -183,7 +239,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
threadmark_chaps = True
|
||||
if self.getConfig('always_include_first_post'):
|
||||
self.chapterUrls.append((first_post_title,useurl))
|
||||
|
||||
|
||||
for (atag,url,name) in [ (x,x['href'],stripHTML(x)) for x in markas ]:
|
||||
date = self.make_date(atag.find_next_sibling('div',{'class':'extra'}))
|
||||
if not self.story.getMetadataRaw('datePublished') or date < self.story.getMetadataRaw('datePublished'):
|
||||
@@ -202,7 +258,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
if self.getConfig('capitalize_forumtags'):
|
||||
tstr = tstr.title()
|
||||
self.story.addToList('forumtags',tstr)
|
||||
|
||||
|
||||
# Now go hunting for the 'chapter list'.
|
||||
bq = soup.find('blockquote') # assume first posting contains TOC urls.
|
||||
|
||||
@@ -222,28 +278,9 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
if not self.chapterUrls:
|
||||
self.chapterUrls.append((first_post_title,useurl))
|
||||
for (url,name) in [ (x['href'],stripHTML(x)) for x in bq.find_all('a') ]:
|
||||
#logger.debug("found chapurl:%s"%url)
|
||||
if not url.startswith('http'):
|
||||
url = self.getURLPrefix()+'/'+url
|
||||
|
||||
if ( url.startswith(self.getURLPrefix()) or
|
||||
url.startswith('http://'+self.getSiteDomain()) or
|
||||
url.startswith('https://'+self.getSiteDomain()) ) and \
|
||||
( '/posts/' in url or '/threads/' in url or 'showpost.php' in url or 'goto/post' in url):
|
||||
|
||||
# brute force way to deal with SB's http->https change when hardcoded http urls.
|
||||
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
|
||||
|
||||
# http://forums.spacebattles.com/showpost.php?p=4755532&postcount=9
|
||||
url = re.sub(r'showpost\.php\?p=([0-9]+)(&postcount=[0-9]+)?',r'/posts/\1/',url)
|
||||
|
||||
# http://forums.spacebattles.com/goto/post?id=15222406#post-15222406
|
||||
url = re.sub(r'/goto/post\?id=([0-9]+)(#post-[0-9]+)?',r'/posts/\1/',url)
|
||||
|
||||
url = re.sub(r'(^[\'"]+|[\'"]+$)','',url) # strip leading or trailing '" from incorrect quoting.
|
||||
url = re.sub(r'like$','',url) # strip 'like' if incorrect 'like' link instead of proper post URL.
|
||||
|
||||
logger.debug("(ch:%s)used chapurl:%s"%(len(self.chapterUrls)+1,url))
|
||||
(is_chapter_url,url) = self._is_normalize_chapterurl(url)
|
||||
if is_chapter_url:
|
||||
self.chapterUrls.append((name,url))
|
||||
if url == useurl and first_post_title == self.chapterUrls[0][0] \
|
||||
and not self.getConfig('always_include_first_post',False):
|
||||
@@ -286,22 +323,6 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
## there's some history of stories with links to the wrong
|
||||
## page. This changes page#post URLs to perma-link URLs.
|
||||
## Which will be redirected back to page#posts, but the
|
||||
## *correct* ones.
|
||||
# http://forums.sufficientvelocity.com/threads/harry-potter-and-the-not-fatal-at-all-cultural-exchange-program.330/page-4#post-39915
|
||||
# https://forums.sufficientvelocity.com/posts/39915/
|
||||
if '#post-' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('#post-')[1]+'/'
|
||||
|
||||
## Same as above except for for case where author mistakenly
|
||||
## used the reply link instead of normal link to post.
|
||||
# "http://forums.spacebattles.com/threads/manager-worm-story-thread-iv.301602/reply?quote=15962513"
|
||||
# https://forums.spacebattles.com/posts/
|
||||
if 'reply?quote=' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('reply?quote=')[1]+'/'
|
||||
|
||||
try:
|
||||
origurl = url
|
||||
(data,opened) = self._fetchUrlOpened(url)
|
||||
@@ -309,20 +330,20 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
if '#' in origurl and '#' not in url:
|
||||
url = url + origurl[origurl.index('#'):]
|
||||
logger.debug("chapter URL redirected to: %s"%url)
|
||||
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
|
||||
if '#' in url:
|
||||
anchorid = url.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
|
||||
|
||||
bq = soup.find('blockquote')
|
||||
|
||||
|
||||
bq.name='div'
|
||||
|
||||
|
||||
for iframe in bq.find_all('iframe'):
|
||||
iframe.extract() # calibre book reader & editor don't like iframes to youtube.
|
||||
|
||||
|
||||
for qdiv in bq.find_all('div',{'class':'quoteExpand'}):
|
||||
qdiv.extract() # Remove <div class="quoteExpand">click to expand</div>
|
||||
|
||||
@@ -330,7 +351,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
## include lazy load images.
|
||||
for img in bq.find_all('img',{'class':'lazyload'}):
|
||||
img['src'] = img['data-src']
|
||||
|
||||
|
||||
except Exception as e:
|
||||
if self.getConfig('continue_on_chapter_error'):
|
||||
bq = self.make_soup("""<div>
|
||||
|
||||
+200
-112
@@ -26,6 +26,8 @@ import pprint
|
||||
import string
|
||||
import sys
|
||||
|
||||
version="2.5.1"
|
||||
|
||||
if sys.version_info < (2, 5):
|
||||
print 'This program requires Python 2.5 or newer.'
|
||||
sys.exit(1)
|
||||
@@ -43,13 +45,13 @@ try:
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.configurable import Configuration
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.epubutils import (
|
||||
get_dcsource_chaptercount, get_update_data, reset_orig_chapters_epub)
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.geturls import get_urls_from_page
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.geturls import get_urls_from_page, get_urls_from_imap
|
||||
except ImportError:
|
||||
from fanficfare import adapters, writers, exceptions
|
||||
from fanficfare.configurable import Configuration
|
||||
from fanficfare.epubutils import (
|
||||
get_dcsource_chaptercount, get_update_data, reset_orig_chapters_epub)
|
||||
from fanficfare.geturls import get_urls_from_page
|
||||
from fanficfare.geturls import get_urls_from_page, get_urls_from_imap
|
||||
|
||||
|
||||
def write_story(config, adapter, writeformat, metaonly=False, outstream=None):
|
||||
@@ -59,13 +61,15 @@ def write_story(config, adapter, writeformat, metaonly=False, outstream=None):
|
||||
del writer
|
||||
return output_filename
|
||||
|
||||
|
||||
def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=None):
|
||||
def main(argv=None,
|
||||
parser=None,
|
||||
passed_defaultsini=None,
|
||||
passed_personalini=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
# read in args, anything starting with -- will be treated as --<varible>=<value>
|
||||
if not parser:
|
||||
parser = OptionParser('usage: %prog [options] storyurl')
|
||||
parser = OptionParser('usage: %prog [options] [STORYURL]...')
|
||||
parser.add_option('-f', '--format', dest='format', default='epub',
|
||||
help='write story as FORMAT, epub(default), mobi, text or html', metavar='FORMAT')
|
||||
|
||||
@@ -88,41 +92,68 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
help='Retrieve metadata and stop. Or, if --update-epub, update metadata title page only.', )
|
||||
parser.add_option('-u', '--update-epub',
|
||||
action='store_true', dest='update',
|
||||
help='Update an existing epub with new chapters, give epub filename instead of storyurl.', )
|
||||
parser.add_option('--unnew',
|
||||
action='store_true', dest='unnew',
|
||||
help='Remove (new) chapter marks left by mark_new_chapters setting.', )
|
||||
help='Update an existing epub(if present) with new chapters. Give either epub filename or story URL.', )
|
||||
parser.add_option('--update-cover',
|
||||
action='store_true', dest='updatecover',
|
||||
help='Update cover in an existing epub, otherwise existing cover (if any) is used on update. Only valid with --update-epub.', )
|
||||
parser.add_option('--unnew',
|
||||
action='store_true', dest='unnew',
|
||||
help='Remove (new) chapter marks left by mark_new_chapters setting.', )
|
||||
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).',
|
||||
help='Give a filename to read for URLs (and/or existing EPUB files with --update-epub).',
|
||||
dest='infile', default=None,
|
||||
metavar='INFILE')
|
||||
|
||||
parser.add_option('-l', '--list',
|
||||
action='store_true', dest='list',
|
||||
dest='list', default=None, metavar='URL',
|
||||
help='Get list of valid story URLs from page given.', )
|
||||
parser.add_option('-n', '--normalize-list',
|
||||
action='store_true', dest='normalize', default=False,
|
||||
dest='normalize', default=None, metavar='URL',
|
||||
help='Get list of valid story URLs from page given, but normalized to standard forms.', )
|
||||
parser.add_option('--download-list',
|
||||
dest='downloadlist', default=None, metavar='URL',
|
||||
help='Download story URLs retrieved from page given. Update existing EPUBs if used with --update-epub.', )
|
||||
|
||||
parser.add_option('--imap',
|
||||
action='store_true', dest='imaplist',
|
||||
help='Get list of valid story URLs from unread email from IMAP account configured in ini.', )
|
||||
|
||||
parser.add_option('--download-imap',
|
||||
action='store_true', dest='downloadimap',
|
||||
help='Download valid story URLs from unread email from IMAP account configured in ini. Update existing EPUBs if used with --update-epub.', )
|
||||
|
||||
parser.add_option('-s', '--sites-list',
|
||||
action='store_true', dest='siteslist', default=False,
|
||||
help='Get list of valid story URLs examples.', )
|
||||
parser.add_option('-d', '--debug',
|
||||
action='store_true', dest='debug',
|
||||
help='Show debug output while downloading.', )
|
||||
help='Show debug and notice output.', )
|
||||
parser.add_option('-v', '--version',
|
||||
action='store_true', dest='version',
|
||||
help='Display version and quit.', )
|
||||
|
||||
options, args = parser.parse_args(argv)
|
||||
|
||||
if options.version:
|
||||
print("Version: %s" % version)
|
||||
return
|
||||
|
||||
if not options.debug:
|
||||
logger = logging.getLogger('fanficfare')
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
if not (options.siteslist or options.infile) and len(args) != 1:
|
||||
parser.error('incorrect number of arguments')
|
||||
list_only = any((options.imaplist,
|
||||
options.siteslist,
|
||||
options.list,
|
||||
options.normalize,
|
||||
))
|
||||
|
||||
if list_only and (args or any((options.downloadimap,
|
||||
options.downloadlist))):
|
||||
parser.error('Incorrect arguments: Cannot download and list URLs at the same time.')
|
||||
|
||||
if options.siteslist:
|
||||
for site, examples in adapters.getSiteExamples():
|
||||
@@ -137,43 +168,85 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
if options.unnew and options.format != 'epub':
|
||||
parser.error('--unnew only works with epub')
|
||||
|
||||
urls=args
|
||||
|
||||
if not list_only and not (args or any((options.infile,
|
||||
options.downloadimap,
|
||||
options.downloadlist))):
|
||||
parser.print_help();
|
||||
return
|
||||
|
||||
if options.list:
|
||||
configuration = get_configuration(options.list,
|
||||
passed_defaultsini,
|
||||
passed_personalini,options)
|
||||
retlist = get_urls_from_page(options.list, configuration)
|
||||
print '\n'.join(retlist)
|
||||
|
||||
if options.normalize:
|
||||
configuration = get_configuration(options.normalize,
|
||||
passed_defaultsini,
|
||||
passed_personalini,options)
|
||||
retlist = get_urls_from_page(options.normalize, configuration,normalize=True)
|
||||
print '\n'.join(retlist)
|
||||
|
||||
if options.downloadlist:
|
||||
configuration = get_configuration(options.downloadlist,
|
||||
passed_defaultsini,
|
||||
passed_personalini,options)
|
||||
retlist = get_urls_from_page(options.downloadlist, configuration)
|
||||
urls.extend(retlist)
|
||||
|
||||
if options.imaplist or options.downloadimap:
|
||||
# list doesn't have a supported site.
|
||||
configuration = get_configuration('test1.com',passed_defaultsini,passed_personalini,options)
|
||||
markread = configuration.getConfig('imap_mark_read') == 'true' or \
|
||||
(configuration.getConfig('imap_mark_read') == 'downloadonly' and options.downloadimap)
|
||||
retlist = get_urls_from_imap(configuration.getConfig('imap_server'),
|
||||
configuration.getConfig('imap_username'),
|
||||
configuration.getConfig('imap_password'),
|
||||
configuration.getConfig('imap_folder'),
|
||||
markread)
|
||||
|
||||
if options.downloadimap:
|
||||
urls.extend(retlist)
|
||||
else:
|
||||
print '\n'.join(retlist)
|
||||
|
||||
# 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 '#' in url:
|
||||
url = url[:url.find('#')].strip()
|
||||
url = url.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)
|
||||
if not list_only:
|
||||
if len(urls) < 1:
|
||||
print "No valid story URLs found"
|
||||
else:
|
||||
for url in urls:
|
||||
try:
|
||||
do_download(url,
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini)
|
||||
#print("pagecache:%s"%options.pagecache.keys())
|
||||
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)
|
||||
except Exception, e:
|
||||
if len(urls) == 1:
|
||||
raise
|
||||
print "URL(%s) Failed: Exception (%s). Run URL individually for more detail."%(url,e)
|
||||
|
||||
# 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
|
||||
@@ -182,7 +255,7 @@ def do_download(arg,
|
||||
# remove mark_new_chapters marks
|
||||
reset_orig_chapters_epub(arg,arg)
|
||||
return
|
||||
|
||||
|
||||
if options.update:
|
||||
try:
|
||||
url, chaptercount = get_dcsource_chaptercount(arg)
|
||||
@@ -197,71 +270,13 @@ def do_download(arg,
|
||||
url = arg
|
||||
else:
|
||||
url = arg
|
||||
|
||||
try:
|
||||
configuration = Configuration(adapters.getConfigSectionsFor(url), options.format)
|
||||
except exceptions.UnknownSite, e:
|
||||
if options.list or options.normalize:
|
||||
# list for page doesn't have to be a supported site.
|
||||
configuration = Configuration('test1.com', options.format)
|
||||
else:
|
||||
raise e
|
||||
|
||||
conflist = []
|
||||
homepath = join(expanduser('~'), '.fanficdownloader')
|
||||
## also look for .fanficfare now, give higher priority than old dir.
|
||||
homepath2 = join(expanduser('~'), '.fanficfare')
|
||||
|
||||
if passed_defaultsini:
|
||||
configuration.readfp(passed_defaultsini)
|
||||
|
||||
# don't need to check existance for our selves.
|
||||
conflist.append(join(dirname(__file__), 'defaults.ini'))
|
||||
conflist.append(join(homepath, 'defaults.ini'))
|
||||
conflist.append(join(homepath2, 'defaults.ini'))
|
||||
conflist.append('defaults.ini')
|
||||
|
||||
if passed_personalini:
|
||||
configuration.readfp(passed_personalini)
|
||||
|
||||
conflist.append(join(homepath, 'personal.ini'))
|
||||
conflist.append(join(homepath2, 'personal.ini'))
|
||||
conflist.append('personal.ini')
|
||||
|
||||
if options.configfile:
|
||||
conflist.extend(options.configfile)
|
||||
|
||||
logging.debug('reading %s config file(s), if present' % conflist)
|
||||
configuration.read(conflist)
|
||||
|
||||
try:
|
||||
configuration.add_section('overrides')
|
||||
except ConfigParser.DuplicateSectionError:
|
||||
pass
|
||||
|
||||
if options.force:
|
||||
configuration.set('overrides', 'always_overwrite', 'true')
|
||||
|
||||
if options.update and chaptercount:
|
||||
configuration.set('overrides', 'output_filename', output_filename)
|
||||
|
||||
if options.update and not options.updatecover:
|
||||
configuration.set('overrides', 'never_make_cover', 'true')
|
||||
|
||||
# images only for epub, even if the user mistakenly turned it
|
||||
# on else where.
|
||||
if options.format not in ('epub', 'html'):
|
||||
configuration.set('overrides', 'include_images', 'false')
|
||||
|
||||
if options.options:
|
||||
for opt in options.options:
|
||||
(var, val) = opt.split('=')
|
||||
configuration.set('overrides', var, val)
|
||||
|
||||
if options.list or options.normalize:
|
||||
retlist = get_urls_from_page(arg, configuration, normalize=options.normalize)
|
||||
print '\n'.join(retlist)
|
||||
return
|
||||
configuration = get_configuration(url,
|
||||
passed_defaultsini,
|
||||
passed_personalini,
|
||||
options,
|
||||
chaptercount,
|
||||
output_filename)
|
||||
|
||||
try:
|
||||
adapter = adapters.getAdapter(configuration, url)
|
||||
@@ -269,10 +284,10 @@ def do_download(arg,
|
||||
if not hasattr(options,'pagecache'):
|
||||
options.pagecache = adapter.get_empty_pagecache()
|
||||
options.cookiejar = adapter.get_empty_cookiejar()
|
||||
|
||||
|
||||
adapter.set_pagecache(options.pagecache)
|
||||
adapter.set_cookiejar(options.cookiejar)
|
||||
|
||||
|
||||
adapter.setChaptersRange(options.begin, options.end)
|
||||
|
||||
# check for updating from URL (vs from file)
|
||||
@@ -290,17 +305,20 @@ def do_download(arg,
|
||||
if adapter.getConfig('include_images') and not adapter.getConfig('no_image_processing'):
|
||||
try:
|
||||
from calibre.utils.magick import Image
|
||||
|
||||
logging.debug('Using calibre.utils.magick')
|
||||
except ImportError:
|
||||
try:
|
||||
import Image
|
||||
|
||||
logging.debug('Using PIL')
|
||||
## Pillow is a more current fork of PIL library
|
||||
from PIL import Image
|
||||
logging.debug('Using Pillow')
|
||||
except ImportError:
|
||||
print "You have include_images enabled, but Python Image Library(PIL) isn't found.\nImages will be included full size in original format.\nContinue? (y/n)?"
|
||||
if not sys.stdin.readline().strip().lower().startswith('y'):
|
||||
return
|
||||
try:
|
||||
import Image
|
||||
logging.debug('Using PIL')
|
||||
except ImportError:
|
||||
print "You have include_images enabled, but Python Image Library(PIL) isn't found.\nImages will be included full size in original format.\nContinue? (y/n)?"
|
||||
if not sys.stdin.readline().strip().lower().startswith('y'):
|
||||
return
|
||||
|
||||
# three tries, that's enough if both user/pass & is_adult needed,
|
||||
# or a couple tries of one or the other
|
||||
@@ -360,7 +378,10 @@ def do_download(arg,
|
||||
output_filename = write_story(configuration, adapter, options.format, options.metaonly)
|
||||
|
||||
if not options.metaonly and adapter.getConfig('post_process_cmd'):
|
||||
metadata = adapter.story.metadata
|
||||
if adapter.getConfig('post_process_apply_filename_safepattern'):
|
||||
metadata = adapter.story.get_filename_safe_metadata()
|
||||
else:
|
||||
metadata = adapter.story.getAllMetadata()
|
||||
metadata['output_filename'] = output_filename
|
||||
call(string.Template(adapter.getConfig('post_process_cmd')).substitute(metadata), shell=True)
|
||||
|
||||
@@ -375,6 +396,73 @@ def do_download(arg,
|
||||
except exceptions.AccessDenied as ad:
|
||||
print ad
|
||||
|
||||
def get_configuration(url,
|
||||
passed_defaultsini,
|
||||
passed_personalini,
|
||||
options,
|
||||
chaptercount=None,
|
||||
output_filename=None):
|
||||
try:
|
||||
configuration = Configuration(adapters.getConfigSectionsFor(url), options.format)
|
||||
except exceptions.UnknownSite, e:
|
||||
if options.list or options.normalize or options.downloadlist:
|
||||
# list for page doesn't have to be a supported site.
|
||||
configuration = Configuration('test1.com', options.format)
|
||||
else:
|
||||
raise e
|
||||
|
||||
conflist = []
|
||||
homepath = join(expanduser('~'), '.fanficdownloader')
|
||||
## also look for .fanficfare now, give higher priority than old dir.
|
||||
homepath2 = join(expanduser('~'), '.fanficfare')
|
||||
|
||||
if passed_defaultsini:
|
||||
configuration.readfp(passed_defaultsini)
|
||||
|
||||
# don't need to check existance for our selves.
|
||||
conflist.append(join(dirname(__file__), 'defaults.ini'))
|
||||
conflist.append(join(homepath, 'defaults.ini'))
|
||||
conflist.append(join(homepath2, 'defaults.ini'))
|
||||
conflist.append('defaults.ini')
|
||||
|
||||
if passed_personalini:
|
||||
configuration.readfp(passed_personalini)
|
||||
|
||||
conflist.append(join(homepath, 'personal.ini'))
|
||||
conflist.append(join(homepath2, 'personal.ini'))
|
||||
conflist.append('personal.ini')
|
||||
|
||||
if options.configfile:
|
||||
conflist.extend(options.configfile)
|
||||
|
||||
logging.debug('reading %s config file(s), if present' % conflist)
|
||||
configuration.read(conflist)
|
||||
|
||||
try:
|
||||
configuration.add_section('overrides')
|
||||
except ConfigParser.DuplicateSectionError:
|
||||
pass
|
||||
|
||||
if options.force:
|
||||
configuration.set('overrides', 'always_overwrite', 'true')
|
||||
|
||||
if options.update and chaptercount and output_filename:
|
||||
configuration.set('overrides', 'output_filename', output_filename)
|
||||
|
||||
if options.update and not options.updatecover:
|
||||
configuration.set('overrides', 'never_make_cover', 'true')
|
||||
|
||||
# images only for epub, even if the user mistakenly turned it
|
||||
# on else where.
|
||||
if options.format not in ('epub', 'html'):
|
||||
configuration.set('overrides', 'include_images', 'false')
|
||||
|
||||
if options.options:
|
||||
for opt in options.options:
|
||||
(var, val) = opt.split('=')
|
||||
configuration.set('overrides', var, val)
|
||||
|
||||
return configuration
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+56
-39
@@ -40,7 +40,7 @@ import adapters
|
||||
def re_compile(regex,line):
|
||||
try:
|
||||
return re.compile(regex)
|
||||
except Exception, e:
|
||||
except Exception, e:
|
||||
raise exceptions.RegularExpresssionFailed(e,regex,line)
|
||||
|
||||
# fall back labels.
|
||||
@@ -59,6 +59,7 @@ titleLabels = {
|
||||
'warnings':'Warnings',
|
||||
'numChapters':'Chapters',
|
||||
'numWords':'Words',
|
||||
'words_added':'Words Added', # logpage only
|
||||
'site':'Site',
|
||||
'storyId':'Story ID',
|
||||
'authorId':'Author ID',
|
||||
@@ -78,7 +79,7 @@ formatsections = ['html','txt','epub','mobi']
|
||||
othersections = ['defaults','overrides']
|
||||
|
||||
def get_valid_sections():
|
||||
sites = adapters.getConfigSections()
|
||||
sites = adapters.getConfigSections()
|
||||
sitesections = list(othersections)
|
||||
for section in sites:
|
||||
sitesections.append(section)
|
||||
@@ -90,7 +91,7 @@ def get_valid_sections():
|
||||
else:
|
||||
# add w/ www if doesn't www
|
||||
sitesections.append('www.%s'%section)
|
||||
|
||||
|
||||
allowedsections = []
|
||||
allowedsections.extend(formatsections)
|
||||
|
||||
@@ -99,7 +100,7 @@ def get_valid_sections():
|
||||
for f in formatsections:
|
||||
allowedsections.append('%s:%s'%(section,f))
|
||||
return allowedsections
|
||||
|
||||
|
||||
def get_valid_list_entries():
|
||||
return list(['category',
|
||||
'genre',
|
||||
@@ -127,6 +128,12 @@ def get_valid_set_options():
|
||||
This is to further restrict keywords to certain sections and/or
|
||||
values. get_valid_keywords() below is the list of allowed
|
||||
keywords. Any keyword listed here must also be listed there.
|
||||
|
||||
This is what's used by the code when you save personal.ini in
|
||||
plugin that stops and points out possible errors in keyword
|
||||
*values*. It doesn't flag 'bad' keywords. Note that it's
|
||||
separate from color highlighting and most keywords need to be
|
||||
added to both.
|
||||
'''
|
||||
|
||||
valdict = {'collect_series':(None,None,boollist),
|
||||
@@ -144,15 +151,15 @@ def get_valid_set_options():
|
||||
'strip_chapter_numbers':(None,None,boollist),
|
||||
'mark_new_chapters':(None,None,boollist),
|
||||
'titlepage_use_table':(None,None,boollist),
|
||||
|
||||
|
||||
'use_ssl_unverified_context':(None,None,boollist),
|
||||
|
||||
|
||||
'add_chapter_numbers':(None,None,boollist+['toconly']),
|
||||
|
||||
|
||||
'check_next_chapter':(['fanfiction.net'],None,boollist),
|
||||
'tweak_fg_sleep':(['fanfiction.net'],None,boollist),
|
||||
'skip_author_cover':(['fanfiction.net'],None,boollist),
|
||||
|
||||
|
||||
'fix_fimf_blockquotes':(['fimfiction.net'],None,boollist),
|
||||
'fail_on_password':(['fimfiction.net'],None,boollist),
|
||||
'do_update_hook':(['fimfiction.net',
|
||||
@@ -174,15 +181,17 @@ def get_valid_set_options():
|
||||
# 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']),
|
||||
'logpage_at_end':(None,['epub'],boollist),
|
||||
|
||||
|
||||
'windows_eol':(None,['txt'],boollist),
|
||||
|
||||
|
||||
'include_images':(None,['epub','html'],boollist),
|
||||
'grayscale_images':(None,['epub','html'],boollist),
|
||||
'no_image_processing':(None,['epub','html'],boollist),
|
||||
'normalize_text_links':(None,['epub','html'],boollist),
|
||||
'internalize_text_links':(None,['epub','html'],boollist),
|
||||
|
||||
'capitalize_forumtags':(base_xenforo_list,None,boollist),
|
||||
'continue_on_chapter_error':(base_xenforo_list,None,boollist),
|
||||
@@ -205,6 +214,7 @@ def get_valid_scalar_entries():
|
||||
'rating',
|
||||
'numChapters',
|
||||
'numWords',
|
||||
'words_added', # logpage only.
|
||||
'site',
|
||||
'storyId',
|
||||
'title',
|
||||
@@ -227,6 +237,11 @@ def get_valid_entries():
|
||||
|
||||
# *known* keywords -- or rather regexps for them.
|
||||
def get_valid_keywords():
|
||||
'''
|
||||
Among other things, this list is used by the color highlighting in
|
||||
personal.ini editing in plugin. Note that it's separate from
|
||||
value checking and most keywords need to be added to both.
|
||||
'''
|
||||
return list(['(in|ex)clude_metadata_(pre|post)',
|
||||
'add_chapter_numbers',
|
||||
'add_genre_when_multi_category',
|
||||
@@ -359,7 +374,9 @@ def get_valid_keywords():
|
||||
'minimum_threadmarks',
|
||||
'first_post_title',
|
||||
'always_include_first_post',
|
||||
'',
|
||||
'always_reload_first_chapter',
|
||||
'normalize_text_links',
|
||||
'internalize_text_links',
|
||||
])
|
||||
|
||||
# *known* entry keywords -- or rather regexps for them.
|
||||
@@ -376,9 +393,9 @@ def make_generate_cover_settings(param):
|
||||
(template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
re_compile(regexp,line)
|
||||
vlist.append((template,regexp,setting))
|
||||
except Exception, e:
|
||||
except Exception, e:
|
||||
raise exceptions.PersonalIniFailed(e,line,param)
|
||||
|
||||
|
||||
return vlist
|
||||
|
||||
|
||||
@@ -389,9 +406,9 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
ConfigParser.SafeConfigParser.__init__(self)
|
||||
|
||||
self.lightweight = lightweight
|
||||
|
||||
|
||||
self.linenos=dict() # key by section or section,key -> lineno
|
||||
|
||||
|
||||
## [injected] section has even less priority than [defaults]
|
||||
self.sectionslist = ['defaults','injected']
|
||||
|
||||
@@ -399,17 +416,17 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
## but before site-specific.
|
||||
for section in sections[:-1]:
|
||||
self.addConfigSection(section)
|
||||
|
||||
|
||||
if site.startswith("www."):
|
||||
sitewith = site
|
||||
sitewithout = site.replace("www.","")
|
||||
else:
|
||||
sitewith = "www."+site
|
||||
sitewithout = site
|
||||
|
||||
|
||||
self.addConfigSection(sitewith)
|
||||
self.addConfigSection(sitewithout)
|
||||
|
||||
|
||||
if fileform:
|
||||
self.addConfigSection(fileform)
|
||||
## add other sections:fileform (not including site DN)
|
||||
@@ -419,9 +436,9 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
self.addConfigSection(sitewith+":"+fileform)
|
||||
self.addConfigSection(sitewithout+":"+fileform)
|
||||
self.addConfigSection("overrides")
|
||||
|
||||
|
||||
self.listTypeEntries = get_valid_list_entries()
|
||||
|
||||
|
||||
self.validEntries = get_valid_entries()
|
||||
|
||||
self.url_config_set = False
|
||||
@@ -446,7 +463,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
|
||||
def isListType(self,key):
|
||||
return key in self.listTypeEntries or self.hasConfig("include_in_"+key)
|
||||
|
||||
|
||||
def isValidMetaEntry(self, key):
|
||||
return key in self.getValidMetaList()
|
||||
|
||||
@@ -476,7 +493,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
# used by adapters & writers, non-convention naming style
|
||||
def getConfig(self, key, default=""):
|
||||
return self.get_config(self.sectionslist,key,default)
|
||||
|
||||
|
||||
def get_config(self, sections, key, default=""):
|
||||
val = default
|
||||
for section in sections:
|
||||
@@ -496,7 +513,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
#print "getConfig(add_to_%s)=[%s]%s" % (key,section,val)
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e:
|
||||
pass
|
||||
|
||||
|
||||
return val
|
||||
|
||||
# split and strip each.
|
||||
@@ -508,7 +525,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
return default
|
||||
else:
|
||||
return vlist
|
||||
|
||||
|
||||
# used by adapters & writers, non-convention naming style
|
||||
def getConfigList(self, key, default=[]):
|
||||
return self.get_config_list(self.sectionslist, key, default)
|
||||
@@ -522,7 +539,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
return self.linenos.get(section+','+key,None)
|
||||
else:
|
||||
return self.linenos.get(section,None)
|
||||
|
||||
|
||||
## Copied from Python 2.7 library so as to make read utf8.
|
||||
def read(self, filenames):
|
||||
"""Read and parse a filename or a list of filenames.
|
||||
@@ -546,7 +563,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
fp.close()
|
||||
read_ok.append(filename)
|
||||
return read_ok
|
||||
|
||||
|
||||
## Copied from Python 2.7 library so as to make it save linenos too.
|
||||
#
|
||||
# Regular expressions for parsing section headers and options.
|
||||
@@ -626,7 +643,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
optval = ''
|
||||
optname = self.optionxform(optname.rstrip())
|
||||
cursect[optname] = optval
|
||||
self.linenos[cursect['__name__']+','+optname]=lineno
|
||||
self.linenos[cursect['__name__']+','+optname]=lineno
|
||||
else:
|
||||
# a non-fatal parsing error occurred. set up the
|
||||
# exception but keep going. the exception will be
|
||||
@@ -654,11 +671,11 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
from story import set_in_ex_clude, make_replacements
|
||||
|
||||
custom_columns_settings_re = re.compile(r'(add_to_)?custom_columns_settings')
|
||||
|
||||
|
||||
generate_cover_settings_re = re.compile(r'(add_to_)?generate_cover_settings')
|
||||
|
||||
|
||||
valdict = get_valid_set_options()
|
||||
|
||||
|
||||
for section in self.sections():
|
||||
allow_all_section = allow_all_sections_re.match(section)
|
||||
if section not in allowedsections and not allow_all_section:
|
||||
@@ -674,17 +691,17 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
elif sitename in othersections:
|
||||
formatname = None
|
||||
sitename = None
|
||||
|
||||
|
||||
## check each keyword in section. Due to precedence
|
||||
## order of sections, it's possible for bad lines to
|
||||
## never be used.
|
||||
for keyword,value in self.items(section):
|
||||
try:
|
||||
|
||||
|
||||
## check regex bearing keywords first. Each
|
||||
## will raise exceptions if flawed.
|
||||
if clude_metadata_re.match(keyword):
|
||||
set_in_ex_clude(value)
|
||||
set_in_ex_clude(value)
|
||||
|
||||
if replace_metadata_re.match(keyword):
|
||||
make_replacements(value)
|
||||
@@ -716,7 +733,7 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
## used with CLI/web yet.
|
||||
|
||||
except Exception as e:
|
||||
errors.append((self.get_lineno(section,keyword),"Error:%s in (%s:%s)"%(e,keyword,value)))
|
||||
errors.append((self.get_lineno(section,keyword),"Error:%s in (%s:%s)"%(e,keyword,value)))
|
||||
|
||||
return errors
|
||||
|
||||
@@ -731,7 +748,7 @@ class Configurable(object):
|
||||
|
||||
def addUrlConfigSection(self,url):
|
||||
self.configuration.addUrlConfigSection(url)
|
||||
|
||||
|
||||
def isListType(self,key):
|
||||
return self.configuration.isListType(key)
|
||||
|
||||
@@ -740,10 +757,10 @@ class Configurable(object):
|
||||
|
||||
def getValidMetaList(self):
|
||||
return self.configuration.getValidMetaList()
|
||||
|
||||
|
||||
def hasConfig(self, key):
|
||||
return self.configuration.hasConfig(key)
|
||||
|
||||
return self.configuration.hasConfig(key)
|
||||
|
||||
def has_config(self, sections, key):
|
||||
return self.configuration.has_config(sections, key)
|
||||
|
||||
|
||||
+79
-6
@@ -180,7 +180,7 @@ extratags: FanFiction
|
||||
## Can also be used for other metadata values
|
||||
#default_value_category:FanFiction
|
||||
|
||||
## number of seconds to sleep between calls to the story site. May by
|
||||
## number of seconds to sleep between calls to the story site. May be
|
||||
## useful if pulling large numbers of stories or if the site is slow.
|
||||
#slow_down_sleep_time:0.5
|
||||
|
||||
@@ -189,11 +189,17 @@ extratags: FanFiction
|
||||
## prevent excessive wait when your network or the site is down.
|
||||
connect_timeout:60.0
|
||||
|
||||
## For use only with stand-alone CLI version--run a command on the
|
||||
## generated file after it's produced. All of the titlepage_entries
|
||||
## values are available, plus output_filename.
|
||||
## For use only with CLI version--run a command on the generated file
|
||||
## after it's produced. All of the titlepage_entries values are
|
||||
## available, plus output_filename.
|
||||
#post_process_cmd: addbook -f "${output_filename}" -t "${title}"
|
||||
|
||||
## Some operating systems and command shells have problems with some
|
||||
## characters. When true, the output_filename_safepattern will be
|
||||
## applied to each metadata item passed to post_process_cmd before
|
||||
## it's called.
|
||||
#post_process_apply_filename_safepattern:false
|
||||
|
||||
## Use regular expressions to find and replace (or remove) metadata.
|
||||
## For example, you could change Sci-Fi=>SF, remove *-Centered tags,
|
||||
## etc. See http://docs.python.org/library/re.html (look for re.sub)
|
||||
@@ -404,6 +410,40 @@ user_agent:FFF/2.X
|
||||
## non-intuitive.
|
||||
#description_limit:1000
|
||||
|
||||
## The FFF CLI can fetch story URLs from unread emails when configured
|
||||
## to read from your IMAP mail server. The example shows GMail, but
|
||||
## other services that support IMAP can be used. GMail requires you
|
||||
## to turn on an option to enable IMAP access. Only the CLI uses these
|
||||
## options--the Calibre Plugin stores these separately.
|
||||
##
|
||||
## It's safest if you create a separate email account that you use
|
||||
## only for your story update notices. FanFicFare cannot guarantee
|
||||
## that malicious code cannot get your email password once you've
|
||||
## saved it. Use this feature at your own risk.
|
||||
##
|
||||
#imap_server:imap.gmail.com
|
||||
#imap_username:youraddress@gmail.com
|
||||
#imap_password:XXXXXXXX
|
||||
#imap_folder:INBOX
|
||||
|
||||
## Mark mails with story URLs read:
|
||||
## imap_mark_read can be 'true', 'false'(default) or 'downloadonly'.
|
||||
##
|
||||
## If 'true', unread emails will be marked as read when
|
||||
## either CLI option --imap to list the story URLs or --download-imap
|
||||
## to download story URLs from email are used.
|
||||
##
|
||||
## If 'downloadonly', unread emails will be marked as read
|
||||
## only when CLI --download-imap to download story URLs from email are
|
||||
## used.
|
||||
##
|
||||
## If 'false', unread emails will not be marked as read.
|
||||
##
|
||||
## Only unread emails will be searched for story URLs, and only emails
|
||||
## containing valid story URLs will ever be marked read.
|
||||
##
|
||||
#imap_mark_read:true
|
||||
|
||||
[base_efiction]
|
||||
## At the time of writing, eFiction Base adapters allow downloading
|
||||
## the whole story in bulk using the 'Print' feature. If 'bulk_load'
|
||||
@@ -726,6 +766,18 @@ remove_transparency: true
|
||||
## true--replace_br_with_p also fixes the problem.
|
||||
nook_img_fix:true
|
||||
|
||||
## Apply adapter's normalize_chapterurl() to all links in chapter
|
||||
## texts, if they match chapter URLs. Currently only implemented by
|
||||
## base_xenforoforum adapters.
|
||||
#normalize_text_links:false
|
||||
|
||||
## Search all links in chapter texts and, if they match any included
|
||||
## chapter URLs, replace them with links to the chapter in the
|
||||
## download. Only works with epub and html output formats.
|
||||
## base_xenforoforum adapters should also use normalize_text_links
|
||||
## with this.
|
||||
#internalize_text_links:false
|
||||
|
||||
[mobi]
|
||||
## mobi TOC cannot be turned off right now.
|
||||
#include_tocpage: true
|
||||
@@ -1156,6 +1208,21 @@ romance_label: Romance
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[fictionhunt.com]
|
||||
## Archive only site for ffnet HP stories.
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
extra_valid_entries: origin,originUrl,originHTML,reviews
|
||||
originHTML_label:Original Story URL
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:origin
|
||||
|
||||
add_to_extra_titlepage_entries:originHTML
|
||||
|
||||
[fictionmania.tv]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
@@ -1527,6 +1594,11 @@ comments_label:Comments
|
||||
|
||||
include_in_category:category,searchtags
|
||||
|
||||
[royalroadl.com]
|
||||
extra_valid_entries:stars
|
||||
|
||||
#add_to_extra_titlepage_entries:,stars
|
||||
|
||||
[samandjack.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -2292,8 +2364,9 @@ extracharacters:Wolverine,Rogue
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: Atlantis
|
||||
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
##site stopped showing reviews ~ Oct 2016
|
||||
#extra_valid_entries:reviews
|
||||
#reviews_label:Reviews
|
||||
|
||||
[buffygiles.velocitygrass.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
|
||||
@@ -110,12 +110,14 @@ def get_update_data(inputio,
|
||||
if( item.getAttribute("media-type") == "application/xhtml+xml" ):
|
||||
href=relpath+item.getAttribute("href")
|
||||
#print("---- item href:%s path part: %s"%(href,get_path_part(href)))
|
||||
if re.match(r'.*/log_page\.x?html',href):
|
||||
if re.match(r'.*/log_page(_u\d+)?\.x?html',href):
|
||||
try:
|
||||
logfile = epub.read(href).decode("utf-8")
|
||||
except:
|
||||
pass # corner case I bumped into while testing.
|
||||
if re.match(r'.*/(file|chapter)\d+\.x?html',href):
|
||||
if re.match(r'.*/(file|chapter)\d+(_u\d+)?\.x?html',href):
|
||||
# (_u\d+)? is from calibre convert naming files
|
||||
# 3/OEBPS/file0005_u3.xhtml etc.
|
||||
if getsoups:
|
||||
soup = bs.BeautifulSoup(epub.read(href).decode("utf-8"),"html5lib")
|
||||
for img in soup.findAll('img'):
|
||||
|
||||
+17
-9
@@ -997,20 +997,28 @@ class Story(Configurable):
|
||||
|
||||
return retval
|
||||
|
||||
def get_filename_safe_metadata(self):
|
||||
origvalues = self.getAllMetadata()
|
||||
values={}
|
||||
pattern = re_compile(self.getConfig("output_filename_safepattern",
|
||||
r"(^\.|/\.|[^a-zA-Z0-9_\. \[\]\(\)&'-]+)"),
|
||||
"output_filename_safepattern")
|
||||
for k in origvalues.keys():
|
||||
if k == 'formatext': # don't do file extension--we set it anyway.
|
||||
values[k]=self.getMetadata(k)
|
||||
else:
|
||||
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
|
||||
return values
|
||||
|
||||
def formatFileName(self,template,allowunsafefilename=True):
|
||||
values = origvalues = self.getAllMetadata()
|
||||
# fall back default:
|
||||
if not template:
|
||||
template="${title}-${siteabbrev}_${storyId}${formatext}"
|
||||
|
||||
if not allowunsafefilename:
|
||||
values={}
|
||||
pattern = re_compile(self.getConfig("output_filename_safepattern",r"(^\.|/\.|[^a-zA-Z0-9_\. \[\]\(\)&'-]+)"),"output_filename_safepattern")
|
||||
for k in origvalues.keys():
|
||||
if k == 'formatext': # don't do file extension--we set it anyway.
|
||||
values[k]=self.getMetadata(k)
|
||||
else:
|
||||
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
|
||||
if allowunsafefilename:
|
||||
values = self.getAllMetadata()
|
||||
else:
|
||||
values = self.get_filename_safe_metadata()
|
||||
|
||||
return string.Template(template).substitute(values).encode('utf8')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -27,8 +27,11 @@ import re
|
||||
## use DOM to generate the XML files.
|
||||
from xml.dom.minidom import parse, parseString, getDOMImplementation
|
||||
|
||||
import bs4
|
||||
|
||||
from base_writer import *
|
||||
from ..htmlcleanup import stripHTML,removeEntities
|
||||
from ..story import commaGroups
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -206,7 +209,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
# to add to.
|
||||
if self.story.logfile:
|
||||
logger.debug("existing logfile found, appending")
|
||||
logger.debug("existing data:%s"%self._getLastLogData(self.story.logfile))
|
||||
# logger.debug("existing data:%s"%self._getLastLogData(self.story.logfile))
|
||||
replace_string = "</body>" # "</h3>"
|
||||
self._write(out,self.story.logfile.replace(replace_string,self._makeLogEntry(self._getLastLogData(self.story.logfile))+replace_string))
|
||||
else:
|
||||
@@ -253,6 +256,14 @@ div { margin: 0pt; padding: 0pt; }
|
||||
|
||||
retval = START.substitute(self.story.getAllMetadata())
|
||||
|
||||
## words_added is only used in logpage because it's the only
|
||||
## place we know the previous version's word count.
|
||||
if 'words_added' in (self.getConfigList("logpage_entries") + self.getConfigList("extra_logpage_entries")):
|
||||
new_words = self.story.getMetadata('numWords')
|
||||
old_words = oldvalues.get('numWords',None)
|
||||
if new_words and old_words:
|
||||
self.story.setMetadata('words_added',commaGroups(unicode(int(new_words.replace(',',''))-int(old_words.replace(',','')))))
|
||||
|
||||
for entry in self.getConfigList("logpage_entries") + self.getConfigList("extra_logpage_entries"):
|
||||
if self.isValidMetaEntry(entry):
|
||||
val = self.story.getMetadata(entry)
|
||||
@@ -502,6 +513,8 @@ div { margin: 0pt; padding: 0pt; }
|
||||
(self.story.logfile or self.story.getMetadataRaw("status") == "In-Progress") ) \
|
||||
or self.getConfig("include_logpage") == "true"
|
||||
|
||||
## collect chapter urls and file names for internalize_text_links option.
|
||||
chapurlmap = {}
|
||||
for index, chap in enumerate(self.story.getChapters(fortoc=True)):
|
||||
if chap.html:
|
||||
i=index+1
|
||||
@@ -510,6 +523,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
"application/xhtml+xml",
|
||||
chap.title))
|
||||
itemrefs.append("file%04d"%i)
|
||||
chapurlmap[chap.url]="file%04d.xhtml"%i # url -> relative epub file name.
|
||||
|
||||
if dologpage:
|
||||
if self.getConfig("logpage_at_end") == "true":
|
||||
@@ -659,6 +673,20 @@ div { margin: 0pt; padding: 0pt; }
|
||||
|
||||
for index, chap in enumerate(self.story.getChapters()): # (url,title,html)
|
||||
if chap.html:
|
||||
chap_data = chap.html
|
||||
if self.getConfig('internalize_text_links'):
|
||||
soup = bs4.BeautifulSoup(chap.html,'html5lib')
|
||||
changed=False
|
||||
for alink in soup.find_all('a'):
|
||||
if alink.has_attr('href') and alink['href'] in chapurlmap:
|
||||
alink['href']=chapurlmap[alink['href']]
|
||||
changed=True
|
||||
if changed:
|
||||
chap_data = unicode(soup)
|
||||
# Don't want html, head or body tags in
|
||||
# chapter html--bs4 insists on adding them.
|
||||
chap_data = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",chap_data)
|
||||
|
||||
#logger.debug('Writing chapter text for: %s' % chap.title)
|
||||
vals={'url':removeEntities(chap.url),
|
||||
'chapter':removeEntities(chap.title),
|
||||
@@ -670,7 +698,9 @@ div { margin: 0pt; padding: 0pt; }
|
||||
for k,v in vals.items():
|
||||
if isinstance(v,basestring): vals[k]=v.replace('"','"')
|
||||
fullhtml = CHAPTER_START.substitute(vals) + \
|
||||
chap.html + CHAPTER_END.substitute(vals)
|
||||
chap_data.strip() + \
|
||||
CHAPTER_END.substitute(vals)
|
||||
# strip to avoid ever growning numbers of newlines.
|
||||
# ffnet(& maybe others) gives the whole chapter text
|
||||
# as one line. This causes problems for nook(at
|
||||
# least) when the chapter size starts getting big
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,8 @@
|
||||
import logging
|
||||
import string
|
||||
|
||||
import bs4
|
||||
|
||||
from base_writer import *
|
||||
|
||||
class HTMLWriter(BaseStoryWriter):
|
||||
@@ -32,7 +34,7 @@ class HTMLWriter(BaseStoryWriter):
|
||||
|
||||
def __init__(self, config, story):
|
||||
BaseStoryWriter.__init__(self, config, story)
|
||||
|
||||
|
||||
self.HTML_FILE_START = string.Template('''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -48,7 +50,7 @@ ${output_css}
|
||||
self.HTML_COVER = string.Template('''
|
||||
<img src="${coverimg}" alt="cover" />
|
||||
''')
|
||||
|
||||
|
||||
self.HTML_TITLE_PAGE_START = string.Template('''
|
||||
<table class="full">
|
||||
''')
|
||||
@@ -62,14 +64,14 @@ ${output_css}
|
||||
''')
|
||||
|
||||
self.HTML_TOC_PAGE_START = string.Template('''
|
||||
<a name="TOCTOP"><h2>Table of Contents</h2>
|
||||
<a name="TOCTOP"><h2>Table of Contents</h2></a>
|
||||
<p>
|
||||
''')
|
||||
|
||||
self.HTML_TOC_ENTRY = string.Template('''
|
||||
<a href="#section${index}">${chapter}</a><br />
|
||||
''')
|
||||
|
||||
|
||||
self.HTML_TOC_PAGE_END = string.Template('''
|
||||
</p>
|
||||
''')
|
||||
@@ -100,12 +102,12 @@ ${output_css}
|
||||
FILE_END = string.Template(self.getConfig("file_end"))
|
||||
else:
|
||||
FILE_END = self.HTML_FILE_END
|
||||
|
||||
|
||||
self._write(out,FILE_START.substitute(self.story.getAllMetadata()))
|
||||
|
||||
if self.getConfig('include_images') and self.story.cover:
|
||||
self._write(out,COVER.substitute(dict(self.story.getAllMetadata().items()+{'coverimg':self.story.cover}.items())))
|
||||
|
||||
|
||||
self.writeTitlePage(out,
|
||||
self.HTML_TITLE_PAGE_START,
|
||||
self.HTML_TITLE_ENTRY,
|
||||
@@ -120,18 +122,43 @@ ${output_css}
|
||||
CHAPTER_START = string.Template(self.getConfig("chapter_start"))
|
||||
else:
|
||||
CHAPTER_START = self.HTML_CHAPTER_START
|
||||
|
||||
|
||||
if self.hasConfig('chapter_end'):
|
||||
CHAPTER_END = string.Template(self.getConfig("chapter_end"))
|
||||
else:
|
||||
CHAPTER_END = self.HTML_CHAPTER_END
|
||||
|
||||
|
||||
## collect chapter urls and file names for internalize_text_links option.
|
||||
chapurlmap = {}
|
||||
for index, chap in enumerate(self.story.getChapters()):
|
||||
if chap.html:
|
||||
## HTML_CHAPTER_START needs to have matching <a>
|
||||
## anchor to work. Which it does by default. This
|
||||
## could also be made configurable if some user
|
||||
## changed it.
|
||||
chapurlmap[chap.url]="#section%04d"%(index+1) # url -> index
|
||||
|
||||
for index, chap in enumerate(self.story.getChapters()):
|
||||
if chap.html:
|
||||
chap_data = chap.html
|
||||
if self.getConfig('internalize_text_links'):
|
||||
soup = bs4.BeautifulSoup(chap.html,'html5lib')
|
||||
changed=False
|
||||
for alink in soup.find_all('a'):
|
||||
if alink.has_attr('href') and alink['href'] in chapurlmap:
|
||||
alink['href']=chapurlmap[alink['href']]
|
||||
changed=True
|
||||
if changed:
|
||||
chap_data = unicode(soup)
|
||||
# Don't want html, head or body tags in
|
||||
# chapter html--bs4 insists on adding them.
|
||||
chap_data = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",chap_data)
|
||||
|
||||
|
||||
logging.debug('Writing chapter text for: %s' % chap.title)
|
||||
vals={'url':chap.url, 'chapter':chap.title, 'index':"%04d"%(index+1), 'number':index+1}
|
||||
self._write(out,CHAPTER_START.substitute(vals))
|
||||
self._write(out,chap.html)
|
||||
self._write(out,chap_data)
|
||||
self._write(out,CHAPTER_END.substitute(vals))
|
||||
|
||||
self._write(out,FILE_END.substitute(self.story.getAllMetadata()))
|
||||
@@ -139,4 +166,4 @@ ${output_css}
|
||||
if self.getConfig('include_images'):
|
||||
for imgmap in self.story.getImgUrls():
|
||||
self.writeFile(imgmap['newsrc'],imgmap['data'])
|
||||
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ from os import path
|
||||
# Get the long description from the relevant file
|
||||
with codecs.open('DESCRIPTION.rst', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
|
||||
setup(
|
||||
name="FanFicFare",
|
||||
|
||||
# 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.3.6",
|
||||
# Versions should comply with PEP440.
|
||||
version="2.5.1",
|
||||
|
||||
description='A tool for downloading fanfiction to eBook formats',
|
||||
long_description=long_description,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import codecs, sys, re
|
||||
|
||||
from tempfile import mkstemp
|
||||
from os import rename, close, unlink
|
||||
|
||||
#print sys.argv[1:]
|
||||
|
||||
## Files that contain version numbers that will need to be updated.
|
||||
version_files = [
|
||||
# 'version_test.sml',
|
||||
# 'version_test.txt',
|
||||
'setup.py',
|
||||
'calibre-plugin/__init__.py',
|
||||
'webservice/app.yaml',
|
||||
'fanficfare/cli.py',
|
||||
]
|
||||
|
||||
## save version from this file for index.html link.
|
||||
# save_file='version_test.txt'
|
||||
save_file='webservice/app.yaml'
|
||||
saved_version = None
|
||||
|
||||
|
||||
def main(args):
|
||||
## major.minor.micro
|
||||
'''
|
||||
version = (2, 3, 6)
|
||||
version="2.3.6",
|
||||
version: 2-3-06a
|
||||
version="2.3.6"
|
||||
'''
|
||||
version_re = \
|
||||
r'^(?P<prefix>[ ]*)version(?P<infix>[ =:"\\(]+)' \
|
||||
r'(?P<major>[0-9]+)(?P<dot1>[, \\.-]+)' \
|
||||
r'(?P<minor>[0-9]+)(?P<dot2>[, \\.-]+)' \
|
||||
r'(?P<micro>[0-9]+[a-z]?)(?P<suffix>[",\\)]*\r?\n)$'
|
||||
|
||||
version_subs = '\g<prefix>version\g<infix>%s\g<dot1>%s\g<dot2>%s\g<suffix>' % tuple(args)
|
||||
|
||||
do_loop(version_files, version_re, version_subs)
|
||||
|
||||
if saved_version:
|
||||
# index_files = ['index.html']
|
||||
index_files = ['webservice/index.html']
|
||||
index_re = 'http://([0-9-]+[a-z]?)\\.fanficfare\\.appspot\\.com'
|
||||
index_subs = 'http://%s-%s-%s.fanficfare.appspot.com'%saved_version
|
||||
do_loop(index_files, index_re, index_subs)
|
||||
|
||||
def do_loop(files, pattern, substring):
|
||||
global saved_version
|
||||
for source_file_path in files:
|
||||
print "src:"+source_file_path
|
||||
fh, target_file_path = mkstemp()
|
||||
with codecs.open(target_file_path, 'w', 'utf-8') as target_file:
|
||||
with codecs.open(source_file_path, 'r', 'utf-8') as source_file:
|
||||
for line in source_file:
|
||||
repline = re.sub(pattern, substring, line)
|
||||
if line != repline and source_file_path == save_file:
|
||||
m = re.match(pattern,line)
|
||||
saved_version = (m.group('major'),m.group('minor'),m.group('micro'))
|
||||
print("<-%s->%s"%(line,repline))
|
||||
target_file.write(repline)
|
||||
close(fh)
|
||||
unlink(source_file_path)
|
||||
rename(target_file_path,source_file_path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv[1:]
|
||||
try:
|
||||
if len(args) != 3:
|
||||
raise Exception()
|
||||
[int(x) for x in args]
|
||||
except:
|
||||
print "Requires exactly 3 numeric args: major minor micro"
|
||||
exit()
|
||||
main(args)
|
||||
# print saved_version
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanficfare
|
||||
application: fanficfare
|
||||
version: 2-3-06
|
||||
version: 2-5-0
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
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://2-3-05.fanficfare.appspot.com">previous version
|
||||
<a href="http://2-4-0.fanficfare.appspot.com">previous version
|
||||
</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
|
||||
+16
-23
@@ -45,11 +45,11 @@ from fanficfare import adapters, writers, exceptions
|
||||
from fanficfare.configurable import Configuration
|
||||
|
||||
class UserConfigServer(webapp2.RequestHandler):
|
||||
|
||||
|
||||
def getUserConfig(self,user,url,fileformat):
|
||||
|
||||
configuration = Configuration(adapters.getConfigSectionsFor(url),fileformat)
|
||||
|
||||
|
||||
logging.debug('reading defaults.ini config file')
|
||||
configuration.read('fanficfare/defaults.ini')
|
||||
|
||||
@@ -96,7 +96,7 @@ class MainHandler(webapp2.RequestHandler):
|
||||
template_values = {'login_url' : url, 'authorized': False}
|
||||
path = os.path.join(os.path.dirname(__file__), 'index.html')
|
||||
|
||||
|
||||
|
||||
template_values['supported_sites'] = '<dl>\n'
|
||||
for (site,examples) in adapters.getSiteExamples():
|
||||
template_values['supported_sites'] += "<dt>%s</dt>\n<dd>Example Story URLs:<br>"%site
|
||||
@@ -139,7 +139,7 @@ class EditConfigServer(UserConfigServer):
|
||||
self.redirect("/?error=configsaved")
|
||||
except Exception, e:
|
||||
logging.info("Saved Config Failed:%s"%e)
|
||||
self.redirect("/?error=custom&errtext=%s"%urlEscape(unicode(e)))
|
||||
self.redirect("/?error=custom&errtext=%s"%urllib.quote(unicode(e),''))
|
||||
else: # not update, assume display for edit
|
||||
if uconfig is not None and uconfig.config:
|
||||
config = uconfig.config
|
||||
@@ -240,7 +240,7 @@ class FileStatusServer(webapp2.RequestHandler):
|
||||
if download:
|
||||
logging.info("Status url: %s" % download.url)
|
||||
if download.completed and download.format=='epub':
|
||||
escaped_url = urlEscape(self.request.host_url+"/file/"+download.name+"."+download.format+"?id="+fileId+"&fake=file."+download.format)
|
||||
escaped_url = urllib.quote(self.request.host_url+"/file/"+download.name+"."+download.format+"?id="+fileId+"&fake=file."+download.format,'')
|
||||
else:
|
||||
download = DownloadMeta()
|
||||
download.failure = "Download not found"
|
||||
@@ -295,7 +295,7 @@ class RecentFilesServer(webapp2.RequestHandler):
|
||||
|
||||
for fic in fics:
|
||||
if fic.completed and fic.format == 'epub':
|
||||
fic.escaped_url = urlEscape(self.request.host_url+"/file/"+fic.name+"."+fic.format+"?id="+unicode(fic.key())+"&fake=file."+fic.format)
|
||||
fic.escaped_url = urllib.quote(self.request.host_url+"/file/"+fic.name+"."+fic.format+"?id="+unicode(fic.key())+"&fake=file."+fic.format,'')
|
||||
|
||||
template_values = dict(fics = fics, nickname = user.nickname())
|
||||
path = os.path.join(os.path.dirname(__file__), 'recent.html')
|
||||
@@ -313,7 +313,7 @@ class AllRecentFilesServer(webapp2.RequestHandler):
|
||||
q.order('-date')
|
||||
else:
|
||||
q.order('-count')
|
||||
|
||||
|
||||
fics = q.fetch(200)
|
||||
logging.info("Recent fetched %d downloads for user %s."%(len(fics),user.nickname()))
|
||||
|
||||
@@ -322,7 +322,7 @@ class AllRecentFilesServer(webapp2.RequestHandler):
|
||||
for fic in fics:
|
||||
ficslug = FicSlug(fic)
|
||||
sendslugs.append(ficslug)
|
||||
|
||||
|
||||
template_values = dict(fics = sendslugs, nickname = user.nickname())
|
||||
path = os.path.join(os.path.dirname(__file__), 'allrecent.html')
|
||||
self.response.out.write(template.render(path, template_values))
|
||||
@@ -333,7 +333,7 @@ class FicSlug():
|
||||
self.count = savedmeta.count
|
||||
for k, v in savedmeta.meta.iteritems():
|
||||
setattr(self,k,v)
|
||||
|
||||
|
||||
class FanfictionDownloader(UserConfigServer):
|
||||
def get(self):
|
||||
self.post()
|
||||
@@ -361,7 +361,7 @@ class FanfictionDownloader(UserConfigServer):
|
||||
ch_end = mc.group('end')
|
||||
if ch_begin and not mc.group('comma'):
|
||||
ch_end = ch_begin
|
||||
|
||||
|
||||
logging.info("Queuing Download: %s" % url)
|
||||
login = self.request.get('login')
|
||||
password = self.request.get('password')
|
||||
@@ -376,8 +376,11 @@ class FanfictionDownloader(UserConfigServer):
|
||||
try:
|
||||
try:
|
||||
configuration = self.getUserConfig(user,url,format)
|
||||
except exceptions.UnknownSite:
|
||||
self.redirect("/?error=custom&errtext=%s"%urllib.quote("Unsupported site in URL (%s). See 'Support sites' list below."%url,''))
|
||||
return
|
||||
except Exception, e:
|
||||
self.redirect("/?error=custom&errtext=%s"%urlEscape("There's an error in your User Configuration: "+unicode(e)))
|
||||
self.redirect("/?error=custom&errtext=%s"%urllib.quote("There's an error in your User Configuration: "+unicode(e),'')[:2048]) # limited due to Locatton header length limit.
|
||||
return
|
||||
|
||||
adapter = adapters.getAdapter(configuration,url)
|
||||
@@ -537,7 +540,7 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
# delete existing chunks first
|
||||
for chunk in download.data_chunks:
|
||||
chunk.delete()
|
||||
|
||||
|
||||
index=0
|
||||
while( len(data) > 0 ):
|
||||
# logging.info("len(data): %s" % len(data))
|
||||
@@ -563,7 +566,7 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
smeta.meta = allmeta
|
||||
smeta.date = datetime.datetime.now()
|
||||
smeta.put()
|
||||
|
||||
|
||||
logging.info("Download finished OK")
|
||||
del data
|
||||
|
||||
@@ -616,16 +619,6 @@ def getDownloadMeta(id=None,url=None,user=None,format=None,new=False):
|
||||
|
||||
return download
|
||||
|
||||
def toPercentDecimal(match):
|
||||
"Return the %decimal number for the character for url escaping"
|
||||
s = match.group(1)
|
||||
return "%%%02x" % ord(s)
|
||||
|
||||
def urlEscape(data):
|
||||
"Escape text, including unicode, for use in URLs"
|
||||
p = re.compile(r'([^\w])')
|
||||
return p.sub(toPercentDecimal, data.encode("utf-8"))
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
app = webapp2.WSGIApplication([('/', MainHandler),
|
||||
('/fdowntask', FanfictionDownloaderTask),
|
||||
|
||||
Reference in New Issue
Block a user