mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-12 12:02:50 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
173fca8773 | ||
|
|
2ce514a38e | ||
|
|
d0e4999712 | ||
|
|
5c8940d333 | ||
|
|
664684b04f | ||
|
|
75064b2267 | ||
|
|
06d3fd9080 | ||
|
|
c6cd9c57e2 | ||
|
|
1887cf2cb9 | ||
|
|
2e6a1ef65d | ||
|
|
a0b276beb4 | ||
|
|
1ac9e5d36c | ||
|
|
93712de4b7 | ||
|
|
e7941298b6 | ||
|
|
65f286be99 | ||
|
|
1030cf44af | ||
|
|
e9190b9a12 | ||
|
|
dc63773fad | ||
|
|
710800f976 | ||
|
|
3eca202567 | ||
|
|
6d423e586f | ||
|
|
bda62aab9f | ||
|
|
f6d65c334f | ||
|
|
2c5371d95e | ||
|
|
6f4660763f | ||
|
|
75f8f72266 | ||
|
|
b1d689ba3e | ||
|
|
39bb6e37f6 | ||
|
|
288f12afed | ||
|
|
2a25aef7ac | ||
|
|
085fb47b08 | ||
|
|
5fdcbab46a | ||
|
|
2d83fa8f5d | ||
|
|
4a752e05e1 | ||
|
|
f1d9760aa9 | ||
|
|
b11bdd82db | ||
|
|
6e5de8060e | ||
|
|
2c1b9456bf | ||
|
|
60439cc658 | ||
|
|
fb95e1e168 | ||
|
|
0d490d2e50 | ||
|
|
838510c011 | ||
|
|
954ad00ca6 | ||
|
|
ee462d3742 | ||
|
|
56401e6dfa | ||
|
|
6070accbf5 | ||
|
|
eebb1ee8d0 | ||
|
|
bda307ed3d | ||
|
|
6b0c519f99 | ||
|
|
6533ee0187 | ||
|
|
3cc3b32012 | ||
|
|
315536a75b | ||
|
|
1c4250c0c1 | ||
|
|
6f7e700cd2 |
@@ -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, 2)
|
||||
version = (2, 5, 2)
|
||||
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"]),
|
||||
)
|
||||
|
||||
+150
-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():
|
||||
@@ -328,6 +331,8 @@ class ConfigWidget(QWidget):
|
||||
prefs['std_cols_newonly'] = colsnewonly
|
||||
|
||||
prefs['set_author_url'] =self.std_columns_tab.set_author_url.isChecked()
|
||||
prefs['includecomments'] =self.std_columns_tab.includecomments.isChecked()
|
||||
prefs['anth_comments_newonly'] =self.std_columns_tab.anth_comments_newonly.isChecked()
|
||||
|
||||
# Custom Columns tab
|
||||
# error column
|
||||
@@ -350,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()
|
||||
@@ -360,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):
|
||||
@@ -378,7 +384,7 @@ class BasicTab(QWidget):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
topl = QVBoxLayout()
|
||||
self.setLayout(topl)
|
||||
|
||||
@@ -433,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'])
|
||||
@@ -449,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'])
|
||||
@@ -466,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'])
|
||||
@@ -499,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.")
|
||||
@@ -516,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'])
|
||||
@@ -568,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)
|
||||
@@ -596,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()
|
||||
@@ -612,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(),
|
||||
@@ -621,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'],
|
||||
@@ -638,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"),
|
||||
@@ -652,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)
|
||||
|
||||
@@ -676,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()
|
||||
@@ -694,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)
|
||||
@@ -715,7 +729,7 @@ class PersonalIniTab(QWidget):
|
||||
label = QLabel(view_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
|
||||
self.l.addSpacing(5)
|
||||
|
||||
groupbox = QGroupBox(_("Calibre Columns"))
|
||||
@@ -730,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.")
|
||||
@@ -754,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'),
|
||||
@@ -764,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(),
|
||||
@@ -776,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,
|
||||
@@ -788,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.
|
||||
@@ -809,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)
|
||||
|
||||
@@ -825,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)
|
||||
@@ -847,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)
|
||||
@@ -864,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)
|
||||
|
||||
@@ -898,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"
|
||||
@@ -906,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"
|
||||
@@ -947,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)
|
||||
@@ -960,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)
|
||||
@@ -1005,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)
|
||||
@@ -1019,7 +1033,7 @@ class CalibreCoverTab(QWidget):
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
|
||||
self.gc_dropdowns = {}
|
||||
|
||||
sitelist = getSiteSections()
|
||||
@@ -1052,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)
|
||||
@@ -1068,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)
|
||||
|
||||
@@ -1082,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)
|
||||
|
||||
@@ -1113,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'])
|
||||
@@ -1126,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)
|
||||
|
||||
@@ -1160,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)
|
||||
@@ -1170,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') \
|
||||
@@ -1187,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_()
|
||||
@@ -1269,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()
|
||||
@@ -1279,7 +1293,7 @@ class CustomColumnsTab(QWidget):
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
self.custcol_dropdowns = {}
|
||||
self.custcol_newonlycheck = {}
|
||||
|
||||
@@ -1291,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:
|
||||
@@ -1321,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)
|
||||
@@ -1331,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.)")
|
||||
@@ -1350,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'+
|
||||
@@ -1358,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.)")
|
||||
@@ -1377,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'])
|
||||
@@ -1391,7 +1405,7 @@ class StandardColumnsTab(QWidget):
|
||||
QWidget.__init__(self)
|
||||
|
||||
columns=OrderedDict()
|
||||
|
||||
|
||||
columns["title"]=_("Title")
|
||||
columns["authors"]=_("Author(s)")
|
||||
columns["publisher"]=_("Publisher")
|
||||
@@ -1410,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():
|
||||
@@ -1425,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)
|
||||
@@ -1438,7 +1452,18 @@ class StandardColumnsTab(QWidget):
|
||||
self.set_author_url.setToolTip(_("Set Calibre Author URL to Author's URL on story site."))
|
||||
self.set_author_url.setChecked(prefs['set_author_url'])
|
||||
self.l.addWidget(self.set_author_url)
|
||||
|
||||
|
||||
self.includecomments = QCheckBox(_("Include Books' Comments in Anthology Comments?"),self)
|
||||
self.includecomments.setToolTip(_('''Include all the merged books' comments in the new book's comments.
|
||||
Default is a list of included titles only.'''))
|
||||
self.includecomments.setChecked(prefs['includecomments'])
|
||||
self.l.addWidget(self.includecomments)
|
||||
|
||||
self.anth_comments_newonly = QCheckBox(_("Set Anthology Comments only for new books"),self)
|
||||
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):
|
||||
@@ -1447,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)
|
||||
@@ -1466,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)
|
||||
@@ -1493,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. "
|
||||
@@ -1530,5 +1561,5 @@ class ImapTab(QWidget):
|
||||
self.l.addWidget(label,row,0,1,-1,Qt.AlignTop)
|
||||
self.l.setRowStretch(row,1)
|
||||
row+=1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+30
-14
@@ -21,19 +21,19 @@ from datetime import datetime
|
||||
try:
|
||||
from PyQt5 import QtWidgets as QtGui
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QFont, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
from PyQt5.Qt import (QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout,
|
||||
QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon,
|
||||
QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit,
|
||||
pyqtSignal, QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
except ImportError as e:
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QFont, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
from PyQt4.Qt import (QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout,
|
||||
QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon,
|
||||
QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit,
|
||||
pyqtSignal, QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
|
||||
try:
|
||||
from calibre.gui2 import QVariant
|
||||
@@ -622,6 +622,7 @@ class _LoopProgressDialog(QProgressDialog):
|
||||
self.status_prefix = status_prefix
|
||||
self.i = 0
|
||||
self.start_time = datetime.now()
|
||||
self.first = True
|
||||
|
||||
# can't import at file load.
|
||||
from calibre_plugins.fanficfare_plugin.prefs import prefs
|
||||
@@ -648,8 +649,14 @@ class _LoopProgressDialog(QProgressDialog):
|
||||
|
||||
def do_loop(self):
|
||||
|
||||
if self.i == 0:
|
||||
self.setValue(0)
|
||||
if self.first:
|
||||
## Windows 10 doesn't want to show the prog dialog content
|
||||
## until after the timer's been called again. Something to
|
||||
## do with cooperative multi threading maybe?
|
||||
## So this just trips the timer loop an extra time at the start.
|
||||
self.first = False
|
||||
QTimer.singleShot(0, self.do_loop)
|
||||
return
|
||||
|
||||
book = self.book_list[self.i]
|
||||
try:
|
||||
@@ -1450,6 +1457,15 @@ class ViewLog(SizePersistedDialog):
|
||||
|
||||
self.lineno = None
|
||||
|
||||
scrollable = QScrollArea()
|
||||
scrollcontent = QWidget()
|
||||
scrollable.setWidget(scrollcontent)
|
||||
scrollable.setWidgetResizable(True)
|
||||
self.l.addWidget(scrollable)
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
## error = (lineno, msg)
|
||||
for (lineno, error_msg) in errors:
|
||||
# print('adding label for error:%s: %s'%(lineno, error_msg))
|
||||
@@ -1460,7 +1476,7 @@ class ViewLog(SizePersistedDialog):
|
||||
label.setStyleSheet("QLabel { margin-left: 2em; color : blue; } QLabel:hover { color: red; }");
|
||||
label.setToolTip(_('Click to go to line %s')%lineno)
|
||||
label.mouseReleaseEvent = partial(self.label_clicked, lineno=lineno)
|
||||
self.l.addWidget(label)
|
||||
self.sl.addWidget(label)
|
||||
|
||||
# html='<p>'+'</p><p>'.join([ '(lineno: %s) %s'%e for e in errors ])+'</p>'
|
||||
|
||||
@@ -1470,7 +1486,7 @@ class ViewLog(SizePersistedDialog):
|
||||
# self.tb.setHtml(html)
|
||||
# l.addWidget(self.tb)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
|
||||
|
||||
+180
-75
@@ -73,7 +73,7 @@ except:
|
||||
|
||||
from calibre.library.field_metadata import FieldMetadata
|
||||
field_metadata = FieldMetadata()
|
||||
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.common_utils import (
|
||||
set_plugin_icon_resources, get_icon, create_menu_action_unique,
|
||||
get_library_uuid)
|
||||
@@ -83,7 +83,7 @@ from calibre_plugins.fanficfare_plugin.fanficfare import (
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.epubutils import (
|
||||
get_dcsource, get_dcsource_chaptercount, get_story_url_from_html,
|
||||
reset_orig_chapters_epub)
|
||||
reset_orig_chapters_epub, get_cover_data)
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare.geturls import (
|
||||
get_urls_from_page, get_urls_from_html,get_urls_from_text,
|
||||
@@ -116,6 +116,13 @@ formmapping = {
|
||||
'txt':'TXT'
|
||||
}
|
||||
|
||||
imagetypes = {
|
||||
'image/jpeg':'jpg',
|
||||
'image/png':'png',
|
||||
'image/gif':'gif',
|
||||
'image/svg+xml':'svg',
|
||||
}
|
||||
|
||||
PLUGIN_ICONS = ['images/icon.png']
|
||||
|
||||
class FanFicFarePlugin(InterfaceAction):
|
||||
@@ -477,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()
|
||||
|
||||
@@ -497,19 +511,21 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
},"\n".join(url_list))
|
||||
else:
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs from Email.'),3000)
|
||||
|
||||
|
||||
else:
|
||||
if url_list:
|
||||
self.add_dialog("\n".join(url_list),merge=False)
|
||||
else:
|
||||
else:
|
||||
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,
|
||||
show_copy_button=False)
|
||||
|
||||
|
||||
def get_urls_from_page_menu(self,anthology=False):
|
||||
|
||||
urltxt = ""
|
||||
@@ -534,7 +550,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs from Page.'),3000)
|
||||
self.restore_cursor()
|
||||
|
||||
|
||||
if url_list:
|
||||
self.add_dialog("\n".join(url_list),merge=d.anthology,anthology_url=url)
|
||||
else:
|
||||
@@ -606,7 +622,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
self.gui.status_bar.show_message(_('Can only UnNew books in library'),
|
||||
3000)
|
||||
return
|
||||
|
||||
|
||||
if not self.gui.current_view().selectionModel().selectedRows() :
|
||||
self.gui.status_bar.show_message(_('No Selected Books to Get URLs From'),
|
||||
3000)
|
||||
@@ -632,7 +648,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
suffix='.epub',
|
||||
dir=tdir)
|
||||
db.copy_format_to(book['calibre_id'],'EPUB',tmp,index_is_id=True)
|
||||
|
||||
|
||||
unnewtmp = PersistentTemporaryFile(prefix='unnew-%s-'%book['calibre_id'],
|
||||
suffix='.epub',
|
||||
dir=tdir)
|
||||
@@ -656,7 +672,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if fmt.lower() != 'epub' and db.has_format(book['calibre_id'],fmt,index_is_id=True):
|
||||
logger.debug("autoconvert remove f:"+fmt)
|
||||
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
|
||||
|
||||
|
||||
def get_unnew_books_finish(self, book_list, tdir=None):
|
||||
remove_dir(tdir)
|
||||
if prefs['autoconvert']:
|
||||
@@ -787,7 +803,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.busy_cursor()
|
||||
self.gui.status_bar.show_message(_('Fetching Story URLs for Series...'))
|
||||
|
||||
|
||||
# get list from identifiers:url/uri if present, but only if
|
||||
# it's *not* a valid story URL.
|
||||
mergeurl = self.get_story_url(db,book_id)
|
||||
@@ -798,7 +814,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs for Series.'),3000)
|
||||
self.restore_cursor()
|
||||
|
||||
|
||||
#print("urlmapfile:%s"%urlmapfile)
|
||||
|
||||
# AddNewDialog collects URLs, format and presents buttons.
|
||||
@@ -958,7 +974,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
init_label=_("Fetching metadata for stories...")
|
||||
win_title=_("Downloading metadata for stories")
|
||||
status_prefix=_("Fetched metadata for")
|
||||
|
||||
|
||||
self.gui.status_bar.show_message(status_bar, 3000)
|
||||
LoopProgressDialog(self.gui,
|
||||
books,
|
||||
@@ -1072,7 +1088,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
## Getting metadata from configured column.
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if ( collision in (CALIBREONLYSAVECOL) and
|
||||
prefs['savemetacol'] != '' and
|
||||
prefs['savemetacol'] != '' and
|
||||
prefs['savemetacol'] in custom_columns ):
|
||||
|
||||
savedmeta_book_id = book['calibre_id']
|
||||
@@ -1081,13 +1097,13 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
identicalbooks = self.do_id_search(url)
|
||||
if len(identicalbooks) == 1:
|
||||
savedmeta_book_id = identicalbooks.pop()
|
||||
|
||||
|
||||
if savedmeta_book_id:
|
||||
label = custom_columns[prefs['savemetacol']]['label']
|
||||
savedmetadata = db.get_custom(savedmeta_book_id, label=label, index_is_id=True)
|
||||
else:
|
||||
savedmetadata = None
|
||||
|
||||
|
||||
if savedmetadata:
|
||||
# sets flag inside story so getStoryMetadataOnly won't hit server.
|
||||
adapter.setStoryMetadata(savedmetadata)
|
||||
@@ -1123,19 +1139,19 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if userpass.status:
|
||||
adapter.username = userpass.user.text()
|
||||
adapter.password = userpass.passwd.text()
|
||||
|
||||
|
||||
except exceptions.AdultCheckRequired:
|
||||
if question_dialog(self.gui, _('Are You an Adult?'), '<p>'+
|
||||
_("%s requires that you be an adult. Please confirm you are an adult in your locale:")%url,
|
||||
show_copy_button=False):
|
||||
adapter.is_adult=True
|
||||
|
||||
|
||||
# let other exceptions percolate up.
|
||||
story = adapter.getStoryMetadataOnly(get_cover=False)
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['url'] = story.getMetadata('storyUrl')
|
||||
|
||||
|
||||
## Check reject list. Redundant with below for when story
|
||||
## URL changes, but also kept here to avoid network hit in
|
||||
## most common case where given url is story url.
|
||||
@@ -1148,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>
|
||||
@@ -1158,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')]
|
||||
@@ -1167,7 +1184,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Skipped')
|
||||
return
|
||||
|
||||
|
||||
################################################################################################################################################33
|
||||
|
||||
book['is_adult'] = adapter.is_adult
|
||||
@@ -1176,7 +1193,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
book['icon'] = 'plus.png'
|
||||
book['status'] = _('Add')
|
||||
|
||||
|
||||
if not bgmeta:
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
@@ -1187,7 +1204,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if prefs['savemetacol'] != '':
|
||||
# get metadata to save in configured column.
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
|
||||
|
||||
book['title'] = story.getMetadata("title", removeallentities=True)
|
||||
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
|
||||
book['publisher'] = story.getMetadata("site")
|
||||
@@ -1198,7 +1215,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
else:
|
||||
book['comments']=''
|
||||
book['series'] = story.getMetadata("series", removeallentities=True)
|
||||
|
||||
|
||||
if story.getMetadataRaw('datePublished'):
|
||||
book['pubdate'] = story.getMetadataRaw('datePublished').replace(tzinfo=local_tz)
|
||||
if story.getMetadataRaw('dateUpdated'):
|
||||
@@ -1207,7 +1224,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz)
|
||||
else:
|
||||
book['timestamp'] = None # need *something* there for calibre.
|
||||
|
||||
|
||||
if not merge:# skip all the collision code when d/ling for merging.
|
||||
if collision in (CALIBREONLY, CALIBREONLYSAVECOL):
|
||||
book['icon'] = 'metadata.png'
|
||||
@@ -1334,7 +1351,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# check make sure incoming is newer.
|
||||
lastupdated=story.getMetadataRaw('dateUpdated')
|
||||
logger.debug("OVERWRITE site updated: %s"%lastupdated)
|
||||
|
||||
|
||||
# updated doesn't have time (or is midnight), use dates only.
|
||||
# updated does have time, use full timestamps.
|
||||
if (lastupdated.time() == time.min and fileupdated.date() > lastupdated.date()) or \
|
||||
@@ -1377,19 +1394,19 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if not label and k in field_metadata:
|
||||
label=field_metadata[k]['name']
|
||||
key='calibre_std_'+k
|
||||
|
||||
|
||||
# if k == 'user_categories':
|
||||
# value=u', '.join(mi.get(k))
|
||||
# label=_('User Categories')
|
||||
|
||||
|
||||
if label: # only if it has a human readable name.
|
||||
if value is None or not book['calibre_id']:
|
||||
## if existing book, populate existing calibre column
|
||||
## values in metadata, else '' to hide.
|
||||
value=''
|
||||
value=''
|
||||
book['calibre_columns'][key]={'val':value,'label':label}
|
||||
#logger.debug("%s(%s): %s"%(label,key,value))
|
||||
|
||||
|
||||
# custom columns
|
||||
for k, column in self.gui.library_view.model().custom_columns.iteritems():
|
||||
if k != prefs['savemetacol']:
|
||||
@@ -1405,7 +1422,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
value=''
|
||||
book['calibre_columns'][key]={'val':value,'label':label}
|
||||
# logger.debug("%s(%s): %s"%(label,key,value))
|
||||
|
||||
|
||||
# if still 'good', make a temp file to write the output to.
|
||||
# For HTML format users, make the filename inside the zip something reasonable.
|
||||
# For crazy long titles/authors, limit it to 200chars.
|
||||
@@ -1472,11 +1489,20 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
htmllog = htmllog + '</table></body></html>'
|
||||
|
||||
payload = ([], book_list, options)
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download ended'), msg,
|
||||
show_copy_button=False,
|
||||
log_viewer_unique_name="FanFicFare log viewer")
|
||||
|
||||
# log_viewer_unique_name implemented here: https://github.com/kovidgoyal/calibre/compare/v2.56.0...v2.57.0
|
||||
if calibre_version >= (2, 57, 0):
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download ended'), msg,
|
||||
show_copy_button=False,
|
||||
log_viewer_unique_name="FanFicFare log viewer")
|
||||
else:
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download ended'), msg,
|
||||
show_copy_button=False)
|
||||
|
||||
return
|
||||
|
||||
cookiejarfile = PersistentTemporaryFile(suffix='.cookiejar',
|
||||
@@ -1710,13 +1736,21 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
do_update_func = self.do_download_list_update
|
||||
|
||||
self.gui.proceed_question(do_update_func,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download complete'), msg,
|
||||
show_copy_button=False,
|
||||
log_viewer_unique_name="FanFicFare log viewer")
|
||||
# log_viewer_unique_name implemented here: https://github.com/kovidgoyal/calibre/compare/v2.56.0...v2.57.0
|
||||
if calibre_version >= (2, 57, 0):
|
||||
self.gui.proceed_question(do_update_func,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download complete'), msg,
|
||||
show_copy_button=False,
|
||||
log_viewer_unique_name="FanFicFare log viewer")
|
||||
else:
|
||||
self.gui.proceed_question(do_update_func,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download complete'), msg,
|
||||
show_copy_button=False)
|
||||
|
||||
def do_download_merge_update(self, payload):
|
||||
db = self.gui.current_db
|
||||
|
||||
(good_list,bad_list,options) = payload
|
||||
total_good = len(good_list)
|
||||
@@ -1742,19 +1776,62 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
#print("mergebook:\n%s"%mergebook)
|
||||
|
||||
if mergebook['good']: # there shouldn't be any !'good' books at this point.
|
||||
# if still 'good', make a temp file to write the output to.
|
||||
tmp = PersistentTemporaryFile(suffix='.'+options['fileform'],
|
||||
dir=options['tdir'])
|
||||
logger.debug("title:"+mergebook['title'])
|
||||
logger.debug("outfile:"+tmp.name)
|
||||
mergebook['outfile'] = tmp.name
|
||||
# make a temp file to write the output to.
|
||||
tmp = PersistentTemporaryFile(suffix='.'+options['fileform'],
|
||||
dir=options['tdir'])
|
||||
# logger.debug("title:"+mergebook['title'])
|
||||
logger.debug("outfile:"+tmp.name)
|
||||
mergebook['outfile'] = tmp.name
|
||||
|
||||
## Calibre's Polish heuristics for covers can cause problems
|
||||
## if a merged anthology book has sub-book covers, but not a
|
||||
## proper main cover. So, now if there are any covers, we
|
||||
## will force a main cover.
|
||||
|
||||
## start with None. If no subbook covers, don't force one
|
||||
## here. User can configure FFF to always create/polish a
|
||||
## cover if they want. This is about when we force it.
|
||||
coverpath = None
|
||||
coverimgtype = None
|
||||
|
||||
## first, look for covers inside the subbooks. Stop at the
|
||||
## first one, which will be used if there isn't a pre-existing
|
||||
## calibre cover.
|
||||
if not coverpath:
|
||||
for book in good_list:
|
||||
coverdata = get_cover_data(book['outfile'])
|
||||
if coverdata: # found a cover.
|
||||
(coverimgtype,coverimgdata) = coverdata[4:6]
|
||||
logger.debug('coverimgtype:%s [%s]'%(coverimgtype,imagetypes[coverimgtype]))
|
||||
tmpcover = PersistentTemporaryFile(suffix='.'+imagetypes[coverimgtype],
|
||||
dir=options['tdir'])
|
||||
tmpcover.write(coverimgdata)
|
||||
tmpcover.flush()
|
||||
tmpcover.close()
|
||||
coverpath = tmpcover.name
|
||||
break
|
||||
# logger.debug('coverpath:%s'%coverpath)
|
||||
|
||||
## if updating an existing book and there is at least one
|
||||
## subbook cover:
|
||||
if coverpath and mergebook['calibre_id']:
|
||||
# Couldn't find a better way to get the cover path.
|
||||
calcoverpath = os.path.join(db.library_path,
|
||||
db.path(mergebook['calibre_id'], index_is_id=True),
|
||||
'cover.jpg')
|
||||
## if there's an existing cover, use it. Calibre will set
|
||||
## it for us during lots of different actions anyway.
|
||||
if os.path.exists(calcoverpath):
|
||||
coverpath = calcoverpath
|
||||
|
||||
# logger.debug('coverpath:%s'%coverpath)
|
||||
self.get_epubmerge_plugin().do_merge(tmp.name,
|
||||
[ x['outfile'] for x in good_list ],
|
||||
tags=mergebook['tags'],
|
||||
titleopt=mergebook['title'],
|
||||
keepmetadatafiles=True,
|
||||
source=mergebook['url'])
|
||||
source=mergebook['url'],
|
||||
coverjpgpath=coverpath)
|
||||
|
||||
options['collision']=OVERWRITEALWAYS
|
||||
self.update_books_loop(mergebook,self.gui.current_db,options)
|
||||
@@ -1874,7 +1951,8 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
#print("mi.tags:%s"%mi.tags)
|
||||
|
||||
if book['all_metadata']['langcode']:
|
||||
mi.languages=[book['all_metadata']['langcode']]
|
||||
# split due to anthologies. Gives list of one for non-anth.
|
||||
mi.languages=book['all_metadata']['langcode'].split(', ')
|
||||
else:
|
||||
# Set language english, but only if not already set.
|
||||
if not oldmi.languages:
|
||||
@@ -1941,14 +2019,17 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
configuration = None
|
||||
if prefs['allow_custcol_from_ini']:
|
||||
configuration = get_fff_config(book['url'],options['fileform'])
|
||||
# meta => custcol[,a|n|r]
|
||||
# meta => custcol[,a|n|r|n_anthaver,r_anthaver]
|
||||
# cliches=>\#acolumn,r
|
||||
for line in configuration.getConfig('custom_columns_settings').splitlines():
|
||||
if "=>" in line:
|
||||
(meta,custcol) = map( lambda x: x.strip(), line.split("=>") )
|
||||
flag='r'
|
||||
anthaver=False
|
||||
if "," in custcol:
|
||||
(custcol,flag) = map( lambda x: x.strip(), custcol.split(",") )
|
||||
anthaver = 'anthaver' in flag
|
||||
flag=flag[0] # first char only.
|
||||
|
||||
if meta not in book['all_metadata']:
|
||||
# if double quoted, use as a literal value.
|
||||
@@ -1970,8 +2051,15 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if flag == 'r' or (flag == 'n' and book['added']):
|
||||
if coldef['datatype'] in ('int','float'): # for favs, etc--site specific metadata.
|
||||
if 'anthology_meta_list' in book and meta in book['anthology_meta_list']:
|
||||
# re-split list, strip commas, convert to floats, sum up.
|
||||
val = sum([ float(x.replace(",","")) for x in val.split(", ") ])
|
||||
# re-split list, strip commas, convert to floats
|
||||
items = [ float(x.replace(",","")) for x in val.split(", ") ]
|
||||
if anthaver:
|
||||
if items:
|
||||
val = sum(items) / float(len(items))
|
||||
else:
|
||||
val = 0
|
||||
else:
|
||||
val = sum(items)
|
||||
else:
|
||||
val = unicode(val).replace(",","")
|
||||
else:
|
||||
@@ -2044,7 +2132,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
prefs['gencalcover'] == SAVE_YES ## yes, always
|
||||
or (prefs['gencalcover'] == SAVE_YES_UNLESS_IMG ## yes, unless image.
|
||||
and book['all_metadata']['cover_image'] not in ('specific','first','default')) ):
|
||||
|
||||
|
||||
cover_generated = False # flag for polish below.
|
||||
# Yes, should do gencov. Which?
|
||||
if prefs['calibre_gen_cover'] and HAS_CALGC:
|
||||
@@ -2057,19 +2145,19 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
cover_generated = True
|
||||
elif prefs['plugin_gen_cover'] and 'Generate Cover' in self.gui.iactions:
|
||||
# plugin, if available.
|
||||
|
||||
|
||||
#logger.debug("Do Generate Cover added:%s gcnewonly:%s"%(book['added'],prefs['gcnewonly']))
|
||||
|
||||
|
||||
# force a refresh if generating cover so complex composite
|
||||
# custom columns are current and correct
|
||||
db.refresh_ids([book_id])
|
||||
|
||||
|
||||
gc_plugin = self.gui.iactions['Generate Cover']
|
||||
setting_name = None
|
||||
if prefs['allow_gc_from_ini']:
|
||||
if not configuration: # might already have it from allow_custcol_from_ini
|
||||
configuration = get_fff_config(book['url'],options['fileform'])
|
||||
|
||||
|
||||
# template => regexp to match => GC Setting to use.
|
||||
# generate_cover_settings:
|
||||
# ${category} => Buffy:? the Vampire Slayer => Buffy
|
||||
@@ -2082,21 +2170,21 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Generate Cover Setting from generate_cover_settings(%s)"%setting_name)
|
||||
if setting_name not in gc_plugin.get_saved_setting_names():
|
||||
logger.info("GC Name %s not found, discarding! (check personal.ini for typos)"%setting_name)
|
||||
setting_name = None
|
||||
|
||||
|
||||
if not setting_name and book['all_metadata']['site'] in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings'][book['all_metadata']['site']]
|
||||
logger.debug("Generate Cover Setting from site(%s)"%setting_name)
|
||||
|
||||
|
||||
if not setting_name and 'Default' in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings']['Default']
|
||||
logger.debug("Generate Cover Setting from Default(%s)"%setting_name)
|
||||
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Running Generate Cover with settings %s."%setting_name)
|
||||
## fetch updated mi object from
|
||||
@@ -2105,14 +2193,14 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
cover_generated = True
|
||||
|
||||
|
||||
if cover_generated and prefs['gc_polish_cover'] and \
|
||||
options['fileform'] == "epub":
|
||||
# set cover inside epub from calibre's polish feature
|
||||
from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS
|
||||
from calibre.utils.logging import Log
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
# Couldn't find a better way to get the cover path.
|
||||
cover_path = os.path.join(db.library_path,
|
||||
db.path(book_id, index_is_id=True),
|
||||
@@ -2123,7 +2211,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
opts.update(data)
|
||||
O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys()))
|
||||
opts = O(**opts)
|
||||
|
||||
|
||||
log = Log(level=Log.DEBUG)
|
||||
outfile = db.format_abspath(book_id,
|
||||
formmapping[options['fileform']],
|
||||
@@ -2383,7 +2471,9 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
#print("book series:%s"%serieslist[-1])
|
||||
|
||||
if b['publisher']:
|
||||
if 'publisher' not in book:
|
||||
if not book['publisher']:
|
||||
## not set in all_metadata because it's not one of
|
||||
## the permitted metadata--use site instead.
|
||||
book['publisher']=b['publisher']
|
||||
elif book['publisher']!=b['publisher']:
|
||||
book['publisher']=None # if any are different, don't use.
|
||||
@@ -2441,18 +2531,33 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['anthology_meta_list'][k]=True
|
||||
|
||||
logger.debug("book['url']:%s"%book['url'])
|
||||
|
||||
book['comments'] = _("Anthology containing:")+"\n\n"
|
||||
if len(book['author']) > 1:
|
||||
mkbooktitle = lambda x : _("%s by %s") % (x['title'],' & '.join(x['author']))
|
||||
else:
|
||||
mkbooktitle = lambda x : x['title']
|
||||
|
||||
if prefs['includecomments']:
|
||||
def mkbookcomments(x):
|
||||
if x['comments']:
|
||||
return '<b>%s</b>\n\n%s'%(mkbooktitle(x),x['comments'])
|
||||
else:
|
||||
return '<b>%s</b>\n'%mkbooktitle(x)
|
||||
|
||||
book['comments'] += ('<div class="mergedbook">' +
|
||||
'<hr></div><div class="mergedbook">'.join([ mkbookcomments(x) for x in book_list]) +
|
||||
'</div>')
|
||||
else:
|
||||
book['comments'] += '\n'.join( [ mkbooktitle(x) for x in book_list ] )
|
||||
|
||||
configuration = get_fff_config(book['url'],fileform)
|
||||
if existingbook:
|
||||
book['title'] = deftitle = existingbook['title']
|
||||
book['comments'] = existingbook['comments']
|
||||
if prefs['anth_comments_newonly']:
|
||||
book['comments'] = existingbook['comments']
|
||||
else:
|
||||
book['title'] = deftitle = book_list[0]['title']
|
||||
if len(book['author']) > 1:
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ _("%s by %s")%(b['title'],', '.join(b['author'])) for b in book_list ])
|
||||
else:
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ b['title'] for b in book_list ])
|
||||
# book['all_metadata']['description']
|
||||
|
||||
# if all same series, use series for name. But only if all and not previous named
|
||||
@@ -2489,7 +2594,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
def restore_cursor(self):
|
||||
QApplication.restoreOverrideCursor()
|
||||
|
||||
|
||||
def split_text_to_urls(urls):
|
||||
# remove dups while preserving order.
|
||||
dups=set()
|
||||
|
||||
@@ -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
-117
@@ -25,7 +25,7 @@
|
||||
## titlepage_entries: category,genre, status,dateUpdated,rating
|
||||
## [epub]
|
||||
## # overrides defaults & site section
|
||||
## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated
|
||||
## titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated
|
||||
## [www.whofic.com:epub]
|
||||
## # overrides defaults, site section & format section
|
||||
## titlepage_entries: category,genre, status,datePublished
|
||||
@@ -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
|
||||
|
||||
@@ -510,6 +510,20 @@ first_post_title:First Post
|
||||
## if threadmarks are used. Can result in a duplicated chapter.
|
||||
always_include_first_post:false
|
||||
|
||||
## In normal operation, when updating an existing epub, old chapters
|
||||
## will be reused as-is. Normally, that works fine, but forum stories
|
||||
## sometimes have an index post as the first 'chapter', and the
|
||||
## version in the first chapter gets out of sync.
|
||||
##
|
||||
## If always_reload_first_chapter:true, then the first chapter will
|
||||
## always be downloaded again (ie, reloaded). It will NOT be maked
|
||||
## '(new)' (see mark_new_chapters). Because it is reloaded, manual
|
||||
## edits made to the first chapter will be lost.
|
||||
##
|
||||
## While intended for base_xenforoforum sites, this setting can be
|
||||
## applied to other sites.
|
||||
always_reload_first_chapter:false
|
||||
|
||||
## In normal operation, forumtags will only be populated when
|
||||
## threadmarks are used for chapters (see minimum_threadmarks above).
|
||||
## When always_use_forumtags:true, always populate forumtags.
|
||||
@@ -585,6 +599,11 @@ include_logpage: false
|
||||
## end up with Completed stories that have just one logpage entry.
|
||||
#include_logpage: smart
|
||||
|
||||
## By default, logpage is placed before the story chapters. This
|
||||
## setting, if true, will place the logpage after the chapters
|
||||
## instead.
|
||||
logpage_at_end: false
|
||||
|
||||
## items to include in the log page Empty metadata entries, or those
|
||||
## that haven't changed since the last update, will *not* appear, even
|
||||
## if in the list. You can include extra text or HTML that will be
|
||||
@@ -701,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
|
||||
@@ -748,6 +779,19 @@ extratags: FanFiction,Testing,Text
|
||||
[test1.com:html]
|
||||
extratags: FanFiction,Testing,HTML
|
||||
|
||||
[adult-fanfiction.org]
|
||||
extra_valid_entries:eroticatags,disclaimer
|
||||
eroticatags_label:Erotica Tags
|
||||
disclaimer_label:Disclaimer
|
||||
extra_titlepage_entries:eroticatags,disclaimer
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[archive.skyehawke.com]
|
||||
|
||||
[archiveofourown.org]
|
||||
@@ -847,18 +891,6 @@ extracategories:The Sentinel
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[bdsm-geschichten.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## This site offers no index page so we can either guess the chapter URLs
|
||||
## by dec/incrementing numbers ('guess') or walk all the chapters in the metadata
|
||||
## parsing state ('parse'). Since guessing can lead to errors for non-standard
|
||||
## story URLs, the default is to parse
|
||||
#find_chapters:guess
|
||||
|
||||
[bloodshedverse.com]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
@@ -904,15 +936,6 @@ strip_text_links:true
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Blood Ties
|
||||
|
||||
[buffynfaith.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[fanfic.castletv.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1029,15 +1052,24 @@ cliches_label:Character Cliches
|
||||
## 'mode'. 'r' to Replace any existing values, 'a' to Add to existing
|
||||
## value (use with tag-like columns), and 'n' for setting on New books
|
||||
## only. (Default is 'r'.)
|
||||
|
||||
## Literal strings can be set into custom columns using double quotes.
|
||||
## Each metadata=>column mapping must be on a separate line and each
|
||||
## needs to have one space at the start of each line.
|
||||
|
||||
## 'r_anthaver' and 'n_anthaver' can be used to indicate the same as
|
||||
## 'r' and 'n' for normal downloads, but to average the metadata for
|
||||
## the differents story in an anthology before setting in integer and
|
||||
## float type custom columns. This can be useful for a averrating
|
||||
## column, for example. Default is to sum the values of all stories,
|
||||
## and numChapters and numWords are always summed.
|
||||
|
||||
#custom_columns_settings:
|
||||
# cliches=>#acolumn
|
||||
# themes=>#bcolumn,a
|
||||
# timeline=>#ccolumn,n
|
||||
# "FanFiction"=>#collection
|
||||
# averrating=>#averrating,r_anthaver
|
||||
|
||||
[efiction.esteliel.de]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
@@ -1157,13 +1189,20 @@ romance_label: Romance
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
[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
|
||||
@@ -1221,6 +1260,14 @@ views_label:Views
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[finestories.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1262,13 +1309,9 @@ universe_as_series: true
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[forums.spacebattles.com]
|
||||
[forum.questionablequesting.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
@@ -1276,18 +1319,24 @@ cover_exclusion_regexp:/css/bir.png
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
[forums.spacebattles.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[harem.lucifael.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
extracharacters:Hermione Granger
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:read,reviews
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[hlfiction.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
@@ -1401,6 +1450,19 @@ extra_titlepage_entries: eroticatags
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Merlin
|
||||
|
||||
[mujaji.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[national-library.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:West Wing
|
||||
@@ -1502,15 +1564,21 @@ extracategories:My Little Pony: Friendship is Magic
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:The Pretender
|
||||
|
||||
[forum.questionablequesting.com]
|
||||
## see [base_xenforoforum]
|
||||
[quotev.com]
|
||||
extra_valid_entries:pages,readers,reads,favorites,searchtags,comments
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
searchtags_label:Search Tags
|
||||
comments_label:Comments
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
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
|
||||
@@ -1536,22 +1604,6 @@ extracategories:Supernatural
|
||||
extracharacters:Sam,Dean
|
||||
extraships:Sam/Dean
|
||||
|
||||
[scarhead.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[sheppardweir.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1693,15 +1745,6 @@ readings_label: Readings
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[tokra.fandomnet.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[tolkienfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
@@ -1769,6 +1812,12 @@ extraships:Draco Malfoy/Ginny Weasley
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[www.deepinmysoul.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.destinysgateway.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -2069,11 +2118,6 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:NCIS
|
||||
|
||||
[www.nickngreg.nl]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:CSI
|
||||
extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
[www.phoenixsong.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -2128,6 +2172,8 @@ extracategories:Psych
|
||||
extracategories:Queer as Folk
|
||||
|
||||
[quotev.com]
|
||||
user_agent:
|
||||
slow_down_sleep_time:2
|
||||
extra_valid_entries:pages,readers,reads,favorites,searchtags,comments
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
@@ -2204,23 +2250,9 @@ extracategories:Lord of the Rings
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[www.twcslibrary.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## twcslibrary.net (ab)uses series as personal reading lists.
|
||||
collect_series: false
|
||||
|
||||
[www.tthfanfic.org]
|
||||
user_agent:
|
||||
slow_down_sleep_time:2
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
@@ -2255,6 +2287,22 @@ pairingcat_to_characters_ships:true
|
||||
## instead be added to characters Buffy, Spike and ships Buffy/Spike
|
||||
romancecat_to_characters_ships:true
|
||||
|
||||
[www.twcslibrary.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## twcslibrary.net (ab)uses series as personal reading lists.
|
||||
collect_series: false
|
||||
|
||||
[www.twilightarchives.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Twilight
|
||||
@@ -2303,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
|
||||
@@ -2367,20 +2416,6 @@ extracategories:Andromeda
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Artemis Fowl
|
||||
|
||||
[www.therabidreader.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
|
||||
[www.naiceanilme.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -2394,8 +2429,6 @@ extracategories:Artemis Fowl
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
@@ -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
|
||||
@@ -166,6 +167,8 @@ default_prefs['allow_custcol_from_ini'] = True
|
||||
|
||||
default_prefs['std_cols_newonly'] = {}
|
||||
default_prefs['set_author_url'] = True
|
||||
default_prefs['includecomments'] = False
|
||||
default_prefs['anth_comments_newonly'] = True
|
||||
|
||||
default_prefs['imapserver'] = ''
|
||||
default_prefs['imapuser'] = ''
|
||||
@@ -174,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):
|
||||
|
||||
+495
-452
File diff suppressed because it is too large
Load Diff
+494
-450
File diff suppressed because it is too large
Load Diff
+483
-441
File diff suppressed because it is too large
Load Diff
+487
-443
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+490
-448
File diff suppressed because it is too large
Load Diff
+498
-454
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+561
-518
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+497
-454
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)
|
||||
|
||||
|
||||
@@ -138,6 +138,13 @@ import adapter_buffygilescom
|
||||
import adapter_andromedawebcom
|
||||
import adapter_artemisfowlcom
|
||||
import adapter_naiceanilmenet
|
||||
import adapter_deepinmysoulnet
|
||||
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
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
################################################################################
|
||||
### Written by GComyn
|
||||
################################################################################
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import sys
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
################################################################################
|
||||
|
||||
def getClass():
|
||||
return AdultFanFictionOrgAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class AdultFanFictionOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
logger.debug("AdultFanFictionOrgAdapter.__init__ - url='{0}'".format(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
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
#Setting the 'Zone' for each "Site"
|
||||
self.zone = self.parsedUrl.netloc.split('.')[0]
|
||||
|
||||
# normalized story URL. (checking self.zone against list
|
||||
# removed--it was redundant w/getAcceptDomains and
|
||||
# getSiteURLPattern both)
|
||||
self._setURL('http://' + self.zone + '.' + self.getBaseDomain() + '/story.php?no='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
#self.story.setMetadata('siteabbrev',self.getSiteAbbrev())
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev',self.zone+'aff')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y-%m-%d"
|
||||
|
||||
|
||||
##This method will be moved to the sub-adapters
|
||||
# @classmethod
|
||||
# def getSiteAbbrev(self):
|
||||
# return self.zone+'aff'
|
||||
|
||||
## Added because adult-fanfiction.org does send you to
|
||||
## www.adult-fanfiction.org when you go to it and it also moves
|
||||
## the site & examples down the web service front page so the
|
||||
## first screen isn't dominated by 'adult' links.
|
||||
def getBaseDomain(self):
|
||||
return 'adult-fanfiction.org'
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.adult-fanfiction.org'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
# mobile.fimifction.com isn't actually a valid domain, but we can still get the story id from URLs anyway
|
||||
return ['anime.adult-fanfiction.org',
|
||||
'anime2.adult-fanfiction.org',
|
||||
'bleach.adult-fanfiction.org',
|
||||
'books.adult-fanfiction.org',
|
||||
'buffy.adult-fanfiction.org',
|
||||
'cartoon.adult-fanfiction.org',
|
||||
'celeb.adult-fanfiction.org',
|
||||
'comics.adult-fanfiction.org',
|
||||
'ff.adult-fanfiction.org',
|
||||
'games.adult-fanfiction.org',
|
||||
'hp.adult-fanfiction.org',
|
||||
'inu.adult-fanfiction.org',
|
||||
'lotr.adult-fanfiction.org',
|
||||
'manga.adult-fanfiction.org',
|
||||
'movies.adult-fanfiction.org',
|
||||
'naruto.adult-fanfiction.org',
|
||||
'ne.adult-fanfiction.org',
|
||||
'original.adult-fanfiction.org',
|
||||
'tv.adult-fanfiction.org',
|
||||
'xmen.adult-fanfiction.org',
|
||||
'ygo.adult-fanfiction.org',
|
||||
'yuyu.adult-fanfiction.org']
|
||||
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return ("http://anime.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://anime2.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://bleach.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://books.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://buffy.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://cartoon.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://celeb.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://comics.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://ff.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://games.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://hp.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://inu.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://lotr.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://manga.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://movies.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://naruto.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://ne.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://original.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://tv.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://xmen.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://ygo.adult-fanfiction.org/story.php?no=123456789 "
|
||||
+ "http://yuyu.adult-fanfiction.org/story.php?no=123456789")
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r'http?://(anime|anime2|bleach|books|buffy|cartoon|celeb|comics|ff|games|hp|inu|lotr|manga|movies|naruto|ne|original|tv|xmen|ygo|yuyu)\.adult-fanfiction\.org/story\.php\?no=\d+$'
|
||||
|
||||
##This is not working right now, so I'm commenting it out, but leaving it for future testing
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
#def needToLoginCheck(self, data):
|
||||
##This adapter will always require a login
|
||||
# return True
|
||||
|
||||
# <form name="login" method="post" action="">
|
||||
# <div class="top">E-mail: <span id="sprytextfield1">
|
||||
# <input name="email" type="text" id="email" size="20" maxlength="255" />
|
||||
# <span class="textfieldRequiredMsg">Email is required.</span><span class="textfieldInvalidFormatMsg">Invalid E-mail.</span></span></div>
|
||||
# <div class="top">Password: <span id="sprytextfield2">
|
||||
# <input name="pass1" type="password" id="pass1" size="20" maxlength="32" />
|
||||
# <span class="textfieldRequiredMsg">password is required.</span><span class="textfieldMinCharsMsg">Minimum 8 characters8.</span><span class="textfieldMaxCharsMsg">Exceeded 32 characters.</span></span></div>
|
||||
# <div class="top"><br /> <input name="loginsubmittop" type="hidden" id="loginsubmit" value="TRUE" />
|
||||
# <input type="submit" value="Login" />
|
||||
# </div>
|
||||
# </form>
|
||||
|
||||
|
||||
##This is not working right now, so I'm commenting it out, but leaving it for future testing
|
||||
#def performLogin(self, url, soup):
|
||||
# params = {}
|
||||
|
||||
# if self.password:
|
||||
# params['email'] = self.username
|
||||
# params['pass1'] = self.password
|
||||
# else:
|
||||
# params['email'] = self.getConfig("username")
|
||||
# params['pass1'] = self.getConfig("password")
|
||||
# params['submit'] = 'Login'
|
||||
|
||||
# # copy all hidden input tags to pick up appropriate tokens.
|
||||
# for tag in soup.findAll('input',{'type':'hidden'}):
|
||||
# params[tag['name']] = tag['value']
|
||||
|
||||
# logger.debug("Will now login to URL {0} as {1} with password: {2}".format(url, params['email'],params['pass1']))
|
||||
|
||||
# d = self._postUrl(url, params, usecache=False)
|
||||
# d = self._fetchUrl(url, params, usecache=False)
|
||||
# soup = self.make_soup(d)
|
||||
|
||||
#if not (soup.find('form', {'name' : 'login'}) == None):
|
||||
# logger.info("Failed to login to URL %s as %s" % (url, params['email']))
|
||||
# raise exceptions.FailedToLogin(url,params['email'])
|
||||
# return False
|
||||
#else:
|
||||
# return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def doExtractChapterUrlsAndMetadata(self, get_cover=True):
|
||||
|
||||
## You need to have your is_adult set to true to get this story
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code in 404:
|
||||
raise exceptions.StoryDoesNotExist("Code: 404. %s"%self.url)
|
||||
elif e.code == 410:
|
||||
raise exceptions.StoryDoesNotExist("Code: 410. %s"%self.url)
|
||||
elif e.code == 401:
|
||||
self.needToLogin = True
|
||||
data = ''
|
||||
else:
|
||||
raise e
|
||||
|
||||
if "The dragons running the back end of the site can not seem to find the story you are looking for." in data:
|
||||
raise exceptions.StoryDoesNotExist(self.zone+'.'+self.getBaseDomain()
|
||||
+" says: The dragons running the back end of the site can not seem to find the story you are looking for.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
##This is not working right now, so I'm commenting it out, but leaving it for future testing
|
||||
#self.performLogin(url, soup)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
## Some of the titles have a backslash on the story page, but not on the Author's page
|
||||
## So I am removing it from the title, so it can be found on the Author's page further in the code.
|
||||
## Also, some titles may have extra spaces ' ', and the search on the Author's page removes them,
|
||||
## so I have to here as well. I used multiple replaces to make sure, since I did the same below.
|
||||
a = soup.find('a', href=re.compile(r'story.php\?no='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(a).replace('\\','').replace(' ',' ').replace(' ',' ').replace(' ',' ').strip())
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"profile.php\?no=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl',a['href'])
|
||||
self.story.setMetadata('author',stripHTML(a))
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.find('div',{'id':'snav'})
|
||||
for i, chapter in enumerate(chapters.findAll('a')):
|
||||
self.chapterUrls.append((stripHTML(chapter),self.url+'&chapter='+str(i+1)))
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
##The story page does not give much Metadata, so we go to the Author's page
|
||||
|
||||
##Get the first Author page to see if there are multiple pages.
|
||||
##AFF doesn't care if the page number is larger than the actual pages,
|
||||
##it will continue to show the last page even if the variable is larger than the actual page
|
||||
author_Url = self.story.getMetadata('authorUrl')+'&view=story&zone='+self.zone+'&page=1'
|
||||
|
||||
##I'm resetting the author page to the zone for this story
|
||||
self.story.setMetadata('authorUrl',author_Url)
|
||||
|
||||
logger.debug('Getting the author page: {0}'.format(author_Url))
|
||||
try:
|
||||
adata = self._fetchUrl(author_Url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code in 404:
|
||||
raise exceptions.StoryDoesNotExist("Author Page: Code: 404. %s"%author_Url)
|
||||
elif e.code == 410:
|
||||
raise exceptions.StoryDoesNotExist("Author Page: Code: 410. %s"%author_Url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if "The member you are looking for does not exist." in adata:
|
||||
raise exceptions.StoryDoesNotExist(self.zone+'.'+self.getBaseDomain() +" says: The member you are looking for does not exist.")
|
||||
|
||||
asoup = self.make_soup(adata)
|
||||
|
||||
##Getting the number of pages
|
||||
pages=asoup.find('div',{'class' : 'pagination'}).findAll('li')[-1].find('a')
|
||||
if not pages == None:
|
||||
pages = pages['href'].split('=')[-1]
|
||||
else:
|
||||
pages = 0
|
||||
logger.info(pages)
|
||||
##If there is only 1 page of stories, check it to get the Metadata,
|
||||
if pages == 0:
|
||||
a = asoup.findAll('li')
|
||||
for lc2 in a:
|
||||
if lc2.find('a', href=re.compile(r'story.php\?no='+self.story.getMetadata('storyId')+"$")):
|
||||
break
|
||||
## otherwise go through the pages
|
||||
else:
|
||||
page=1
|
||||
i=0
|
||||
while i == 0:
|
||||
##We already have the first page, so if this is the first time through, skip getting the page
|
||||
if page != 1:
|
||||
author_Url = self.story.getMetadata('authorUrl')+'&view=story&zone='+self.zone+'&page='+str(page)
|
||||
logger.debug('Getting the author page: {0}'.format(author_Url))
|
||||
try:
|
||||
adata = self._fetchUrl(author_Url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code in 404:
|
||||
raise exceptions.StoryDoesNotExist("Author Page: Code: 404. %s"%author_Url)
|
||||
elif e.code == 410:
|
||||
raise exceptions.StoryDoesNotExist("Author Page: Code: 410. %s"%author_Url)
|
||||
else:
|
||||
raise e
|
||||
##This will probably never be needed, since AFF doesn't seem to care what number you put as
|
||||
## the page number, it will default to the last page, even if you use 1000, for an author
|
||||
## that only hase 5 pages of stories, but I'm keeping it in to appease Saint Justin Case (just in case).
|
||||
if "The member you are looking for does not exist." in adata:
|
||||
raise exceptions.StoryDoesNotExist(self.zone+'.'+self.getBaseDomain() +" says: The member you are looking for does not exist.")
|
||||
|
||||
asoup = self.make_soup(adata)
|
||||
|
||||
a = asoup.findAll('li')
|
||||
for lc2 in a:
|
||||
if lc2.find('a', href=re.compile(r'story.php\?no='+self.story.getMetadata('storyId')+"$")):
|
||||
i=1
|
||||
break
|
||||
page = page + 1
|
||||
if page > pages:
|
||||
break
|
||||
|
||||
##Split the Metadata up into a list
|
||||
##We have to change the soup type to a string, then remove the newlines, and double spaces,
|
||||
##then changes the <br/> to '-:-', which seperates the different elemeents.
|
||||
##Then we strip the HTML elements from the string.
|
||||
##There is also a double <br/>, so we have to fix that, then remove the leading and trailing '-:-'.
|
||||
##They are always in the same order.
|
||||
liMetadata = stripHTML(str(lc2).replace('\n','').replace('\r','').replace('\t',' ').replace(' ',' ').replace(' ',' ').replace(' ',' ').replace(r'<br/>','-:-'))
|
||||
liMetadata = liMetadata.replace(r'-:--:-','-:-').strip('-:-').strip('-:-')
|
||||
|
||||
for i, value in enumerate(liMetadata.split('-:-')):
|
||||
##The item 6 is the reviews... We are disregarding them.
|
||||
##The item 7 is the 'Dragon Prints'... not sure what they are, so disregarding them.
|
||||
##The 0 item is the title
|
||||
if i == 0:
|
||||
if value <> self.story.getMetadata('title'):
|
||||
raise exceptions.StoryDoesNotExist('Did not find story in author story list: {0}'.format(author_Url))
|
||||
elif i == 1:
|
||||
##Get the description
|
||||
self.story.setMetadata('description',stripHTML(value.strip()))
|
||||
elif i == 2:
|
||||
##The Get the Category
|
||||
self.story.setMetadata('category',value.replace(r'>',r'>').replace(r'Located :',r'').strip())
|
||||
elif i == 3:
|
||||
##Get the Erotic Tags
|
||||
value = stripHTML(value.replace(r'Content Tags :',r'')).strip()
|
||||
for code in re.split(r'\s',value):
|
||||
self.story.addToList('eroticatags',code)
|
||||
elif i == 4:
|
||||
##Get the Posted Date
|
||||
value = value.replace(r'Posted :',r'').strip()
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
elif i == 5:
|
||||
##Get the 'Updated' Edited date
|
||||
##AFF has the time for the Updated date, and we only want the date,
|
||||
##so we take the first 10 characters only
|
||||
value = value.replace(r'Edited :',r'').strip()[0:10]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
#Since each chapter is on 1 page, we don't need to do anything special, just get the content of the page.
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
chaptertag = soup.find('div',{'class' : 'pagination'}).parent.findNext('td')
|
||||
|
||||
if None == chaptertag:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,chaptertag)
|
||||
@@ -187,7 +187,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
for a in alist:
|
||||
self.story.addToList('authorId',a['href'].split('/')[-1])
|
||||
self.story.addToList('authorUrl',a['href'])
|
||||
self.story.addToList('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.addToList('author',a.text)
|
||||
|
||||
byline = metasoup.find('h3',{'class':'byline'})
|
||||
|
||||
@@ -77,7 +77,8 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'This story contains adult content and/or themes.' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
or "That password doesn't match the one in our database" in data \
|
||||
or "Member Login" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return DeepInMySoulNetAdapter ## XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class DeepInMySoulNetAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fiction part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','dimsn') ## XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.deepinmysoul.net' # XXX
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/fiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/fiction/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&warning=4"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
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
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. ksarchive uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# fiction/viewstory.php?sid=1882&warning=4
|
||||
# fiction/viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
m = re.search(r"'fiction/viewstory.php\?sid=29(&warning=4)'",data)
|
||||
m = re.search(r"'fiction/viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'pagecontent'})
|
||||
|
||||
## Title
|
||||
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fiction/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"fiction/viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^fiction/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('fiction/viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
# 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', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class FanfictionLucifaelComAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'fanfiction.lucifael.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'luci'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%d/%m/%Y"
|
||||
|
||||
def getClass():
|
||||
return FanfictionLucifaelComAdapter
|
||||
@@ -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
|
||||
@@ -170,9 +168,11 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
elif 'Crossover' in categories[0]['href']:
|
||||
caturl = "https://%s%s"%(self.getSiteDomain(),categories[0]['href'])
|
||||
catsoup = self.make_soup(self._fetchUrl(caturl))
|
||||
found = False
|
||||
for a in catsoup.findAll('a',href=re.compile(r"^/crossovers/.+?/\d+/")):
|
||||
self.story.addToList('category',stripHTML(a))
|
||||
else:
|
||||
found = True
|
||||
if not found:
|
||||
# Fall back. I ran across a story with a Crossver
|
||||
# category link to a broken page once.
|
||||
# http://www.fanfiction.net/s/2622060/1/
|
||||
@@ -181,8 +181,6 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
for c in stripHTML(categories[0]).replace(" Crossover","").split(' + '):
|
||||
self.story.addToList('category',c)
|
||||
|
||||
|
||||
|
||||
a = soup.find('a', href=re.compile(r'https?://www\.fictionratings\.com/'))
|
||||
rating = a.string
|
||||
if 'Fiction' in rating: # if rating has 'Fiction ', strip that out for consistency with past.
|
||||
@@ -206,6 +204,12 @@ 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:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
metalist = metatext.split(" - ")
|
||||
#logger.debug("metalist:(%s)"%metalist)
|
||||
|
||||
@@ -240,35 +244,43 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('dateUpdated',datetime.fromtimestamp(float(dates[0]['data-xutime'])))
|
||||
self.story.setMetadata('datePublished',datetime.fromtimestamp(float(dates[-1]['data-xutime'])))
|
||||
|
||||
donechars = False
|
||||
# Meta key titles and the metadata they go into, if any.
|
||||
metakeys = {
|
||||
# These are already handled separately.
|
||||
'Chapters':False,
|
||||
'Status':False,
|
||||
'id':False,
|
||||
'Updated':False,
|
||||
'Published':False,
|
||||
'Reviews':'reviews',
|
||||
'Favs':'favs',
|
||||
'Follows':'follows',
|
||||
'Words':'numWords',
|
||||
}
|
||||
|
||||
chars_ships_list=[]
|
||||
while len(metalist) > 0:
|
||||
if metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:') or metalist[0].startswith('Updated:') or metalist[0].startswith('Published:'):
|
||||
pass
|
||||
elif metalist[0].startswith('Reviews'):
|
||||
self.story.setMetadata('reviews',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Favs:'):
|
||||
self.story.setMetadata('favs',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Follows:'):
|
||||
self.story.setMetadata('follows',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Words'):
|
||||
self.story.setMetadata('numWords',metalist[0].split(':')[1].strip())
|
||||
elif not donechars:
|
||||
# with 'pairing' support, pairings are bracketed w/o comma after
|
||||
# [Caspian X, Lucy Pevensie] Edmund Pevensie, Peter Pevensie
|
||||
self.story.extendList('characters',metalist[0].replace('[','').replace(']',',').split(','))
|
||||
m = metalist.pop(0)
|
||||
if ':' in m:
|
||||
key = m.split(':')[0].strip()
|
||||
if key in metakeys:
|
||||
if metakeys[key]:
|
||||
self.story.setMetadata(metakeys[key],m.split(':')[1].strip())
|
||||
continue
|
||||
# no ':' or not found in metakeys
|
||||
chars_ships_list.append(m)
|
||||
|
||||
l = metalist[0]
|
||||
while '[' in l:
|
||||
self.story.addToList('ships',l[l.index('[')+1:l.index(']')].replace(', ','/'))
|
||||
l = l[l.index(']')+1:]
|
||||
# all because sometimes chars can have ' - ' in them.
|
||||
chars_ships_text = (' - ').join(chars_ships_list)
|
||||
# print("chars_ships_text:%s"%chars_ships_text)
|
||||
# with 'pairing' support, pairings are bracketed w/o comma after
|
||||
# [Caspian X, Lucy Pevensie] Edmund Pevensie, Peter Pevensie
|
||||
self.story.extendList('characters',chars_ships_text.replace('[','').replace(']',',').split(','))
|
||||
|
||||
donechars = True
|
||||
metalist=metalist[1:]
|
||||
|
||||
if 'Status: Complete' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
l = chars_ships_text
|
||||
while '[' in l:
|
||||
self.story.addToList('ships',l[l.index('[')+1:l.index(']')].replace(', ','/'))
|
||||
l = l[l.index(']')+1:]
|
||||
|
||||
if get_cover:
|
||||
# Try the larger image first.
|
||||
@@ -317,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))
|
||||
@@ -337,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
|
||||
@@ -117,7 +117,7 @@ class FictionManiaTVAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Complete':
|
||||
self.story.setMetadata('status', 'Complete' if value == 'Complete' else 'In-Progress')
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Complete' else 'In-Progress')
|
||||
|
||||
elif key == 'Categories':
|
||||
for element in cells[1]('a'):
|
||||
|
||||
@@ -93,7 +93,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
# non-existent/removed story urls get thrown to the front page.
|
||||
if "<h2>Welcome to FicWad</h2>" in data:
|
||||
if "<h4>Featured Story</h4>" in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
soup = self.make_soup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import re
|
||||
|
||||
from base_xenforoforum_adapter import BaseXenForoForumAdapter
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class HaremLucifaelComAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'harem.lucifael.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'seraglio'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%d/%m/%Y"
|
||||
|
||||
def getClass():
|
||||
return HaremLucifaelComAdapter
|
||||
@@ -188,9 +188,13 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# remove everything after here--the site's chapters break the
|
||||
# BS4 parser.
|
||||
data = data[:data.index('<script type="text/javascript" src="reviewjs.js">')]
|
||||
try:
|
||||
# remove everything after here--the site's chapters break
|
||||
# the BS4 parser.
|
||||
data = data[:data.index('<script type="text/javascript" src="reviewjs.js">')]
|
||||
except:
|
||||
# some older stories don't have the code at the end that breaks things.
|
||||
pass
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return KiaRepositoryMujajiNetAdapter ## XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class KiaRepositoryMujajiNetAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fiction part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/repository/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','kia') ## XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %b %Y" ## XXX
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'mujaji.net' # XXX
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/repository/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/repository/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/repository/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&warning=4"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
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
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. ksarchive uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# fiction/viewstory.php?sid=1882&warning=4
|
||||
# fiction/viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
m = re.search(r"'repository/viewstory.php\?sid=29(&warning=4)'",data)
|
||||
m = re.search(r"'repository/viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'pagetitle'})
|
||||
|
||||
## Title
|
||||
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/repository/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"repository/viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^repository/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('repository/viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
# 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', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -144,6 +144,9 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
raise e
|
||||
|
||||
if "This submission is awaiting moderator's approval" in data1:
|
||||
raise exceptions.StoryDoesNotExist("This submission is awaiting moderator's approval. %s"%self.url)
|
||||
|
||||
# author
|
||||
a = soup1.find("span", "b-story-user-y")
|
||||
self.story.setMetadata('authorId', urlparse.parse_qs(a.a['href'].split('?')[1])['uid'][0])
|
||||
|
||||
@@ -459,7 +459,22 @@ class Chapter(object):
|
||||
.strip(u'| \n')
|
||||
except AttributeError:
|
||||
raise ParsingError(u'Failed to locate date.')
|
||||
date = makeDate(dateText, '%d.%m.%Y')
|
||||
|
||||
# The site uses Europe/Moscow (MSK, UTC+0300) server time.
|
||||
def todayInMoscow():
|
||||
now = datetime.datetime.now() + datetime.timedelta(hours=3)
|
||||
today = datetime.datetime(now.year, now.month, now.day)
|
||||
return today
|
||||
|
||||
def parseDateText(text):
|
||||
if text == u'Вчера':
|
||||
return todayInMoscow() - datetime.timedelta(days=1)
|
||||
elif text == u'Сегодня':
|
||||
return todayInMoscow()
|
||||
else:
|
||||
return makeDate(text, '%d.%m.%Y')
|
||||
|
||||
date = parseDateText(dateText)
|
||||
return date
|
||||
|
||||
def _getInfoBarElement(self):
|
||||
|
||||
@@ -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',{'class':'description'}).text.strip()
|
||||
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)
|
||||
@@ -112,6 +112,11 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
# need(or easier) to pull other metadata from the author's list page.
|
||||
authsoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
|
||||
# remove author profile incase they've put the story URL in their bio.
|
||||
profile = authsoup.find('div',{'id':'profile'})
|
||||
if profile: # in case it changes.
|
||||
profile.extract()
|
||||
|
||||
## Title
|
||||
titlea = authsoup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(titlea))
|
||||
|
||||
@@ -97,7 +97,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
params['page'] = 'http://'+self.getSiteDomain()+'/'
|
||||
params['submit'] = 'Login'
|
||||
|
||||
loginUrl = 'https://' + self.getSiteDomain() + '/login.php'
|
||||
loginUrl = 'https://' + self.getSiteDomain() + '/sol-secure/login.php'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['theusername']))
|
||||
|
||||
@@ -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
|
||||
@@ -132,20 +132,20 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def set_cookiejar(self,cj):
|
||||
self.cookiejar = cj
|
||||
saveheaders = self.opener.addheaders
|
||||
self.opener = u2.build_opener(u2.HTTPCookieProcessor(self.cookiejar),GZipProcessor())
|
||||
self.opener.addheaders = [('User-Agent', self.getConfig('user_agent')),
|
||||
('X-Clacks-Overhead','GNU Terry Pratchett')]
|
||||
|
||||
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,18 +392,25 @@ 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
|
||||
newchap = bool(self.oldchapters or self.oldchaptersmap)
|
||||
|
||||
|
||||
if index == 0 and self.getConfig('always_reload_first_chapter'):
|
||||
data = self.getChapterText(url)
|
||||
# first chapter is rarely marked new
|
||||
# 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 \
|
||||
@@ -416,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):
|
||||
@@ -446,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.
|
||||
@@ -483,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):
|
||||
"""
|
||||
@@ -493,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
|
||||
@@ -502,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
|
||||
@@ -554,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.
|
||||
@@ -563,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'))
|
||||
@@ -585,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.
|
||||
@@ -592,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']):
|
||||
@@ -609,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'):
|
||||
@@ -622,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.
|
||||
@@ -641,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" }
|
||||
@@ -680,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():
|
||||
@@ -701,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.
|
||||
|
||||
@@ -85,7 +85,7 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www\.)?%s%s/%s\?sid=(?P<storyId>\d+)" % (self.getSiteDomain(), self.getPathToArchive(), self.getViewStoryPhpName())
|
||||
return r"https?://(www\.)?%s%s/%s\?sid=(?P<storyId>\d+)" % (self.getSiteDomain(), self.getPathToArchive(), self.getViewStoryPhpName())
|
||||
|
||||
@classmethod
|
||||
def getEncoding(cls):
|
||||
|
||||
@@ -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'):
|
||||
@@ -196,13 +252,13 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
soup = soup.find('li',{'class':'message'}) # limit first post for date stuff below. ('#' posts above)
|
||||
|
||||
if threadmark_chaps or self.getConfig('always_use_forumtags'):
|
||||
## only use tags if threadmarks for chapters or
|
||||
for tag in topsoup.findAll('a',{'class':'tag'}):
|
||||
## only use tags if threadmarks for chapters or always_use_forumtags is on.
|
||||
for tag in topsoup.findAll('a',{'class':'tag'}) + topsoup.findAll('span',{'class':'prefix'}):
|
||||
tstr = stripHTML(tag)
|
||||
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,21 +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):
|
||||
|
||||
# brute force way to deal with SB's http->https change when hardcoded http urls.
|
||||
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
|
||||
|
||||
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):
|
||||
@@ -279,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)
|
||||
@@ -302,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>
|
||||
|
||||
@@ -323,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()
|
||||
|
||||
+59
-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,20 +181,24 @@ 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),
|
||||
'minimum_threadmarks':(base_xenforo_list,None,None),
|
||||
'first_post_title':(base_xenforo_list,None,None),
|
||||
'always_include_first_post':(base_xenforo_list,None,boollist),
|
||||
'always_reload_first_chapter':(base_xenforo_list,None,boollist),
|
||||
}
|
||||
|
||||
return dict(valdict)
|
||||
@@ -203,6 +214,7 @@ def get_valid_scalar_entries():
|
||||
'rating',
|
||||
'numChapters',
|
||||
'numWords',
|
||||
'words_added', # logpage only.
|
||||
'site',
|
||||
'storyId',
|
||||
'title',
|
||||
@@ -225,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',
|
||||
@@ -282,6 +299,7 @@ def get_valid_keywords():
|
||||
'image_max_size',
|
||||
'include_images',
|
||||
'include_logpage',
|
||||
'logpage_at_end',
|
||||
'include_subject_tags',
|
||||
'include_titlepage',
|
||||
'include_tocpage',
|
||||
@@ -356,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.
|
||||
@@ -373,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
|
||||
|
||||
|
||||
@@ -386,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']
|
||||
|
||||
@@ -396,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)
|
||||
@@ -416,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
|
||||
@@ -443,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()
|
||||
|
||||
@@ -473,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:
|
||||
@@ -493,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.
|
||||
@@ -505,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)
|
||||
@@ -519,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.
|
||||
@@ -543,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.
|
||||
@@ -623,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
|
||||
@@ -651,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:
|
||||
@@ -671,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)
|
||||
@@ -713,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
|
||||
|
||||
@@ -728,7 +748,7 @@ class Configurable(object):
|
||||
|
||||
def addUrlConfigSection(self,url):
|
||||
self.configuration.addUrlConfigSection(url)
|
||||
|
||||
|
||||
def isListType(self,key):
|
||||
return self.configuration.isListType(key)
|
||||
|
||||
@@ -737,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)
|
||||
|
||||
|
||||
+184
-120
@@ -25,7 +25,7 @@
|
||||
## titlepage_entries: category,genre, status,dateUpdated,rating
|
||||
## [epub]
|
||||
## # overrides defaults & site section
|
||||
## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated
|
||||
## titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated
|
||||
## [www.whofic.com:epub]
|
||||
## # overrides defaults, site section & format section
|
||||
## titlepage_entries: category,genre, status,datePublished
|
||||
@@ -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'
|
||||
@@ -509,6 +549,20 @@ first_post_title:First Post
|
||||
## if threadmarks are used. Can result in a duplicated chapter.
|
||||
always_include_first_post:false
|
||||
|
||||
## In normal operation, when updating an existing epub, old chapters
|
||||
## will be reused as-is. Normally, that works fine, but forum stories
|
||||
## sometimes have an index post as the first 'chapter', and the
|
||||
## version in the first chapter gets out of sync.
|
||||
##
|
||||
## If always_reload_first_chapter:true, then the first chapter will
|
||||
## always be downloaded again (ie, reloaded). It will NOT be maked
|
||||
## '(new)' (see mark_new_chapters). Because it is reloaded, manual
|
||||
## edits made to the first chapter will be lost.
|
||||
##
|
||||
## While intended for base_xenforoforum sites, this setting can be
|
||||
## applied to other sites.
|
||||
always_reload_first_chapter:false
|
||||
|
||||
## In normal operation, forumtags will only be populated when
|
||||
## threadmarks are used for chapters (see minimum_threadmarks above).
|
||||
## When always_use_forumtags:true, always populate forumtags.
|
||||
@@ -590,6 +644,11 @@ include_logpage: false
|
||||
## end up with Completed stories that have just one logpage entry.
|
||||
#include_logpage: smart
|
||||
|
||||
## By default, logpage is placed before the story chapters. This
|
||||
## setting, if true, will place the logpage after the chapters
|
||||
## instead.
|
||||
logpage_at_end: false
|
||||
|
||||
## items to include in the log page Empty metadata entries, or those
|
||||
## that haven't changed since the last update, will *not* appear, even
|
||||
## if in the list. You can include extra text or HTML that will be
|
||||
@@ -707,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
|
||||
@@ -754,6 +825,19 @@ extratags: FanFiction,Testing,Text
|
||||
[test1.com:html]
|
||||
extratags: FanFiction,Testing,HTML
|
||||
|
||||
[adult-fanfiction.org]
|
||||
extra_valid_entries:eroticatags,disclaimer
|
||||
eroticatags_label:Erotica Tags
|
||||
disclaimer_label:Disclaimer
|
||||
extra_titlepage_entries:eroticatags,disclaimer
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[archive.skyehawke.com]
|
||||
|
||||
[archiveofourown.org]
|
||||
@@ -853,18 +937,6 @@ extracategories:The Sentinel
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[bdsm-geschichten.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## This site offers no index page so we can either guess the chapter URLs
|
||||
## by dec/incrementing numbers ('guess') or walk all the chapters in the metadata
|
||||
## parsing state ('parse'). Since guessing can lead to errors for non-standard
|
||||
## story URLs, the default is to parse
|
||||
#find_chapters:guess
|
||||
|
||||
[bloodshedverse.com]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
@@ -910,15 +982,6 @@ strip_text_links:true
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Blood Ties
|
||||
|
||||
[buffynfaith.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[fanfic.castletv.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1145,13 +1208,20 @@ romance_label: Romance
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
[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
|
||||
@@ -1209,6 +1279,14 @@ views_label:Views
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[finestories.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1250,13 +1328,9 @@ universe_as_series: true
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[forums.spacebattles.com]
|
||||
[forum.questionablequesting.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
@@ -1264,18 +1338,24 @@ cover_exclusion_regexp:/css/bir.png
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
[forums.spacebattles.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[harem.lucifael.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
extracharacters:Hermione Granger
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:read,reviews
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[hlfiction.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
@@ -1389,6 +1469,19 @@ extra_titlepage_entries: eroticatags
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Merlin
|
||||
|
||||
[mujaji.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[national-library.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:West Wing
|
||||
@@ -1490,15 +1583,21 @@ extracategories:My Little Pony: Friendship is Magic
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:The Pretender
|
||||
|
||||
[forum.questionablequesting.com]
|
||||
## see [base_xenforoforum]
|
||||
[quotev.com]
|
||||
extra_valid_entries:pages,readers,reads,favorites,searchtags,comments
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
searchtags_label:Search Tags
|
||||
comments_label:Comments
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
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
|
||||
@@ -1524,22 +1623,6 @@ extracategories:Supernatural
|
||||
extracharacters:Sam,Dean
|
||||
extraships:Sam/Dean
|
||||
|
||||
[scarhead.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[sheppardweir.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1681,15 +1764,6 @@ readings_label: Readings
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[tokra.fandomnet.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[tolkienfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
@@ -1757,6 +1831,12 @@ extraships:Draco Malfoy/Ginny Weasley
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[www.deepinmysoul.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.destinysgateway.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -2051,11 +2131,6 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:NCIS
|
||||
|
||||
[www.nickngreg.nl]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:CSI
|
||||
extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
[www.phoenixsong.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -2110,6 +2185,8 @@ extracategories:Psych
|
||||
extracategories:Queer as Folk
|
||||
|
||||
[quotev.com]
|
||||
user_agent:
|
||||
slow_down_sleep_time:2
|
||||
extra_valid_entries:pages,readers,reads,favorites,searchtags,comments
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
@@ -2186,23 +2263,9 @@ extracategories:Lord of the Rings
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[www.twcslibrary.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## twcslibrary.net (ab)uses series as personal reading lists.
|
||||
collect_series: false
|
||||
|
||||
[www.tthfanfic.org]
|
||||
user_agent:
|
||||
slow_down_sleep_time:2
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
@@ -2237,6 +2300,22 @@ pairingcat_to_characters_ships:true
|
||||
## instead be added to characters Buffy, Spike and ships Buffy/Spike
|
||||
romancecat_to_characters_ships:true
|
||||
|
||||
[www.twcslibrary.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## twcslibrary.net (ab)uses series as personal reading lists.
|
||||
collect_series: false
|
||||
|
||||
[www.twilightarchives.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Twilight
|
||||
@@ -2285,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
|
||||
@@ -2349,20 +2429,6 @@ extracategories:Andromeda
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Artemis Fowl
|
||||
|
||||
[www.therabidreader.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
|
||||
[www.naiceanilme.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -2376,8 +2442,6 @@ extracategories:Artemis Fowl
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
+32
-20
@@ -21,6 +21,10 @@ def get_dcsource(inputio):
|
||||
def get_dcsource_chaptercount(inputio):
|
||||
return get_update_data(inputio,getfilecount=True,getsoups=False)[:2] # (source,filecount)
|
||||
|
||||
def get_cover_data(inputio):
|
||||
# (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata)
|
||||
return get_update_data(inputio,getfilecount=True,getsoups=False)[4]
|
||||
|
||||
def get_update_data(inputio,
|
||||
getfilecount=True,
|
||||
getsoups=True):
|
||||
@@ -50,26 +54,32 @@ def get_update_data(inputio,
|
||||
if item.getAttribute("type") == "cover":
|
||||
# there is a cover (x)html file, save the soup for it.
|
||||
href=relpath+item.getAttribute("href")
|
||||
oldcoverhtmlhref = href
|
||||
oldcoverhtmldata = epub.read(href)
|
||||
oldcoverhtmltype = "application/xhtml+xml"
|
||||
for item in contentdom.getElementsByTagName("item"):
|
||||
if( relpath+item.getAttribute("href") == oldcoverhtmlhref ):
|
||||
oldcoverhtmltype = item.getAttribute("media-type")
|
||||
break
|
||||
soup = bs.BeautifulSoup(oldcoverhtmldata.decode("utf-8"),"html5lib")
|
||||
src = None
|
||||
# first img or image tag.
|
||||
imgs = soup.findAll('img')
|
||||
if imgs:
|
||||
src = get_path_part(href)+imgs[0]['src']
|
||||
else:
|
||||
imgs = soup.findAll('image')
|
||||
try:
|
||||
oldcoverhtmlhref = href
|
||||
oldcoverhtmldata = epub.read(href)
|
||||
oldcoverhtmltype = "application/xhtml+xml"
|
||||
for item in contentdom.getElementsByTagName("item"):
|
||||
if( relpath+item.getAttribute("href") == oldcoverhtmlhref ):
|
||||
oldcoverhtmltype = item.getAttribute("media-type")
|
||||
break
|
||||
soup = bs.BeautifulSoup(oldcoverhtmldata.decode("utf-8"),"html5lib")
|
||||
# first img or image tag.
|
||||
imgs = soup.findAll('img')
|
||||
if imgs:
|
||||
src=get_path_part(href)+imgs[0]['xlink:href']
|
||||
src = get_path_part(href)+imgs[0]['src']
|
||||
else:
|
||||
imgs = soup.findAll('image')
|
||||
if imgs:
|
||||
src=get_path_part(href)+imgs[0]['xlink:href']
|
||||
|
||||
if not src:
|
||||
continue
|
||||
except Exception as e:
|
||||
## Calibre's Polish Book corrupts sub-book covers.
|
||||
logger.warn("Cover (x)html file %s not found"%href)
|
||||
logger.warn("Exception: %s"%(unicode(e)))
|
||||
|
||||
if not src:
|
||||
continue
|
||||
try:
|
||||
# remove all .. and the path part above it, if present.
|
||||
# Mostly for epubs edited by Sigil.
|
||||
@@ -84,7 +94,7 @@ def get_update_data(inputio,
|
||||
oldcover = (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata)
|
||||
except Exception as e:
|
||||
logger.warn("Cover Image %s not found"%src)
|
||||
logger.warn("Exception: %s"%(unicode(e)),exc_info=True)
|
||||
logger.warn("Exception: %s"%(unicode(e)))
|
||||
|
||||
filecount = 0
|
||||
soups = [] # list of xhmtl blocks
|
||||
@@ -100,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'):
|
||||
|
||||
@@ -125,7 +125,10 @@ def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrict
|
||||
|
||||
def get_urls_from_text(data,configuration=None,normalize=False):
|
||||
urls = collections.OrderedDict()
|
||||
data=unicode(data)
|
||||
try:
|
||||
data = unicode(data)
|
||||
except UnicodeDecodeError:
|
||||
data=data.decode('utf8') ## for when called outside calibre.
|
||||
|
||||
if not configuration:
|
||||
configuration = Configuration(["test1.com"],"EPUB",lightweight=True)
|
||||
@@ -229,7 +232,7 @@ def get_urls_from_imap(srv,user,passwd,folder,markread=True):
|
||||
if part.get_content_type() == 'text/html':
|
||||
urllist.extend(get_urls_from_html(part.get_payload(decode=True)))
|
||||
except Exception as e:
|
||||
logger.error("Failed to read email content: %s"%e)
|
||||
logger.error("Failed to read email content: %s"%e,exc_info=True)
|
||||
#logger.debug "urls:%s"%get_urls_from_text(get_first_text_block(email_message))
|
||||
|
||||
if urllist and markread:
|
||||
|
||||
+20
-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')
|
||||
|
||||
@@ -1069,6 +1077,9 @@ class Story(Configurable):
|
||||
if imgurl not in self.imgurls:
|
||||
|
||||
try:
|
||||
if imgurl == 'failedtoload':
|
||||
raise Exception("Previously failed to load")
|
||||
|
||||
parsedUrl = urlparse.urlparse(imgurl)
|
||||
if self.getConfig('no_image_processing'):
|
||||
(data,ext,mime) = no_convert_image(imgurl,
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -46,7 +49,7 @@ class EpubWriter(BaseStoryWriter):
|
||||
BaseStoryWriter.__init__(self, config, story)
|
||||
|
||||
self.EPUB_CSS = string.Template('''${output_css}''')
|
||||
|
||||
|
||||
self.EPUB_TITLE_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
@@ -117,7 +120,7 @@ ${value}<br />
|
||||
self.EPUB_TOC_ENTRY = string.Template('''
|
||||
<a href="file${index}.xhtml">${chapter}</a><br />
|
||||
''')
|
||||
|
||||
|
||||
self.EPUB_TOC_PAGE_END = string.Template('''
|
||||
</div>
|
||||
</body>
|
||||
@@ -156,15 +159,15 @@ ${value}<br />
|
||||
self.EPUB_LOG_UPDATE_START = string.Template('''
|
||||
<p class='log_entry'>
|
||||
''')
|
||||
|
||||
|
||||
self.EPUB_LOG_ENTRY = string.Template('''
|
||||
<b>${label}:</b> <span id="${id}">${value}</span>
|
||||
''')
|
||||
|
||||
|
||||
self.EPUB_LOG_UPDATE_END = string.Template('''
|
||||
</p><hr />
|
||||
''')
|
||||
|
||||
|
||||
self.EPUB_LOG_PAGE_END = string.Template('''
|
||||
</body>
|
||||
</html>
|
||||
@@ -184,7 +187,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
<img src="${coverimg}" alt="cover"/>
|
||||
</div></body></html>
|
||||
''')
|
||||
|
||||
|
||||
def writeLogPage(self, out):
|
||||
"""
|
||||
Write the log page, but only include entries that there's
|
||||
@@ -201,12 +204,12 @@ div { margin: 0pt; padding: 0pt; }
|
||||
END = string.Template(self.getConfig("logpage_end"))
|
||||
else:
|
||||
END = self.EPUB_LOG_PAGE_END
|
||||
|
||||
|
||||
# if there's a self.story.logfile, there's an existing log
|
||||
# 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:
|
||||
@@ -234,7 +237,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
pass
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def _makeLogEntry(self, oldvalues={}):
|
||||
if self.hasConfig("logpage_update_start"):
|
||||
START = string.Template(self.getConfig("logpage_update_start"))
|
||||
@@ -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)
|
||||
@@ -275,16 +286,16 @@ div { margin: 0pt; padding: 0pt; }
|
||||
# mostly it makes it easy to tell when you get the
|
||||
# keyword wrong.
|
||||
retval = retval + entry
|
||||
|
||||
|
||||
retval = retval + END.substitute(self.story.getAllMetadata())
|
||||
|
||||
|
||||
if self.getConfig('replace_hr'):
|
||||
# replacing a self-closing tag with a container tag in the
|
||||
# soup is more difficult than it first appears. So cheat.
|
||||
retval = re.sub("<hr[^>]*>","<div class='center'>* * *</div>",retval)
|
||||
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
def writeStoryImpl(self, out):
|
||||
|
||||
## Python 2.5 ZipFile is rather more primative than later
|
||||
@@ -305,7 +316,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
## Re-open file for content.
|
||||
outputepub = ZipFile(zipio, 'a', compression=ZIP_DEFLATED)
|
||||
outputepub.debug=3
|
||||
|
||||
|
||||
## Create META-INF/container.xml file. The only thing it does is
|
||||
## point to content.opf
|
||||
containerdom = getDOMImplementation().createDocument(None, "container", None)
|
||||
@@ -332,7 +343,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
self.getMetadata('site'),
|
||||
self.story.getList('authorId')[0],
|
||||
self.getMetadata('storyId'))
|
||||
|
||||
|
||||
contentdom = getDOMImplementation().createDocument(None, "package", None)
|
||||
package = contentdom.documentElement
|
||||
package.setAttribute("version","2.0")
|
||||
@@ -374,12 +385,12 @@ div { margin: 0pt; padding: 0pt; }
|
||||
metadata.appendChild(newTag(contentdom,"dc:date",
|
||||
attrs={"opf:event":"publication"},
|
||||
text=self.story.getMetadataRaw('datePublished').strftime("%Y-%m-%d")))
|
||||
|
||||
|
||||
if self.story.getMetadataRaw('dateCreated'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:date",
|
||||
attrs={"opf:event":"creation"},
|
||||
text=self.story.getMetadataRaw('dateCreated').strftime("%Y-%m-%d")))
|
||||
|
||||
|
||||
if self.story.getMetadataRaw('dateUpdated'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:date",
|
||||
attrs={"opf:event":"modification"},
|
||||
@@ -387,7 +398,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
metadata.appendChild(newTag(contentdom,"meta",
|
||||
attrs={"name":"calibre:timestamp",
|
||||
"content":self.story.getMetadataRaw('dateUpdated').strftime("%Y-%m-%dT%H:%M:%S")}))
|
||||
|
||||
|
||||
if self.getMetadata('description'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:description",text=
|
||||
self.getMetadata('description')))
|
||||
@@ -395,11 +406,11 @@ div { margin: 0pt; padding: 0pt; }
|
||||
for subject in self.story.getSubjectTags():
|
||||
metadata.appendChild(newTag(contentdom,"dc:subject",text=subject))
|
||||
|
||||
|
||||
|
||||
if self.getMetadata('site'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:publisher",
|
||||
text=self.getMetadata('site')))
|
||||
|
||||
|
||||
if self.getMetadata('storyUrl'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:identifier",
|
||||
attrs={"opf:scheme":"URL"},
|
||||
@@ -415,7 +426,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
|
||||
guide = None
|
||||
coverIO = None
|
||||
|
||||
|
||||
coverimgid = "image0000"
|
||||
if not self.story.cover and self.story.oldcover:
|
||||
logger.debug("writer_epub: no new cover, has old cover, write image.")
|
||||
@@ -427,7 +438,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
oldcoverimgdata) = self.story.oldcover
|
||||
outputepub.writestr(oldcoverhtmlhref,oldcoverhtmldata)
|
||||
outputepub.writestr(oldcoverimghref,oldcoverimgdata)
|
||||
|
||||
|
||||
coverimgid = "image0"
|
||||
items.append((coverimgid,
|
||||
oldcoverimghref,
|
||||
@@ -441,8 +452,8 @@ div { margin: 0pt; padding: 0pt; }
|
||||
guide.appendChild(newTag(contentdom,"reference",attrs={"type":"cover",
|
||||
"title":"Cover",
|
||||
"href":oldcoverhtmlhref}))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if self.getConfig('include_images'):
|
||||
imgcount=0
|
||||
@@ -459,7 +470,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
# just the first image.
|
||||
coverimgid = items[-1][0]
|
||||
|
||||
|
||||
|
||||
items.append(("style","OEBPS/stylesheet.css","text/css",None))
|
||||
|
||||
if self.story.cover:
|
||||
@@ -467,7 +478,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
# for it to work on Nook.
|
||||
items.append(("cover","OEBPS/cover.xhtml","application/xhtml+xml",None))
|
||||
itemrefs.append("cover")
|
||||
#
|
||||
#
|
||||
# <meta name="cover" content="cover.jpg"/>
|
||||
metadata.appendChild(newTag(contentdom,"meta",{"content":coverimgid,
|
||||
"name":"cover"}))
|
||||
@@ -480,14 +491,14 @@ div { margin: 0pt; padding: 0pt; }
|
||||
guide.appendChild(newTag(contentdom,"reference",attrs={"type":"cover",
|
||||
"title":"Cover",
|
||||
"href":"OEBPS/cover.xhtml"}))
|
||||
|
||||
|
||||
if self.hasConfig("cover_content"):
|
||||
COVER = string.Template(self.getConfig("cover_content"))
|
||||
else:
|
||||
COVER = self.EPUB_COVER
|
||||
coverIO = StringIO.StringIO()
|
||||
coverIO.write(COVER.substitute(dict(self.story.getAllMetadata().items()+{'coverimg':self.story.cover}.items())))
|
||||
|
||||
|
||||
if self.getConfig("include_titlepage"):
|
||||
items.append(("title_page","OEBPS/title_page.xhtml","application/xhtml+xml","Title Page"))
|
||||
itemrefs.append("title_page")
|
||||
@@ -495,14 +506,15 @@ div { margin: 0pt; padding: 0pt; }
|
||||
items.append(("toc_page","OEBPS/toc_page.xhtml","application/xhtml+xml","Table of Contents"))
|
||||
itemrefs.append("toc_page")
|
||||
|
||||
## save where to insert logpage.
|
||||
logpage_indices = (len(items),len(itemrefs))
|
||||
|
||||
dologpage = ( self.getConfig("include_logpage") == "smart" and \
|
||||
(self.story.logfile or self.story.getMetadataRaw("status") == "In-Progress") ) \
|
||||
or self.getConfig("include_logpage") == "true"
|
||||
|
||||
if dologpage:
|
||||
items.append(("log_page","OEBPS/log_page.xhtml","application/xhtml+xml","Update Log"))
|
||||
itemrefs.append("log_page")
|
||||
|
||||
## 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
|
||||
@@ -511,6 +523,14 @@ 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":
|
||||
## insert logpage after chapters.
|
||||
logpage_indices = (len(items),len(itemrefs))
|
||||
items.insert(logpage_indices[0],("log_page","OEBPS/log_page.xhtml","application/xhtml+xml","Update Log"))
|
||||
itemrefs.insert(logpage_indices[1],"log_page")
|
||||
|
||||
manifest = contentdom.createElement("manifest")
|
||||
package.appendChild(manifest)
|
||||
@@ -520,7 +540,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
attrs={'id':id,
|
||||
'href':href,
|
||||
'media-type':type}))
|
||||
|
||||
|
||||
spine = newTag(contentdom,"spine",attrs={"toc":"ncx"})
|
||||
package.appendChild(spine)
|
||||
for itemref in itemrefs:
|
||||
@@ -530,10 +550,10 @@ div { margin: 0pt; padding: 0pt; }
|
||||
# guide only exists if there's a cover.
|
||||
if guide:
|
||||
package.appendChild(guide)
|
||||
|
||||
|
||||
# write content.opf to zip.
|
||||
contentxml = contentdom.toxml(encoding='utf-8')
|
||||
|
||||
|
||||
# tweak for brain damaged Nook STR. Nook insists on name before content.
|
||||
contentxml = contentxml.replace('<meta content="%s" name="cover"/>'%coverimgid,
|
||||
'<meta name="cover" content="%s"/>'%coverimgid)
|
||||
@@ -557,11 +577,11 @@ div { margin: 0pt; padding: 0pt; }
|
||||
attrs={"name":"dtb:totalPageCount", "content":"0"}))
|
||||
head.appendChild(newTag(tocncxdom,"meta",
|
||||
attrs={"name":"dtb:maxPageNumber", "content":"0"}))
|
||||
|
||||
|
||||
docTitle = tocncxdom.createElement("docTitle")
|
||||
docTitle.appendChild(newTag(tocncxdom,"text",text=self.getMetadata('title')))
|
||||
ncx.appendChild(docTitle)
|
||||
|
||||
|
||||
tocnavMap = tocncxdom.createElement("navMap")
|
||||
ncx.appendChild(tocnavMap)
|
||||
|
||||
@@ -586,14 +606,14 @@ div { margin: 0pt; padding: 0pt; }
|
||||
navLabel.appendChild(newTag(tocncxdom,"text",text=stripHTML(title)))
|
||||
navPoint.appendChild(newTag(tocncxdom,"content",attrs={"src":href}))
|
||||
index=index+1
|
||||
|
||||
|
||||
# write toc.ncx to zip file
|
||||
outputepub.writestr("toc.ncx",tocncxdom.toxml(encoding='utf-8'))
|
||||
tocncxdom.unlink()
|
||||
del tocncxdom
|
||||
|
||||
# write stylesheet.css file.
|
||||
outputepub.writestr("OEBPS/stylesheet.css",self.EPUB_CSS.substitute(self.story.getAllMetadata()))
|
||||
outputepub.writestr("OEBPS/stylesheet.css",self.EPUB_CSS.substitute(self.story.getAllMetadata()))
|
||||
|
||||
# write title page.
|
||||
if self.getConfig("titlepage_use_table"):
|
||||
@@ -612,7 +632,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
if coverIO:
|
||||
outputepub.writestr("OEBPS/cover.xhtml",coverIO.getvalue())
|
||||
coverIO.close()
|
||||
|
||||
|
||||
titlepageIO = StringIO.StringIO()
|
||||
self.writeTitlePage(out=titlepageIO,
|
||||
START=TITLE_PAGE_START,
|
||||
@@ -624,7 +644,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
outputepub.writestr("OEBPS/title_page.xhtml",titlepageIO.getvalue())
|
||||
titlepageIO.close()
|
||||
|
||||
# write toc page.
|
||||
# write toc page.
|
||||
tocpageIO = StringIO.StringIO()
|
||||
self.writeTOCPage(tocpageIO,
|
||||
self.EPUB_TOC_PAGE_START,
|
||||
@@ -645,14 +665,28 @@ div { margin: 0pt; padding: 0pt; }
|
||||
CHAPTER_START = string.Template(self.getConfig("chapter_start"))
|
||||
else:
|
||||
CHAPTER_START = self.EPUB_CHAPTER_START
|
||||
|
||||
|
||||
if self.hasConfig('chapter_end'):
|
||||
CHAPTER_END = string.Template(self.getConfig("chapter_end"))
|
||||
else:
|
||||
CHAPTER_END = self.EPUB_CHAPTER_END
|
||||
|
||||
|
||||
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),
|
||||
@@ -664,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.2",
|
||||
# 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
-11
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanficfare
|
||||
application: fanficfare
|
||||
version: 2-3-02
|
||||
version: 2-5-0
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
@@ -34,13 +34,3 @@ handlers:
|
||||
|
||||
- url: /.*
|
||||
script: main.app
|
||||
|
||||
#builtins:
|
||||
#- datastore_admin: on
|
||||
|
||||
libraries:
|
||||
- name: django
|
||||
version: "1.2"
|
||||
|
||||
- name: PIL
|
||||
version: "1.1.7"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div id='urlbox'>
|
||||
<div id='greeting'>
|
||||
<p>Hi, {{ nickname }}! This is FanFicFare, which makes reading stories from various websites
|
||||
much easier. </p>
|
||||
much easier by helping you download them to EBook files. </p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
@@ -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-01.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'>
|
||||
@@ -75,7 +75,7 @@
|
||||
<div id='urlbox'>
|
||||
<div id='greeting'>
|
||||
<p>
|
||||
This is a FanFicFare, which makes reading stories from various websites much easier. Before you
|
||||
This is a FanFicFare, which makes reading stories from various websites much easier by helping you download them to EBook files. Before you
|
||||
can start downloading fanfics, you need to login, so FanFicFare can remember your fanfics and store them.
|
||||
</p>
|
||||
<p><a href="{{ login_url }}">Login using Google account</a></p>
|
||||
@@ -85,17 +85,17 @@
|
||||
|
||||
<div id='typebox'>
|
||||
<p>
|
||||
<b>FanFicFare calibre Plugin</b>
|
||||
<b>FanFicFare Calibre Plugin</b>
|
||||
<br /><br />
|
||||
|
||||
There's also a version of this downloader that runs inside
|
||||
the popular <a href="http://calibre-ebook.com/">calibre</a>
|
||||
the popular <a href="http://calibre-ebook.com/">Calibre</a>
|
||||
ebook management package as a plugin.
|
||||
|
||||
<br /><br />
|
||||
|
||||
Once you have calibre installed and running, inside
|
||||
calibre, you can go to 'Get plugins to enhance calibre' or
|
||||
Once you have Calibre installed and running, inside
|
||||
Calibre, you can go to 'Get plugins to enhance calibre' or
|
||||
'Get new plugins' and
|
||||
install <a href="http://www.mobileread.com/forums/showthread.php?t=259221">FanFicFare</a>.
|
||||
|
||||
|
||||
+16
-37
@@ -30,20 +30,6 @@ import datetime
|
||||
import traceback
|
||||
from StringIO import StringIO
|
||||
|
||||
## Just to shut up the appengine warning about "You are using the
|
||||
## default Django version (0.96). The default Django version will
|
||||
## change in an App Engine release in the near future. Please call
|
||||
## use_library() to explicitly select a Django version. For more
|
||||
## information see
|
||||
## http://code.google.com/appengine/docs/python/tools/libraries.html#Django"
|
||||
## Note that if you are using the SDK App Engine Launcher and hit an SDK
|
||||
## Console page first, you will get a django version mismatch error when you
|
||||
## to go hit one of the application pages. Just change a file again, and
|
||||
## make sure to hit an app page before the SDK page to clear it.
|
||||
#os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
|
||||
#from google.appengine.dist import use_library
|
||||
#use_library('django', '1.2')
|
||||
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.api import taskqueue
|
||||
from google.appengine.api import users
|
||||
@@ -59,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')
|
||||
|
||||
@@ -110,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
|
||||
@@ -153,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
|
||||
@@ -254,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"
|
||||
@@ -309,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')
|
||||
@@ -327,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()))
|
||||
|
||||
@@ -336,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))
|
||||
@@ -347,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()
|
||||
@@ -375,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')
|
||||
@@ -390,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)
|
||||
@@ -551,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))
|
||||
@@ -577,7 +566,7 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
smeta.meta = allmeta
|
||||
smeta.date = datetime.datetime.now()
|
||||
smeta.put()
|
||||
|
||||
|
||||
logging.info("Download finished OK")
|
||||
del data
|
||||
|
||||
@@ -630,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