mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a593bd201c | ||
|
|
37885a9039 | ||
|
|
97437aa7da | ||
|
|
ccfcd801a3 | ||
|
|
4df321c18e | ||
|
|
664fb639bd | ||
|
|
0cd71263b1 | ||
|
|
2fc1a14ac4 | ||
|
|
8772b50451 | ||
|
|
7390424f0f | ||
|
|
3a16c49c2e | ||
|
|
d8066d4bcf | ||
|
|
61fff980b5 | ||
|
|
bf704fcdc7 | ||
|
|
c9f86bd784 | ||
|
|
76faea3b4e | ||
|
|
6b3a19bb45 | ||
|
|
df7bf64845 | ||
|
|
6b8ccc0073 | ||
|
|
2083a79464 | ||
|
|
c0a962bd9d | ||
|
|
66b33369c1 | ||
|
|
82d28f26f4 | ||
|
|
ff6cd7ccf1 | ||
|
|
7d8691171e | ||
|
|
329c55d8ed | ||
|
|
e68d2484a6 | ||
|
|
eb5f10f5c1 | ||
|
|
0161991c2a | ||
|
|
be0d48ec7b | ||
|
|
cf54f274d4 | ||
|
|
bffc389bcf | ||
|
|
6de973fe2d | ||
|
|
67b44a991b | ||
|
|
13b0aa358a | ||
|
|
047a03c61c | ||
|
|
b5cc612f96 | ||
|
|
df836412cc | ||
|
|
a28b8cb139 | ||
|
|
03ceecd38f | ||
|
|
f5d511a996 | ||
|
|
1724c6f42f | ||
|
|
58ad4c0381 | ||
|
|
b1c9fd0e30 | ||
|
|
b8f168add6 | ||
|
|
20e90a5cd5 | ||
|
|
dd4a22e7d8 | ||
|
|
58033a1afa | ||
|
|
1fd6913dfb | ||
|
|
9ccdc4d884 | ||
|
|
12dd969560 | ||
|
|
0af5e1e9b1 | ||
|
|
4a57d95eb2 | ||
|
|
8e9870d4fd | ||
|
|
30bafd4e53 | ||
|
|
c14c52f670 | ||
|
|
664001c35c | ||
|
|
572f4e4c7f | ||
|
|
3ccfa1086a | ||
|
|
2f0e431e35 | ||
|
|
8730e88658 | ||
|
|
bf277ac005 | ||
|
|
1fa2bf356f | ||
|
|
90891a9a52 | ||
|
|
0a2d4c2aca | ||
|
|
cbd8d9c34b | ||
|
|
7e07be9ff7 | ||
|
|
35efd0fe98 | ||
|
|
ca5077b9ef | ||
|
|
a167ec4c59 | ||
|
|
44e12d1ef3 | ||
|
|
83c8987b8b | ||
|
|
2e01380b5d | ||
|
|
d221adabbb | ||
|
|
7d831b9cdc |
+19
-12
@@ -7,15 +7,21 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
import sys, os
|
||||
if sys.version_info >= (2, 7):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFF:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
loghandler.setFormatter(logging.Formatter("FFF: %(levelname)s: %(asctime)s: %(filename)s(%(lineno)d): %(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
if os.environ.get('CALIBRE_WORKER', None) is not None or DEBUG:
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
loghandler.setLevel(logging.CRITICAL)
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
@@ -42,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, 2, 17)
|
||||
version = (2, 4, 0)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
@@ -110,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"]),
|
||||
)
|
||||
|
||||
+240
-141
@@ -10,15 +10,17 @@ __docformat__ = 'restructuredtext en'
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import traceback, copy, threading
|
||||
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,10 +331,13 @@ 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
|
||||
prefs['errorcol'] = unicode(convert_qvariant(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex())))
|
||||
prefs['save_all_errors'] = self.cust_columns_tab.save_all_errors.isChecked()
|
||||
|
||||
# metadata column
|
||||
prefs['savemetacol'] = unicode(convert_qvariant(self.cust_columns_tab.savemetacol.itemData(self.cust_columns_tab.savemetacol.currentIndex())))
|
||||
@@ -349,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()
|
||||
@@ -359,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):
|
||||
@@ -377,7 +384,7 @@ class BasicTab(QWidget):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
|
||||
topl = QVBoxLayout()
|
||||
self.setLayout(topl)
|
||||
|
||||
@@ -432,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'])
|
||||
@@ -448,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'])
|
||||
@@ -465,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'])
|
||||
@@ -498,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.")
|
||||
@@ -515,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'])
|
||||
@@ -567,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)
|
||||
@@ -595,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()
|
||||
@@ -611,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(),
|
||||
@@ -620,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'],
|
||||
@@ -637,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"),
|
||||
@@ -651,68 +666,109 @@ 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)
|
||||
|
||||
label = QLabel(_('These settings provide more detailed control over what metadata will be displayed inside the ebook as well as let you set %(isa)s and %(u)s/%(p)s for different sites.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
# self.l.addSpacing(5)
|
||||
|
||||
label = QLabel(_("FanFicFare now includes find, color coding, and error checking for personal.ini editing. Red generally indicates errors."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
# self.label = QLabel('personal.ini:')
|
||||
# self.l.addWidget(self.label)
|
||||
|
||||
# self.ini = QTextEdit(self)
|
||||
# try:
|
||||
# self.ini.setFont(QFont("Courier",
|
||||
# self.plugin_action.gui.font().pointSize()+1))
|
||||
# except Exception as e:
|
||||
# logger.error("Couldn't get font: %s"%e)
|
||||
# self.ini.setLineWrapMode(QTextEdit.NoWrap)
|
||||
# self.ini.setText(prefs['personal.ini'])
|
||||
# self.l.addWidget(self.ini)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
self.personalini = prefs['personal.ini']
|
||||
|
||||
groupbox = QGroupBox(_("personal.ini"))
|
||||
vert = QVBoxLayout()
|
||||
groupbox.setLayout(vert)
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
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)
|
||||
self.l.addWidget(self.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()
|
||||
vert.addLayout(horz)
|
||||
self.ini_button = QPushButton(_('View "Safe" personal.ini'), self)
|
||||
#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)
|
||||
self.defaults.clicked.connect(self.show_defaults)
|
||||
horz.addWidget(self.defaults)
|
||||
|
||||
label = QLabel(view_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
|
||||
groupbox = QGroupBox(_("Calibre Columns"))
|
||||
vert = QVBoxLayout()
|
||||
groupbox.setLayout(vert)
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
pass_label = _("If checked, when updating/overwriting an existing book, FanFicFare will have the Calibre Columns available to use in replace_metadata, title_page, etc.<br>Click the button below to see the Calibre Column namess.")%no_trans
|
||||
self.cal_cols_pass_in = QCheckBox(_('Pass Calibre Columns into FanFicFare on Update/Overwrite')%no_trans,self)
|
||||
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.")
|
||||
self.showcalcols = QPushButton(_('Show Calibre Column Names'), self)
|
||||
self.showcalcols.setToolTip(col_label)
|
||||
self.showcalcols.clicked.connect(self.show_showcalcols)
|
||||
horz.addWidget(self.showcalcols)
|
||||
|
||||
label = QLabel(col_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
label = QLabel(_("Changes will only be saved if you click 'OK' to leave Customize FanFicFare."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', self)
|
||||
self.defaults.setToolTip(_("View all of the plugin's configurable settings\nand their default settings."))
|
||||
self.defaults.clicked.connect(self.show_defaults)
|
||||
self.l.addWidget(self.defaults)
|
||||
|
||||
self.cal_cols_pass_in = QCheckBox(_('Pass Calibre Columns into FanFicFare on Update/Overwrite')%no_trans,self)
|
||||
self.cal_cols_pass_in.setToolTip(_("If checked, when updating/overwriting an existing book, FanFicFare will have the Calibre Columns available to use in replace_metadata, title_page, etc.<br>Click the button below to see the Calibre Column namess.")%no_trans)
|
||||
self.cal_cols_pass_in.setChecked(prefs['cal_cols_pass_in'])
|
||||
self.l.addWidget(self.cal_cols_pass_in)
|
||||
|
||||
self.showcalcols = QPushButton(_('Show Calibre Column Names'), self)
|
||||
self.showcalcols.setToolTip(_("FanFicFare can pass the Calibre Columns into the download/update process.<br>This will show you the columns available by name."))
|
||||
self.showcalcols.clicked.connect(self.show_showcalcols)
|
||||
self.l.addWidget(self.showcalcols)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
# let edit box fill the space.
|
||||
|
||||
|
||||
def show_defaults(self):
|
||||
IniTextDialog(self,
|
||||
get_resources('plugin-defaults.ini'),
|
||||
@@ -722,7 +778,19 @@ 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(),
|
||||
title=_("View 'Safe' personal.ini"),
|
||||
label=_("View your personal.ini with usernames and passwords removed. For safely sharing your personal.ini settings with others."),
|
||||
save_size_name='fff:safe personal.ini',
|
||||
read_only=True)
|
||||
d.exec_()
|
||||
|
||||
def add_ini_button(self):
|
||||
d = IniTextDialog(self,
|
||||
self.personalini,
|
||||
@@ -734,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.
|
||||
@@ -755,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)
|
||||
|
||||
@@ -771,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)
|
||||
@@ -793,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)
|
||||
@@ -810,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)
|
||||
|
||||
@@ -844,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"
|
||||
@@ -852,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"
|
||||
@@ -893,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)
|
||||
@@ -906,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)
|
||||
@@ -951,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)
|
||||
@@ -965,7 +1033,7 @@ class CalibreCoverTab(QWidget):
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
|
||||
self.gc_dropdowns = {}
|
||||
|
||||
sitelist = getSiteSections()
|
||||
@@ -998,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)
|
||||
@@ -1014,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)
|
||||
|
||||
@@ -1028,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)
|
||||
|
||||
@@ -1059,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'])
|
||||
@@ -1072,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)
|
||||
|
||||
@@ -1106,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)
|
||||
@@ -1116,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') \
|
||||
@@ -1133,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_()
|
||||
@@ -1215,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()
|
||||
@@ -1225,7 +1293,7 @@ class CustomColumnsTab(QWidget):
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
|
||||
self.custcol_dropdowns = {}
|
||||
self.custcol_newonlycheck = {}
|
||||
|
||||
@@ -1237,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:
|
||||
@@ -1267,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)
|
||||
@@ -1277,16 +1345,17 @@ 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.)")
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
|
||||
self.errorcol = QComboBox(self)
|
||||
self.errorcol.setToolTip(tooltip)
|
||||
self.errorcol.addItem('','none')
|
||||
@@ -1295,8 +1364,17 @@ 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'+
|
||||
'\n'.join((_("Not Overwriting, web site is not newer."),
|
||||
_("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.)")
|
||||
@@ -1310,6 +1388,10 @@ class CustomColumnsTab(QWidget):
|
||||
self.savemetacol.addItem(column['name'],key)
|
||||
self.savemetacol.setCurrentIndex(self.savemetacol.findData(prefs['savemetacol']))
|
||||
horz.addWidget(self.savemetacol)
|
||||
|
||||
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'])
|
||||
@@ -1323,7 +1405,7 @@ class StandardColumnsTab(QWidget):
|
||||
QWidget.__init__(self)
|
||||
|
||||
columns=OrderedDict()
|
||||
|
||||
|
||||
columns["title"]=_("Title")
|
||||
columns["authors"]=_("Author(s)")
|
||||
columns["publisher"]=_("Publisher")
|
||||
@@ -1342,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():
|
||||
@@ -1357,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)
|
||||
@@ -1370,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):
|
||||
@@ -1379,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)
|
||||
@@ -1398,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)
|
||||
@@ -1425,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. "
|
||||
@@ -1462,5 +1561,5 @@ class ImapTab(QWidget):
|
||||
self.l.addWidget(label,row,0,1,-1,Qt.AlignTop)
|
||||
self.l.setRowStretch(row,1)
|
||||
row+=1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+57
-22
@@ -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
|
||||
@@ -158,9 +158,10 @@ class RejectUrlEntry:
|
||||
return retval
|
||||
|
||||
class NotGoingToDownload(Exception):
|
||||
def __init__(self,error,icon='dialog_error.png'):
|
||||
def __init__(self,error,icon='dialog_error.png',showerror=True):
|
||||
self.error=error
|
||||
self.icon=icon
|
||||
self.showerror=showerror
|
||||
|
||||
def __str__(self):
|
||||
return self.error
|
||||
@@ -580,14 +581,34 @@ class UserPassDialog(QDialog):
|
||||
self.status=False
|
||||
self.hide()
|
||||
|
||||
class LoopProgressDialog(QProgressDialog):
|
||||
def LoopProgressDialog(gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
finish_function,
|
||||
init_label=_("Fetching metadata for stories..."),
|
||||
win_title=_("Downloading metadata for stories"),
|
||||
status_prefix=_("Fetched metadata for")):
|
||||
ld = _LoopProgressDialog(gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
init_label,
|
||||
win_title,
|
||||
status_prefix)
|
||||
|
||||
# Mac OS X gets upset if the finish_function is called from inside
|
||||
# the real _LoopProgressDialog class.
|
||||
|
||||
# reflect old behavior.
|
||||
if not ld.wasCanceled():
|
||||
finish_function(book_list)
|
||||
|
||||
class _LoopProgressDialog(QProgressDialog):
|
||||
'''
|
||||
ProgressDialog displayed while fetching metadata for each story.
|
||||
'''
|
||||
def __init__(self, gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
finish_function,
|
||||
init_label=_("Fetching metadata for stories..."),
|
||||
win_title=_("Downloading metadata for stories"),
|
||||
status_prefix=_("Fetched metadata for")):
|
||||
@@ -598,10 +619,10 @@ class LoopProgressDialog(QProgressDialog):
|
||||
self.setMinimumWidth(500)
|
||||
self.book_list = book_list
|
||||
self.foreach_function = foreach_function
|
||||
self.finish_function = finish_function
|
||||
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
|
||||
@@ -628,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:
|
||||
@@ -638,15 +665,16 @@ class LoopProgressDialog(QProgressDialog):
|
||||
self.foreach_function(book)
|
||||
|
||||
except NotGoingToDownload as d:
|
||||
book['status']=_('Skipped')
|
||||
book['good']=False
|
||||
book['showerror']=d.showerror
|
||||
book['comment']=unicode(d)
|
||||
book['icon'] = d.icon
|
||||
|
||||
except Exception as e:
|
||||
book['good']=False
|
||||
book['comment']=unicode(e)
|
||||
logger.error("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
logger.error("Exception: %s:%s"%(book,unicode(e)),exc_info=True)
|
||||
|
||||
self.updateStatus()
|
||||
self.i += 1
|
||||
@@ -658,8 +686,6 @@ class LoopProgressDialog(QProgressDialog):
|
||||
|
||||
def do_when_finished(self):
|
||||
self.hide()
|
||||
# Queues a job to process these books in the background.
|
||||
self.finish_function(self.book_list)
|
||||
|
||||
def time_duration_format(seconds):
|
||||
"""
|
||||
@@ -1431,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))
|
||||
@@ -1441,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>'
|
||||
|
||||
@@ -1451,7 +1486,7 @@ class ViewLog(SizePersistedDialog):
|
||||
# self.tb.setHtml(html)
|
||||
# l.addWidget(self.tb)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
|
||||
|
||||
+212
-83
@@ -7,6 +7,22 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# import cProfile
|
||||
|
||||
# def do_cprofile(func):
|
||||
# def profiled_func(*args, **kwargs):
|
||||
# profile = cProfile.Profile()
|
||||
# try:
|
||||
# profile.enable()
|
||||
# result = func(*args, **kwargs)
|
||||
# profile.disable()
|
||||
# return result
|
||||
# finally:
|
||||
# profile.print_stats()
|
||||
# return profiled_func
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,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)
|
||||
@@ -67,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,
|
||||
@@ -100,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):
|
||||
@@ -461,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()
|
||||
|
||||
@@ -481,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 = ""
|
||||
@@ -518,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:
|
||||
@@ -590,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)
|
||||
@@ -616,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)
|
||||
@@ -640,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']:
|
||||
@@ -771,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)
|
||||
@@ -782,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.
|
||||
@@ -942,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,
|
||||
@@ -994,7 +1026,8 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
show_copy_button=False):
|
||||
rejecturllist.remove(url)
|
||||
return False
|
||||
|
||||
|
||||
# @do_cprofile
|
||||
def prep_download_loop(self,book,
|
||||
options={'fileform':'epub',
|
||||
'collision':ADDNEW,
|
||||
@@ -1015,6 +1048,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
## 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.
|
||||
|
||||
if self.reject_url(merge,book):
|
||||
return
|
||||
|
||||
@@ -1054,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']
|
||||
@@ -1063,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)
|
||||
@@ -1105,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.
|
||||
@@ -1130,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>
|
||||
@@ -1140,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')]
|
||||
@@ -1149,7 +1184,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Skipped')
|
||||
return
|
||||
|
||||
|
||||
################################################################################################################################################33
|
||||
|
||||
book['is_adult'] = adapter.is_adult
|
||||
@@ -1158,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:
|
||||
@@ -1169,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")
|
||||
@@ -1180,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'):
|
||||
@@ -1189,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'
|
||||
@@ -1300,7 +1335,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
urlchaptercount = int(story.getMetadata('numChapters').replace(',',''))
|
||||
if chaptercount == urlchaptercount:
|
||||
if collision == UPDATE:
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png')
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png',showerror=False)
|
||||
elif chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
elif chaptercount == 0:
|
||||
@@ -1311,17 +1346,17 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
logger.debug("OVERWRITE file: "+db.format_abspath(book_id, formmapping[fileform], index_is_id=True))
|
||||
fileupdated=datetime.fromtimestamp(os.stat(db.format_abspath(book_id, formmapping[fileform], index_is_id=True))[8])
|
||||
logger.debug("OVERWRITE file updated: %s"%fileupdated)
|
||||
book['updated']=fileupdated
|
||||
book['fileupdated']=fileupdated
|
||||
if not bgmeta:
|
||||
# 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 \
|
||||
(lastupdated.time() != time.min and fileupdated > lastupdated):
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png')
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png',showerror=False)
|
||||
|
||||
# For update, provide a tmp file copy of the existing epub so
|
||||
# it can't change underneath us. Now also overwrite for logpage preserve.
|
||||
@@ -1359,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']:
|
||||
@@ -1387,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.
|
||||
@@ -1454,10 +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 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',
|
||||
@@ -1494,7 +1539,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if book['calibre_id'] and prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
|
||||
label = custom_columns[prefs['errorcol']]['label']
|
||||
if not book['good']:
|
||||
if not book['good'] and (book['showerror'] or prefs['save_all_errors']):
|
||||
logger.debug("record/update error message column %s %s"%(book['title'],book['url']))
|
||||
self.set_custom(db, book['calibre_id'], 'comment', book['comment'], label=label, commit=True) # book['comment']
|
||||
else:
|
||||
@@ -1677,26 +1722,35 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Good'
|
||||
status = _('Good')
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([escapehtml(status),escapehtml(book['title']),escapehtml(", ".join(book['author'])),escapehtml(book['comment']),book['url']]) + '</td></tr>'
|
||||
|
||||
for book in bad_list:
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Bad'
|
||||
status = _('Bad')
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([escapehtml(status),escapehtml(book['title']),escapehtml(", ".join(book['author'])),escapehtml(book['comment']),book['url']]) + '</td></tr>'
|
||||
|
||||
htmllog = htmllog + '</table></body></html>'
|
||||
|
||||
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 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)
|
||||
@@ -1722,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)
|
||||
@@ -1777,7 +1874,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
status_prefix=_("Updated"))
|
||||
|
||||
def update_error_column_loop(self,book,db=None,label=None):
|
||||
if book['calibre_id'] and label:
|
||||
if book['calibre_id'] and label and (book['showerror'] or prefs['save_all_errors']):
|
||||
logger.debug("add/update bad %s %s %s"%(book['title'],book['url'],book['comment']))
|
||||
self.set_custom(db, book['calibre_id'], 'comment', book['comment'], label=label, commit=True)
|
||||
|
||||
@@ -1854,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:
|
||||
@@ -1921,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.
|
||||
@@ -1950,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:
|
||||
@@ -2024,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:
|
||||
@@ -2037,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
|
||||
@@ -2058,25 +2166,25 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# (template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
for (template,regexp,setting) in configuration.get_generate_cover_settings():
|
||||
value = Template(template).safe_substitute(book['all_metadata']).encode('utf8')
|
||||
print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
# print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Generate Cover Setting from generate_cover_settings(%s)"%setting_name)
|
||||
if setting_name not in gc_plugin.get_saved_setting_names():
|
||||
logger.info("GC Name %s not found, discarding! (check personal.ini for typos)"%setting_name)
|
||||
setting_name = None
|
||||
|
||||
|
||||
if not setting_name and book['all_metadata']['site'] in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings'][book['all_metadata']['site']]
|
||||
logger.debug("Generate Cover Setting from site(%s)"%setting_name)
|
||||
|
||||
|
||||
if not setting_name and 'Default' in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings']['Default']
|
||||
logger.debug("Generate Cover Setting from Default(%s)"%setting_name)
|
||||
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Running Generate Cover with settings %s."%setting_name)
|
||||
## fetch updated mi object from
|
||||
@@ -2085,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),
|
||||
@@ -2103,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']],
|
||||
@@ -2194,6 +2302,10 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['comments'] = '' # note this is the book comments.
|
||||
|
||||
book['good'] = True
|
||||
book['showerror'] = True # False when NotGoingToDownload is
|
||||
# not-overwrite / not-update / skip
|
||||
# -- what some would consider 'not an
|
||||
# error'
|
||||
book['calibre_id'] = None
|
||||
book['begin'] = None
|
||||
book['end'] = None
|
||||
@@ -2213,7 +2325,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book = self.convert_url_to_book(url)
|
||||
if book['url'] in uniqueurls:
|
||||
book['good'] = False
|
||||
book['comment'] = "Same story already included."
|
||||
book['comment'] = _("Same story already included.")
|
||||
uniqueurls.add(book['url'])
|
||||
book['listorder']=i # BG d/l jobs don't come back in order.
|
||||
# Didn't matter until anthologies & 'marked' successes
|
||||
@@ -2359,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.
|
||||
@@ -2417,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
|
||||
@@ -2465,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()
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
@@ -68,6 +68,10 @@ class IniHighlighter(QSyntaxHighlighter):
|
||||
self.teststoryRule = HighlightingRule( r"^\[teststory:([0-9]+|defaults)\]", Qt.darkCyan, blocknum=3 )
|
||||
self.highlightingRules.append( self.teststoryRule )
|
||||
|
||||
# storyUrl sections
|
||||
self.storyUrlRule = HighlightingRule( r"^\[https?://.*\]", Qt.darkMagenta, blocknum=4 )
|
||||
self.highlightingRules.append( self.storyUrlRule )
|
||||
|
||||
# NOT comments -- but can be custom columns, so don't flag.
|
||||
#self.highlightingRules.append( HighlightingRule( r"(?<!^)#[^\n]*" , Qt.red ) )
|
||||
|
||||
@@ -96,6 +100,10 @@ class IniHighlighter(QSyntaxHighlighter):
|
||||
if blocknum == 3:
|
||||
self.setFormat( 0, len(text), self.teststoryRule.highlight )
|
||||
|
||||
# storyUrl section rules:
|
||||
if blocknum == 4:
|
||||
self.setFormat( 0, len(text), self.storyUrlRule.highlight )
|
||||
|
||||
self.setCurrentBlockState( blocknum )
|
||||
|
||||
class HighlightingRule():
|
||||
|
||||
+15
-9
@@ -23,6 +23,12 @@ from calibre.library.comments import sanitize_comments_html
|
||||
from calibre_plugins.fanficfare_plugin.wordcount import get_word_count
|
||||
from calibre_plugins.fanficfare_plugin.prefs import (SAVE_YES, SAVE_YES_UNLESS_SITE)
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
load_translations()
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# Functions to perform downloads using worker jobs
|
||||
@@ -82,7 +88,7 @@ def do_download_worker(book_list,
|
||||
book_list.append(job.result)
|
||||
book_id = job._book['calibre_id']
|
||||
count = count + 1
|
||||
notification(float(count)/total, '%d of %d stories finished downloading'%(count,total))
|
||||
notification(float(count)/total, _('%d of %d stories finished downloading')%(count,total))
|
||||
# Add this job's output to the current log
|
||||
logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
|
||||
logger.info(job.details)
|
||||
@@ -155,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.
|
||||
@@ -189,7 +195,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
## No need to download at all. Shouldn't ever get down here.
|
||||
if options['collision'] in (CALIBREONLY, CALIBREONLYSAVECOL):
|
||||
logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
|
||||
book['comment'] = 'Metadata collected.'
|
||||
book['comment'] = _('Metadata collected.')
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
@@ -214,14 +220,14 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
# updated does have time, use full timestamps.
|
||||
if (lastupdated.time() == time.min and fileupdated.date() > lastupdated.date()) or \
|
||||
(lastupdated.time() != time.min and fileupdated > lastupdated):
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png')
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png',showerror=False)
|
||||
|
||||
|
||||
logger.info("write to %s"%outfile)
|
||||
inject_cal_cols(book,story,configuration)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
|
||||
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
|
||||
book['comment'] = _('Download %s completed, %s chapters.')%(options['fileform'],story.getMetadata("numChapters"))
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
@@ -253,7 +259,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
book['outfile'] = book['epub_for_update'] # for anthology merge ops.
|
||||
return book
|
||||
else: # not merge,
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png')
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png',showerror=False)
|
||||
elif chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
elif chaptercount == 0:
|
||||
@@ -304,6 +310,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
|
||||
except NotGoingToDownload as d:
|
||||
book['good']=False
|
||||
book['showerror']=d.showerror
|
||||
book['comment']=unicode(d)
|
||||
book['icon'] = d.icon
|
||||
|
||||
@@ -311,9 +318,8 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
book['good']=False
|
||||
book['comment']=unicode(e)
|
||||
book['icon']='dialog_error.png'
|
||||
book['status'] = 'Error'
|
||||
logger.info("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
book['status'] = _('Error')
|
||||
logger.info("Exception: %s:%s"%(book,unicode(e)),exc_info=True)
|
||||
|
||||
#time.sleep(10)
|
||||
return book
|
||||
|
||||
+291
-107
@@ -1,4 +1,4 @@
|
||||
# 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.
|
||||
@@ -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
|
||||
@@ -474,8 +474,8 @@ add_to_include_subject_tags:,tagsfromtitle.SPLIT,forumtags
|
||||
## base_xenforoforum reads Published and Updated datetimes from
|
||||
## Threadmarks if used, or from the posted & updated times of the
|
||||
## 'first' post if no threadmarks.
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M
|
||||
|
||||
## Only take the first X characters of the 'first' post to use as
|
||||
## the description.
|
||||
@@ -488,6 +488,47 @@ description_limit:500
|
||||
## in the ebook for that chapter.
|
||||
continue_on_chapter_error:false
|
||||
|
||||
## When given a thread URL, use threadmarks as chapter links when
|
||||
## there are at least this many threadmarks. A number of older
|
||||
## threads have a single threadmark to an 'index' post. Set to 1 to
|
||||
## use threadmarks whenever they exist.
|
||||
minimum_threadmarks:2
|
||||
|
||||
## When 'first post' (or post URL) is being added as a chapter, give
|
||||
## the chapter this title.
|
||||
first_post_title:First Post
|
||||
|
||||
## In normal operation, if given a post URL or a thread URL with less
|
||||
## than minimum_threadmarks, the given post or the first post of the
|
||||
## thread will be included as the first chapter (with chapter title
|
||||
## from first_post_title) unless that post is explicitly linked to in
|
||||
## the collected chapter list. First post is not included when using
|
||||
## thread marks.
|
||||
##
|
||||
## If always_include_first_post:true, then the given or first post
|
||||
## will be included as above even if it is a link in the post or even
|
||||
## 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.
|
||||
always_use_forumtags:false
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
|
||||
@@ -558,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
|
||||
@@ -721,6 +767,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]
|
||||
@@ -820,18 +879,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
|
||||
@@ -842,6 +889,12 @@ extracategories:The Sentinel
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:Windows-1252,ISO-8859-1,auto
|
||||
|
||||
## dateUpdate doesn't usually have time, but it does on
|
||||
## bloodshedverse.com. See
|
||||
## http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
@@ -871,15 +924,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
|
||||
@@ -996,15 +1040,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
|
||||
@@ -1124,13 +1177,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
|
||||
@@ -1188,7 +1248,7 @@ views_label:Views
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
|
||||
[finestories.com]
|
||||
[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
|
||||
@@ -1196,11 +1256,56 @@ dislikes_label:Dislikes
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
# shows size as "10 KB", not word count
|
||||
extra_valid_entries:size
|
||||
[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
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
## finestories.com has started requiring login by email rather than
|
||||
## pen name.
|
||||
#username:youremail@yourdomain.dom
|
||||
#password:yourpassword
|
||||
|
||||
# don't show twitter icon.
|
||||
cover_exclusion_regexp:/res/css/bir.png
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,sitetags,notice,codes,score
|
||||
#extra_titlepage_entries:size,universeHTML,sitetags,notice,score
|
||||
include_in_codes:sitetags
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:sitetags
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
sitetags_label:Site Tags
|
||||
notice_label:Notice
|
||||
score_label:Score
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[forum.questionablequesting.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
## 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
|
||||
|
||||
[forums.spacebattles.com]
|
||||
## see [base_xenforoforum]
|
||||
@@ -1208,7 +1313,12 @@ cover_exclusion_regexp:/res/css/bir.png
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[grangerenchanted.com]
|
||||
[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
|
||||
|
||||
## 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
|
||||
@@ -1216,19 +1326,6 @@ cover_exclusion_regexp:/res/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.
|
||||
#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
|
||||
|
||||
[hlfiction.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Highlander
|
||||
@@ -1280,9 +1377,21 @@ crossoverfandom_label:Crossover Fandom
|
||||
extra_titlepage_entries:universe,crossoverfandom
|
||||
|
||||
[literotica.com]
|
||||
extra_valid_entries:eroticatags
|
||||
extra_valid_entries:eroticatags,averrating
|
||||
eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
averrating_label:Average Rating
|
||||
extra_titlepage_entries:eroticatags,averrating
|
||||
|
||||
## Extract more erotica_tags from the meta tag of each chapter
|
||||
use_meta_keywords: true
|
||||
|
||||
## For multiple chapter stories, attempt to clean up the chapter title. This will
|
||||
## remove the story title and change "Ch. 01" to "Chapter 1", "Pt. 01" to "Part 1"
|
||||
## or just use the text. If this can't be done, the full title is used.
|
||||
clean_chapter_titles: false
|
||||
|
||||
## Add the chapter description at the start of each chapter.
|
||||
description_in_chapter: false
|
||||
|
||||
[lotrfanfiction.com]
|
||||
extra_valid_entries: readings
|
||||
@@ -1329,6 +1438,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
|
||||
@@ -1430,15 +1552,16 @@ 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
|
||||
|
||||
[samandjack.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
@@ -1464,22 +1587,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
|
||||
@@ -1621,15 +1728,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
|
||||
@@ -1697,6 +1795,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,
|
||||
@@ -1791,6 +1895,10 @@ check_next_chapter:false
|
||||
#password:yourpassword
|
||||
|
||||
[www.ficbook.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.fictionalley.org]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
@@ -1909,6 +2017,9 @@ extracategories:Harry Potter
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
extra_valid_entries:reads,reviews
|
||||
reads_label:Total Read Count
|
||||
|
||||
[www.hpfanficarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -1990,11 +2101,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
|
||||
@@ -2049,6 +2155,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
|
||||
@@ -2125,23 +2233,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.
|
||||
@@ -2176,6 +2270,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
|
||||
@@ -2227,6 +2337,80 @@ extracategories:Stargate: Atlantis
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
[buffygiles.velocitygrass.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
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Buffy,Giles
|
||||
|
||||
[fanfiction.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
|
||||
|
||||
## 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.andromeda-web.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
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Andromeda
|
||||
|
||||
[www.artemis-fowl.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
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Artemis Fowl
|
||||
|
||||
[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,
|
||||
## 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
|
||||
|
||||
[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
|
||||
@@ -158,6 +159,7 @@ default_prefs['countpagesstats'] = []
|
||||
default_prefs['wordcountmissing'] = False
|
||||
|
||||
default_prefs['errorcol'] = ''
|
||||
default_prefs['save_all_errors'] = True
|
||||
default_prefs['savemetacol'] = ''
|
||||
default_prefs['custom_cols'] = {}
|
||||
default_prefs['custom_cols_newonly'] = {}
|
||||
@@ -165,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'] = ''
|
||||
@@ -173,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):
|
||||
|
||||
+584
-494
File diff suppressed because it is too large
Load Diff
+573
-482
File diff suppressed because it is too large
Load Diff
+718
-630
File diff suppressed because it is too large
Load Diff
+572
-481
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+568
-479
File diff suppressed because it is too large
Load Diff
+579
-489
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+641
-552
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+585
-496
File diff suppressed because it is too large
Load Diff
+720
-446
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
|
||||
@@ -27,8 +27,7 @@ except:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFF:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
loghandler.setFormatter(logging.Formatter("FFF: %(levelname)s: %(asctime)s: %(filename)s(%(lineno)d): %(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -85,7 +85,6 @@ import adapter_hpfanficarchivecom
|
||||
import adapter_twilightarchivescom
|
||||
import adapter_nhamagicalworldsus
|
||||
import adapter_hlfictionnet
|
||||
import adapter_grangerenchantedcom
|
||||
import adapter_dracoandginnycom
|
||||
import adapter_scarvesandcoffeenet
|
||||
import adapter_thepetulantpoetesscom
|
||||
@@ -102,13 +101,9 @@ import adapter_efictionestelielde
|
||||
import adapter_pommedesangcom
|
||||
import adapter_restrictedsectionorg
|
||||
import adapter_imagineeficcom
|
||||
import adapter_buffynfaithnet
|
||||
import adapter_psychficcom
|
||||
import adapter_tokrafandomnetcom
|
||||
import adapter_asr3slashzoneorg
|
||||
import adapter_nickandgregnet
|
||||
import adapter_potterheadsanonymouscom
|
||||
import adapter_scarheadnet
|
||||
import adapter_fictionpadcom
|
||||
import adapter_storiesonlinenet
|
||||
import adapter_trekiverseorg
|
||||
@@ -120,7 +115,6 @@ import adapter_nocturnallightnet
|
||||
import adapter_fanfichu
|
||||
import adapter_fanfictioncsodaidokhu
|
||||
import adapter_fictionmaniatv
|
||||
import adapter_bdsmgeschichten
|
||||
import adapter_tolkienfanfiction
|
||||
import adapter_themaplebookshelf
|
||||
import adapter_fannation
|
||||
@@ -139,6 +133,17 @@ import adapter_ninelivesarchivecom
|
||||
import adapter_masseffect2in
|
||||
import adapter_quotevcom
|
||||
import adapter_mcstoriescom
|
||||
import adapter_lucifaelff
|
||||
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
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
@@ -169,8 +174,9 @@ def getNormalStoryURL(url):
|
||||
return None
|
||||
|
||||
def getNormalStoryURLSite(url):
|
||||
# print("getNormalStoryURLSite:%s"%url)
|
||||
if not getNormalStoryURL.__dummyconfig:
|
||||
getNormalStoryURL.__dummyconfig = Configuration("test1.com","EPUB")
|
||||
getNormalStoryURL.__dummyconfig = Configuration(["test1.com"],"EPUB",lightweight=True)
|
||||
# pulling up an adapter is pretty low over-head. If
|
||||
# it fails, it's a bad url.
|
||||
try:
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- 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.
|
||||
#
|
||||
# ####### Not all lables are captured. they are not formtted correctly on the
|
||||
# ####### webpage.
|
||||
|
||||
# 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 AndromedaWebComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class AndromedaWebComAdapter(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','awc') # 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 'www.andromeda-web.com' # 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() + '/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=2"
|
||||
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=2
|
||||
#print data
|
||||
m = re.search(r"'fiction/viewstory.php\?sid=10(&warning=2)'",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)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'content'})
|
||||
|
||||
## 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', {'class' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -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'})
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- 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.
|
||||
#
|
||||
# ####### Not all lables are captured. they are not formtted correctly on the
|
||||
# ####### webpage.
|
||||
|
||||
# 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 ArtemisFowlComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class ArtemisFowlComAdapter(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() + '/fanfiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','afcff') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%y" # XXX
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.artemis-fowl.com' # XXX
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fanfiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/fanfiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
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=5"
|
||||
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.
|
||||
|
||||
# fanfiction/viewstory.php?sid=1882&warning=4
|
||||
# fanfiction/viewstory.php?sid=1654&ageconsent=ok&warning=2
|
||||
#print data
|
||||
m = re.search(r"'fanfiction/viewstory.php\?sid=10(&warning=5)'",data)
|
||||
m = re.search(r"'fanfiction/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)
|
||||
# print 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+'/fanfiction/'+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"fanfiction/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'^fanfiction/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('fanfiction/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)
|
||||
@@ -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
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 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.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
import time
|
||||
|
||||
from bs4.element import Tag, Comment
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def _translate_date_german_english(date):
|
||||
fullmon = {"Januar":"01",
|
||||
"Februar":"02",
|
||||
u"März":"03",
|
||||
"April":"04",
|
||||
"Mai":"05",
|
||||
"Juni":"06",
|
||||
"Juli":"07",
|
||||
"August":"08",
|
||||
"September":"09",
|
||||
"Oktober":"10",
|
||||
"November":"11",
|
||||
"Dezember":"12"}
|
||||
for (name,num) in fullmon.items():
|
||||
date = date.replace(name,num)
|
||||
return date
|
||||
|
||||
_REGEX_TRAILING_DIGIT = re.compile("(\d+)$")
|
||||
_REGEX_DASH_TO_END = re.compile("-[^-]+$")
|
||||
_REGEX_CHAPTER_TITLE = re.compile(ur"""
|
||||
\s*
|
||||
[\u2013-]?
|
||||
\s*
|
||||
([\dIVX-]+)?
|
||||
\.?
|
||||
\s*
|
||||
[\[\(]?
|
||||
\s*
|
||||
(Teil|Kapitel|Tag)?
|
||||
\s*
|
||||
([\dIVX-]+)?
|
||||
\s*
|
||||
[\]\)]?
|
||||
\s*
|
||||
$
|
||||
""", re.VERBOSE)
|
||||
_INITIAL_STEP = 5
|
||||
|
||||
class BdsmGeschichtenAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8", "Windows-1252"]
|
||||
|
||||
self.story.setMetadata('siteabbrev','bdsmgesch')
|
||||
|
||||
# Replace possible chapter numbering
|
||||
chapterMatch = _REGEX_TRAILING_DIGIT.search(url)
|
||||
if chapterMatch is None:
|
||||
self.maxChapter = 1
|
||||
else:
|
||||
self.maxChapter = int(chapterMatch.group(1))
|
||||
# url = re.sub(_REGEX_TRAILING_DIGIT, "1", url)
|
||||
|
||||
# set storyId
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
|
||||
# normalize URL
|
||||
self._setURL('http://%s/%s' % (self.getSiteDomain(), self.story.getMetadata('storyId')))
|
||||
|
||||
self.dateformat = '%d. %m %Y - %H:%M'
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'bdsm-geschichten.net'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.bdsm-geschichten.net', 'www.bdsm-geschichten.net']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://www.bdsm-geschichten.net/title-of-story-1 http://bdsm-geschichten.net/title-of-story-1"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www\.)?bdsm-geschichten.net/(?P<storyId>[a-zA-Z0-9_-]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
try:
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup = self.make_soup(data1)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
|
||||
# Cache the soups so we won't have to redownload in getChapterText later
|
||||
self.soupsCache = {}
|
||||
self.soupsCache[self.url] = soup
|
||||
|
||||
# author
|
||||
authorDiv = soup.find("div", "author-pane-line author-name")
|
||||
authorId = authorDiv.string.strip()
|
||||
self.story.setMetadata('authorId', authorId)
|
||||
self.story.setMetadata('author', authorId)
|
||||
# TODO not really true need to be loggedin for this to work or fetch userid
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+authorId)
|
||||
|
||||
# TODO better metadata
|
||||
date = soup.find("div", {"class": "submitted"}).string.strip()
|
||||
# 11. April 2015 - 17:08
|
||||
date = re.sub(r"(\d+\. \D+ \d+ - \d+:\d+).*", r"\1", date)
|
||||
date = _translate_date_german_english(date)
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
title1 = soup.find("h1", {'class': 'title'}).string
|
||||
|
||||
|
||||
for tagLink in soup.find("ul", "taxonomy").findAll("a"):
|
||||
self.story.addToList('category', tagLink.string)
|
||||
|
||||
## Retrieve chapter soups
|
||||
if self.getConfig('find_chapters') == 'guess':
|
||||
self.chapterUrls = []
|
||||
self._find_chapters_by_guessing(title1)
|
||||
else:
|
||||
self._find_chapters_by_parsing(soup)
|
||||
|
||||
firstChapterUrl = self.chapterUrls[0][1]
|
||||
if firstChapterUrl in self.soupsCache:
|
||||
firstChapterSoup = self.soupsCache[firstChapterUrl]
|
||||
h1 = firstChapterSoup.find("h1").text
|
||||
else:
|
||||
h1 = soup.find("h1").text
|
||||
|
||||
h1 = re.sub(_REGEX_CHAPTER_TITLE, "", h1)
|
||||
self.story.setMetadata('title', h1)
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
return
|
||||
|
||||
def _find_chapters_by_parsing(self, soup):
|
||||
|
||||
# store original soup
|
||||
origSoup = soup
|
||||
|
||||
#
|
||||
# find first chapter
|
||||
#
|
||||
firstLink = None
|
||||
firstLinkDiv = soup.find("div", "field-field-erster-teil")
|
||||
if firstLinkDiv is not None:
|
||||
firstLink = "http://%s%s" % (self.getSiteDomain(), firstLinkDiv.findNext("a")['href'])
|
||||
logger.debug("Found first chapter right away <%s>" % firstLink)
|
||||
try:
|
||||
soup = self.make_soup(self._fetchUrl(firstLink))
|
||||
self.soupsCache[firstLink] = soup
|
||||
self.chapterUrls.insert(0, (soup.find("h1").text, firstLink))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise exceptions.StoryDoesNotExist(firstLink)
|
||||
else:
|
||||
logger.debug("DIDN'T find first chapter right away")
|
||||
# parse previous Link until first
|
||||
while True:
|
||||
prevLink = None
|
||||
prevLinkDiv = soup.find("div", "field-field-vorheriger-teil")
|
||||
if prevLinkDiv is not None:
|
||||
prevLink = prevLinkDiv.find("a")
|
||||
if prevLink is None:
|
||||
prevLink = soup.find("a", text=re.compile("<<<")) # <<<
|
||||
if prevLink is None:
|
||||
logger.debug("Couldn't find prev part")
|
||||
break
|
||||
else:
|
||||
logger.debug("Previous Chapter <%s>" % prevLink)
|
||||
if type(prevLink) != Tag or prevLink.name != "a":
|
||||
prevLink = prevLink.findParent("a")
|
||||
if prevLink is None or '#' in prevLink['href']:
|
||||
logger.debug("Couldn't find prev part (false positive) <%s>" % prevLink)
|
||||
break
|
||||
prevLink = prevLink['href']
|
||||
try:
|
||||
soup = self.make_soup(self._fetchUrl(prevLink))
|
||||
self.soupsCache[prevLink] = soup
|
||||
prevTtitle = soup.find("h1", {'class': 'title'}).string
|
||||
self.chapterUrls.insert(0, (prevTtitle, prevLink))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(nextLink)
|
||||
else:
|
||||
raise e
|
||||
firstLink = prevLink
|
||||
|
||||
# if first chapter couldn't be determined, assume the URL originally
|
||||
# passed is the first chapter
|
||||
if firstLink is None:
|
||||
logger.debug("Couldn't set first chapter")
|
||||
firstLink = self.url
|
||||
self.chapterUrls.insert(0, (soup.find("h1").text, firstLink))
|
||||
|
||||
# set first URL
|
||||
logger.debug("Set first link: %s" % firstLink)
|
||||
self._setURL(firstLink)
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(firstLink).group('storyId'))
|
||||
|
||||
#
|
||||
# Parse next chapters
|
||||
#
|
||||
while True:
|
||||
nextLink = None
|
||||
nextLinkDiv = soup.find("div", "field-field-naechster-teil")
|
||||
if nextLinkDiv is not None:
|
||||
nextLink = nextLinkDiv.find("a")
|
||||
if nextLink is None:
|
||||
nextLink = soup.find("a", text=re.compile(">>>"))
|
||||
if nextLink is None:
|
||||
nextLink = soup.find("a", text=re.compile("Fortsetzung"))
|
||||
|
||||
if nextLink is None:
|
||||
logger.debug("Couldn't find next part")
|
||||
break
|
||||
else:
|
||||
if type(nextLink) != Tag or nextLink.name != "a":
|
||||
nextLink = nextLink.findParent("a")
|
||||
if nextLink is None or '#' in nextLink['href']:
|
||||
logger.debug("Couldn't find next part (false positive) <%s>" % nextLink)
|
||||
break
|
||||
nextLink = nextLink['href']
|
||||
|
||||
if not nextLink.startswith('http:'):
|
||||
nextLink = 'http://' + self.getSiteDomain() + nextLink
|
||||
|
||||
for loadedChapter in self.chapterUrls:
|
||||
if loadedChapter[0] == nextLink:
|
||||
logger.debug("ERROR: Repeating chapter <%s> Try to fix it" % nextLink)
|
||||
nextLinkMatch = _REGEX_TRAILING_DIGIT.match(nextLink)
|
||||
if nextLinkMatch is not None:
|
||||
curChap = nextLinkMatch.group(1)
|
||||
nextLink = re.sub(_REGEX_TRAILING_DIGIT, unicode(int(curChap) + 1), nextLink)
|
||||
else:
|
||||
break
|
||||
try:
|
||||
data = self._fetchUrl(nextLink)
|
||||
soup = self.make_soup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(nextLink)
|
||||
else:
|
||||
raise e
|
||||
title2 = soup.find("h1", {'class': 'title'}).string
|
||||
self.chapterUrls.append((title2, nextLink))
|
||||
logger.debug("Grabbing next chapter URL " + nextLink)
|
||||
self.soupsCache[nextLink] = soup
|
||||
# [comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
logger.debug("Chapters: %s" % self.chapterUrls)
|
||||
|
||||
|
||||
def _find_chapters_by_guessing(self, title1):
|
||||
step = _INITIAL_STEP
|
||||
curMax = self.maxChapter + step
|
||||
lastHit = True
|
||||
while True:
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, unicode(curMax), self.url)
|
||||
if nextChapterUrl == self.url:
|
||||
logger.debug("Unable to guess next chapter because URL doesn't end in numbers")
|
||||
break;
|
||||
try:
|
||||
logger.debug("Trying chapter URL " + nextChapterUrl)
|
||||
data = self._fetchUrl(nextChapterUrl)
|
||||
hit = True
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
hit = False
|
||||
else:
|
||||
raise e
|
||||
if hit:
|
||||
logger.debug("Found chapter URL " + nextChapterUrl)
|
||||
self.maxChapter = curMax
|
||||
self.soupsCache[nextChapterUrl] = self.make_soup(data)
|
||||
if not lastHit:
|
||||
break
|
||||
lastHit = curMax
|
||||
curMax += step
|
||||
else:
|
||||
lastHit = False
|
||||
curMax -= 1
|
||||
logger.debug(curMax)
|
||||
|
||||
for i in xrange(1, self.maxChapter):
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, unicode(i), self.url)
|
||||
nextChapterTitle = re.sub("1", unicode(i), title1)
|
||||
self.chapterUrls.append((nextChapterTitle, nextChapterUrl))
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
if url in self.soupsCache:
|
||||
logger.debug('Getting chapter <%s> from cache' % url)
|
||||
soup = self.soupsCache[url]
|
||||
else:
|
||||
logger.debug('Downloading chapter <%s>' % url)
|
||||
data1 = self._fetchUrl(url)
|
||||
soup = self.make_soup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
|
||||
# get story text
|
||||
storyDiv1 = soup.new_tag("div")
|
||||
for para in soup.find("div", "full-node").find('div', 'content').findAll("p"):
|
||||
storyDiv1.append(para)
|
||||
storyDiv1.append(soup.new_tag("br"))
|
||||
storytext = self.utf8FromSoup(url,storyDiv1)
|
||||
|
||||
return storytext
|
||||
|
||||
|
||||
def getClass():
|
||||
return BdsmGeschichtenAdapter
|
||||
@@ -28,7 +28,7 @@ class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
READ_URL_TEMPLATE = BASE_URL + 'stories.php?go=read&no=%s'
|
||||
|
||||
STARTED_DATETIME_FORMAT = '%m/%d/%Y'
|
||||
UPDATED_DATETIME_FORMAT = '%m/%d/%Y %I:%M'
|
||||
UPDATED_DATETIME_FORMAT = '%m/%d/%Y %I:%M %p'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -168,14 +168,8 @@ class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.STARTED_DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Updated':
|
||||
date_string, period = value.rsplit(' ', 1)
|
||||
date = makeDate(date_string, self.UPDATED_DATETIME_FORMAT)
|
||||
|
||||
# Rather ugly hack to work around Calibre's changing of
|
||||
# Python's locale setting, causing am/pm to not be properly
|
||||
# parsed by strptime() when using a non-english locale
|
||||
if period == 'pm':
|
||||
date += timedelta(hours=12)
|
||||
date = makeDate(value, self.UPDATED_DATETIME_FORMAT)
|
||||
# ugly %p(am/pm) hack moved into makeDate so other sites can use it.
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
if self.story.getMetadata('rating') == 'NC-17' and not (self.is_adult or self.getConfig('is_adult')):
|
||||
|
||||
+82
-81
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 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.
|
||||
@@ -29,11 +29,11 @@ from .. import exceptions as exceptions
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return ScarHeadNetAdapter
|
||||
return BuffyGilesComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
class BuffyGilesComAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -52,10 +52,11 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
# XXX Most sites don't have the /efiction part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/efiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','shn')
|
||||
self.story.setMetadata('siteabbrev','bufg')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
@@ -64,14 +65,14 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'scarhead.net'
|
||||
return 'buffygiles.velocitygrass.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
return "http://"+cls.getSiteDomain()+"/efiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
return re.escape("http://"+self.getSiteDomain()+"/efiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -116,7 +117,7 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=5"
|
||||
addurl = "&warning=5"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
@@ -143,11 +144,11 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# viewstory.php?sid=1882&warning=4
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
# efiction/viewstory.php?sid=1882&warning=4
|
||||
# efiction/viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
m = re.search(r"'efiction/viewstory.php\?sid=542(&warning=5)'",data)
|
||||
m = re.search(r"'efiction/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
|
||||
@@ -170,7 +171,7 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
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)
|
||||
@@ -178,10 +179,11 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('tr',{'valign':'top'})
|
||||
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.
|
||||
@@ -193,88 +195,87 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
# 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+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/efiction/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
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.
|
||||
|
||||
cats = soup.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
if '/' == cat.string[0]:
|
||||
self.story.addToList('ships','Harry Potter'+cat.string.split('(')[0])
|
||||
elif 'Harry' in cat.string:
|
||||
self.story.addToList('ships',cat.string.split('(')[0])
|
||||
else:
|
||||
self.story.addToList('category',cat.string)
|
||||
if '(' in cat.string:
|
||||
self.story.addToList('category',cat.string.split('(')[1].split(')')[0])
|
||||
|
||||
|
||||
|
||||
|
||||
chars = soup.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
genres = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
warnings = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
textsoup = stripHTML(soup)
|
||||
|
||||
a = textsoup.split('Published: ')[1].split(' ')[0]
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(a), self.dateformat))
|
||||
a = textsoup.split('Updated: ')[1].split(' ')[0]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(a), self.dateformat))
|
||||
a = textsoup.split('Rating: ')[1].split(' ')[0]
|
||||
self.story.setMetadata('rating', a)
|
||||
a = textsoup.split('Length: ')[1].split('(')[1].split(' ')[0]
|
||||
self.story.setMetadata('numWords', a)
|
||||
a = textsoup.split('Completed: ')[1].split(' ')[0]
|
||||
if 'Yes' in a:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
#a = textsoup.split('Summary: ')[1].split('Add Story to Favorites')[0]
|
||||
#self.setDescription(url,a)
|
||||
|
||||
|
||||
|
||||
a=soup.find(text=re.compile("Summary: "))
|
||||
i=0
|
||||
svalue = ""
|
||||
while i == 0:
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
b = unicode(a)
|
||||
svalue += b.split('Summary: ')[1]
|
||||
return d[k]
|
||||
except:
|
||||
svalue += unicode(a)
|
||||
if a.nextSibling != None:
|
||||
a = a.nextSibling
|
||||
else:
|
||||
a = a.parent.nextSibling
|
||||
if 'Disclaimer: ' in stripHTML(a):
|
||||
i=1
|
||||
self.setDescription(url,svalue)
|
||||
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"viewseries.php\?seriesid=\d+"))
|
||||
a = soup.find('a', href=re.compile(r"efiction/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'^viewstory.php\?sid=\d+$'))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^efiction/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
if a['href'] == ('efiction/viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
@@ -283,7 +284,7 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
|
||||
# This function is called by the downloader in all adapter_*.py files
|
||||
# in this dir to register the adapter class. So it needs to be
|
||||
# updated to reflect the class below it. That, plus getSiteDomain()
|
||||
# take care of 'Registering'.
|
||||
def getClass():
|
||||
return BuffyNFaithNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class BuffyNFaithNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.setHeader()
|
||||
|
||||
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 correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
|
||||
# normalized story URL. gets rid of chapter if there, left with ch 1 URL on this site
|
||||
nurl = "http://"+self.getSiteDomain()+"/fanfictions/index.php?act=vie&id="+self.story.getMetadata('storyId')
|
||||
self._setURL(nurl)
|
||||
#argh, this mangles the ampersands I need on metadata['storyUrl']
|
||||
#will set it this way
|
||||
self.story.setMetadata('storyUrl',nurl,condremoveentities=False)
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','bnfnet')
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'buffynfaith.net'
|
||||
|
||||
@classmethod
|
||||
def stripURLParameters(cls,url):
|
||||
"Only needs to be overriden if URL contains more than one parameter"
|
||||
## This adapter needs at least two parameters left on the URL, act and id
|
||||
return re.sub(r"(\?act=(vie|ovr)&id=\d+)&.*$",r"\1",url)
|
||||
|
||||
def setHeader(self):
|
||||
"buffynfaith.net wants a Referer for images. Used both above and below(after cookieproc added)"
|
||||
self.opener.addheaders.append(('Referer', 'http://'+self.getSiteDomain()+'/'))
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fanfictions/index.php?act=vie&id=1234 http://"+cls.getSiteDomain()+"/fanfictions/index.php?act=ovr&id=1234 http://"+cls.getSiteDomain()+"/fanfictions/index.php?act=vie&id=1234&ch=2"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=963
|
||||
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=949
|
||||
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=949&ch=2
|
||||
p = re.escape("http://"+self.getSiteDomain()+"/fanfictions/index.php?act=")+\
|
||||
r"(vie|ovr)&id=(?P<id>\d+)(&ch=(?P<ch>\d+))?$"
|
||||
return p
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
adapters that will work with the page cache need to implement
|
||||
this and change it to True.
|
||||
'''
|
||||
return True
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
dateformat = "%d %B %Y"
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
#set a cookie to get past adult check
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
cookie = cl.Cookie(version=0, name='my_age', value='yes',
|
||||
port=None, port_specified=False,
|
||||
domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False,
|
||||
path='/', path_specified=True,
|
||||
secure=False,
|
||||
expires=time.time()+10000,
|
||||
discard=False,
|
||||
comment=None,
|
||||
comment_url=None,
|
||||
rest={'HttpOnly': None},
|
||||
rfc2109=False)
|
||||
self.get_cookiejar().set_cookie(cookie)
|
||||
self.setHeader()
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
#print data
|
||||
|
||||
if "ADULT CONTENT WARNING" in data:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
# 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.
|
||||
|
||||
#stuff in <head>: description
|
||||
svalue = soup.head.find('meta',attrs={'name':'description'})['content']
|
||||
#self.story.setMetadata('description',svalue)
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
#useful stuff in rest of doc, all contained in this:
|
||||
doc = soup.body.find('div', id='my_wrapper')
|
||||
|
||||
#first the site category (more of a genre to me, meh) and title, in this element:
|
||||
mt = doc.find('div',attrs={'class':'maintitle'})
|
||||
self.story.addToList('genre',mt.findAll('a')[1].string)
|
||||
self.story.setMetadata('title',mt.findAll('a')[1].nextSibling[len(' » '):])
|
||||
del mt
|
||||
|
||||
#the actual category, for me, is 'Buffy: The Vampire Slayer'
|
||||
#self.story.addToList('category','Buffy: The Vampire Slayer')
|
||||
#No need to do it here, it is better to set it in in plugin-defaults.ini and defaults.ini
|
||||
|
||||
#then a block that sits in a table cell like so:
|
||||
#(contains a lot of metadata)
|
||||
mblock = doc.find('td', align='left', width = '70%').contents
|
||||
while len(mblock) > 0:
|
||||
i = mblock.pop(0)
|
||||
if 'Author:' in i.string:
|
||||
#drop empty space
|
||||
mblock.pop(0)
|
||||
#get author link
|
||||
a = mblock.pop(0)
|
||||
authre = re.escape('./index.php?act=bio&id=')+'(?P<authid>\d+)'
|
||||
m = re.match(authre,a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
self.story.setMetadata('authorId',m.group('authid'))
|
||||
authurl = u'http://%s/fanfictions/index.php?act=bio&id=%s' % ( self.getSiteDomain(),
|
||||
self.story.getMetadata('authorId'))
|
||||
self.story.setMetadata('authorUrl',authurl,condremoveentities=False)
|
||||
#drop empty space
|
||||
mblock.pop(0)
|
||||
if 'Rating:' in i.string:
|
||||
self.story.setMetadata('rating',mblock.pop(0).strip())
|
||||
if 'Published:' in i.string:
|
||||
date = mblock.pop(0).strip()
|
||||
#get rid of 'st', 'nd', 'rd', 'th' after day number
|
||||
date = date[0:2]+date[4:]
|
||||
self.story.setMetadata('datePublished',makeDate(date, dateformat))
|
||||
if 'Last Updated:' in i.string:
|
||||
date = mblock.pop(0).strip()
|
||||
#get rid of 'st', 'nd', 'rd', 'th' after day number
|
||||
date = date[0:2]+date[4:]
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, dateformat))
|
||||
if 'Genre:' in i.string:
|
||||
genres = mblock.pop(0).strip()
|
||||
genres = genres.split('/')
|
||||
for genre in genres: self.story.addToList('genre',genre)
|
||||
#end ifs
|
||||
#end while
|
||||
|
||||
# Find the chapter selector
|
||||
select = soup.find('select', { 'name' : 'ch' } )
|
||||
|
||||
if select is None:
|
||||
# no selector found, so it's a one-chapter story.
|
||||
#self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
allOptions = select.findAll('option')
|
||||
for o in allOptions:
|
||||
url = u'http://%s/fanfictions/index.php?act=vie&id=%s&ch=%s' % ( self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
o['value'])
|
||||
title = u"%s" % o
|
||||
title = stripHTML(title)
|
||||
ts = title.split(' ',1)
|
||||
title = ts[0]+'. '+ts[1]
|
||||
self.chapterUrls.append((title,url))
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
## Go scrape the rest of the metadata from the author's page.
|
||||
data = self._fetchUrl(self.story.getMetadata('authorUrl'))
|
||||
soup = self.make_soup(data)
|
||||
#find the story link and its parent div
|
||||
storya = soup.find('a',{'href':self.story.getMetadata('storyUrl')})
|
||||
storydiv = storya.parent
|
||||
#warnings come under a <spawn> tag. Never seen that before...
|
||||
#appears to just be a line of freeform text, not necessarily a list
|
||||
#optional
|
||||
spawn = storydiv.find('spawn',{'id':'warnings'})
|
||||
if spawn is not None:
|
||||
warns = spawn.nextSibling.strip()
|
||||
self.story.addToList('warnings',warns)
|
||||
#some meta in spans - this should get all, even the ones jammed in a table
|
||||
spans = storydiv.findAll('span')
|
||||
for s in spans:
|
||||
if s.string == 'Ship:':
|
||||
list = s.nextSibling.strip().split()
|
||||
self.story.extendList('ships',list)
|
||||
if s.string == 'Characters:':
|
||||
list = s.nextSibling.strip().split(',')
|
||||
self.story.extendList('characters',list)
|
||||
if s.string == 'Status:':
|
||||
st = s.nextSibling.strip()
|
||||
self.story.setMetadata('status',st)
|
||||
if s.string == 'Words:':
|
||||
st = s.nextSibling.strip()
|
||||
self.story.setMetadata('numWords',st)
|
||||
|
||||
#reviews - is this worth having?
|
||||
#ffnet adapter gathers it, don't know if anything else does
|
||||
#or if it's ever going to be used!
|
||||
a = storydiv.find('a',{'id':'bold-blue'})
|
||||
if a:
|
||||
revs = a.nextSibling.strip()[1:-1]
|
||||
self.story.setMetadata('reviews',st)
|
||||
else:
|
||||
revs = '0'
|
||||
self.story.setMetadata('reviews',st)
|
||||
|
||||
# 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' : 'fanfiction'})
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
#remove all the unnecessary bookmark tags
|
||||
[s.extract() for s in div('div',{'class':"tiny_box2"})]
|
||||
|
||||
#is there a review link?
|
||||
r = div.find('a',href=re.compile(re.escape("./index.php?act=irv")+".*$"))
|
||||
if r is not None:
|
||||
#remove the review link and its parent div
|
||||
r.parent.extract()
|
||||
|
||||
#There might also be a link to the sequel on the last chapter
|
||||
#I'm inclined to keep it in, but the URL needs to be changed from relative to absolute
|
||||
#Shame there isn't proper series metadata available
|
||||
#(I couldn't find it anyway)
|
||||
s = div.find('a',href=re.compile(re.escape("./index.php?act=ovr")+".*$"))
|
||||
if s is not None:
|
||||
s['href'] = 'http://'+self.getSiteDomain()+'/fanfictions'+s['href'][1:]
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -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:
|
||||
|
||||
@@ -55,7 +55,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/readfic/'+self.story.getMetadata('storyId'))
|
||||
self._setURL('https://' + self.getSiteDomain() + '/readfic/'+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','fbn')
|
||||
@@ -71,10 +71,10 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/readfic/12345 http://"+cls.getSiteDomain()+"/readfic/93626/246417#part_content"
|
||||
return "https://"+cls.getSiteDomain()+"/readfic/12345 https://"+cls.getSiteDomain()+"/readfic/93626/246417#part_content"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/readfic/")+r"\d+"
|
||||
return r"https?://"+re.escape(self.getSiteDomain()+"/readfic/")+r"\d+"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
@@ -92,39 +92,51 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
adult_div = soup.find('div',id='adultCoverWarning')
|
||||
if adult_div:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
adult_div.extract()
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
table = soup.find('td',{'width':'50%'})
|
||||
|
||||
## Title
|
||||
a = soup.find('h1')
|
||||
a = soup.find('section',{'class':'chapter-info'}).find('h1')
|
||||
# kill '+' marks if present.
|
||||
sup = a.find('sup')
|
||||
if sup:
|
||||
sup.extract()
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
logger.debug("Title: (%s)"%self.story.getMetadata('title'))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = table.find('a')
|
||||
# assume first avatar-nickname -- there can be a second marked 'beta'.
|
||||
a = soup.find('a',{'class':'avatar-nickname'})
|
||||
self.story.setMetadata('authorId',a.text) # Author's name is unique
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('authorUrl','https://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.text)
|
||||
logger.debug("Author: (%s)"%self.story.getMetadata('author'))
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.find('div', {'class' : 'part_list'})
|
||||
chapters = soup.find('ul', {'class' : 'table-of-contents'})
|
||||
if chapters != None:
|
||||
chapters=chapters.findAll('a', href=re.compile(r'/readfic/'+self.story.getMetadata('storyId')+"/\d+#part_content$"))
|
||||
self.story.setMetadata('numChapters',len(chapters))
|
||||
for x in range(0,len(chapters)):
|
||||
chapter=chapters[x]
|
||||
churl='http://'+self.host+chapter['href']
|
||||
churl='https://'+self.host+chapter['href']
|
||||
self.chapterUrls.append((stripHTML(chapter),churl))
|
||||
if x == 0:
|
||||
pubdate = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
pubdate = translit.translit(stripHTML(chapter.parent.find('span')))
|
||||
# pubdate = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
if x == len(chapters)-1:
|
||||
update = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
update = translit.translit(stripHTML(chapter.parent.find('span')))
|
||||
# update = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
else:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
self.story.setMetadata('numChapters',1)
|
||||
pubdate=translit.translit(stripHTML(soup.find('div', {'class' : 'part_added'}).find('span')))
|
||||
pubdate=translit.translit(stripHTML(soup.find('div',{'class':'title-area'}).find('span')))
|
||||
update=pubdate
|
||||
|
||||
logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
@@ -158,54 +170,63 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('dateUpdated', makeDate(update, self.dateformat))
|
||||
self.story.setMetadata('datePublished', makeDate(pubdate, self.dateformat))
|
||||
self.story.setMetadata('language','Russian')
|
||||
|
||||
pr=soup.find('a', href=re.compile(r'/printfic/\w+'))
|
||||
pr='http://'+self.host+pr['href']
|
||||
pr = self.make_soup(self._fetchUrl(pr))
|
||||
pr=pr.findAll('div', {'class' : 'part_text'})
|
||||
|
||||
## after site change, I don't see word count anywhere.
|
||||
# pr=soup.find('a', href=re.compile(r'/printfic/\w+'))
|
||||
# pr='https://'+self.host+pr['href']
|
||||
# pr = self.make_soup(self._fetchUrl(pr))
|
||||
# pr=pr.findAll('div', {'class' : 'part_text'})
|
||||
# i=0
|
||||
# for part in pr:
|
||||
# i=i+len(stripHTML(part).split(' '))
|
||||
# self.story.setMetadata('numWords', unicode(i))
|
||||
|
||||
|
||||
dlinfo = soup.find('dl',{'class':'info'})
|
||||
|
||||
i=0
|
||||
for part in pr:
|
||||
i=i+len(stripHTML(part).split(' '))
|
||||
self.story.setMetadata('numWords', unicode(i))
|
||||
|
||||
i=0
|
||||
fandoms = table.findAll('a', href=re.compile(r'/fanfiction/\w+'))
|
||||
fandoms = dlinfo.find('dd').findAll('a', href=re.compile(r'/fanfiction/\w+'))
|
||||
for fandom in fandoms:
|
||||
self.story.addToList('category',fandom.string)
|
||||
i=i+1
|
||||
if i > 1:
|
||||
self.story.addToList('genre', u'Кроссовер')
|
||||
|
||||
meta=table.findAll('a', href=re.compile(r'/ratings/'))
|
||||
i=0
|
||||
for m in meta:
|
||||
if i == 0:
|
||||
self.story.setMetadata('rating', stripHTML(m))
|
||||
i=1
|
||||
elif i == 1:
|
||||
if not "," in m.nextSibling:
|
||||
i=2
|
||||
self.story.addToList('genre', m.find('b').text)
|
||||
elif i == 2:
|
||||
self.story.addToList('warnings', m.find('b').text)
|
||||
|
||||
|
||||
if table.find('span', {'style' : 'color: green'}):
|
||||
for genre in dlinfo.findAll('a',href=re.compile(r'/genres/')):
|
||||
self.story.addToList('genre',stripHTML(genre))
|
||||
|
||||
ratingdt = dlinfo.find('dt',text='Рейтинг:')
|
||||
self.story.setMetadata('rating', stripHTML(ratingdt.next_sibling))
|
||||
|
||||
# meta=table.findAll('a', href=re.compile(r'/ratings/'))
|
||||
# i=0
|
||||
# for m in meta:
|
||||
# if i == 0:
|
||||
# self.story.setMetadata('rating', stripHTML(m))
|
||||
# i=1
|
||||
# elif i == 1:
|
||||
# if not "," in m.nextSibling:
|
||||
# i=2
|
||||
# self.story.addToList('genre', m.find('b').text)
|
||||
# elif i == 2:
|
||||
# self.story.addToList('warnings', m.find('b').text)
|
||||
|
||||
if dlinfo.find('span', {'style' : 'color: green'}):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
|
||||
tags = table.findAll('b')
|
||||
tags = dlinfo.findAll('dt')
|
||||
for tag in tags:
|
||||
label = translit.translit(tag.text)
|
||||
if 'Piersonazhi:' in label or u'Персонажи:' in label:
|
||||
chars=tag.nextSibling.string.split(', ')
|
||||
chars=stripHTML(tag.next_sibling).split(', ')
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char)
|
||||
break
|
||||
|
||||
summary=soup.find('span', {'class' : 'urlize'})
|
||||
summary=soup.find('div', {'class' : 'urlize'})
|
||||
self.setDescription(url,summary)
|
||||
#self.story.setMetadata('description', summary.text)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -272,7 +272,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
#groups
|
||||
if soup.find('button', {'id':'button-view-all-groups'}):
|
||||
groupResponse = self._fetchUrl("http://www.fimfiction.net/ajax/groups/story_groups_list.php?story=%s" % (self.story.getMetadata("storyId")))
|
||||
groupResponse = self._fetchUrl("https://www.fimfiction.net/ajax/stories/%s/groups" % (self.story.getMetadata("storyId")))
|
||||
groupData = json.loads(groupResponse)
|
||||
groupList = self.make_soup(groupData["content"])
|
||||
else:
|
||||
|
||||
@@ -15,279 +15,32 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
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
|
||||
from adapter_storiesonlinenet import StoriesOnlineNetAdapter
|
||||
|
||||
def getClass():
|
||||
return FineStoriesComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
class FineStoriesComAdapter(StoriesOnlineNetAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0])
|
||||
if 'storyInfo' in self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','fnst')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y-%m-%d"
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'fnst'
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'finestories.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010 http://"+cls.getSiteDomain()+"/library/storyInfo.php?id=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Free Registration' in data \
|
||||
or "Log In" in data \
|
||||
or "Invalid Password!" in data \
|
||||
or "Invalid User Name!" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['theusername'] = self.username
|
||||
params['thepassword'] = self.password
|
||||
else:
|
||||
params['theusername'] = self.getConfig("username")
|
||||
params['thepassword'] = self.getConfig("password")
|
||||
params['rememberMe'] = '1'
|
||||
params['page'] = 'http://'+self.getSiteDomain()+'/'
|
||||
params['submit'] = 'Login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['theusername']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "My Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['theusername']))
|
||||
raise exceptions.FailedToLogin(url,params['theusername'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
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)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.AccessDenied(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)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"/a/\w+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.setMetadata('author',a.text)
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.findAll('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+":\d+$"))
|
||||
if len(chapters) != 0:
|
||||
for chapter in chapters:
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']))
|
||||
else:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId')))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# surprisingly, the detailed page does not give enough details, so go to author's page
|
||||
|
||||
skip=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+unicode(skip)))
|
||||
|
||||
tds = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in tds:
|
||||
if lc2.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId'))):
|
||||
i=1
|
||||
break
|
||||
if tds[len(tds)-1] == lc2:
|
||||
skip=skip+10
|
||||
|
||||
for cat in lc2.findAll('div', {'class' : 'typediv'}):
|
||||
self.story.addToList('category',cat.text)
|
||||
|
||||
self.story.setMetadata('size', lc2.findNext('td', {'class' : 'num'}).text)
|
||||
|
||||
lc4 = lc2.findNext('td', {'class' : 'lc4'})
|
||||
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
|
||||
i = a.parent.text.split('(')[1].split(')')[0]
|
||||
self.setSeries(a.text, i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
|
||||
self.story.addToList("category",a.text)
|
||||
except:
|
||||
pass
|
||||
|
||||
for a in lc4.findAll('span', {'class' : 'help'}) + lc4.findAll('script'):
|
||||
a.extract()
|
||||
|
||||
self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),lc4.text.split('[More Info')[0])
|
||||
|
||||
for b in lc4.findAll('b'):
|
||||
label = b.text
|
||||
value = b.nextSibling
|
||||
|
||||
if 'For Age' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Tags' in label:
|
||||
for genre in value.split(', '):
|
||||
self.story.addToList('genre',genre)
|
||||
|
||||
## Site uses a <script> to inject timestamp in locale plus <noscript> general version.
|
||||
if 'Posted' in label:
|
||||
value = b.find_next_sibling('noscript')
|
||||
if '(' in value:
|
||||
date = makeDate(stripHTML(value.split(' (')[0]), self.dateformat)
|
||||
else:
|
||||
date = makeDate(stripHTML(value), self.dateformat)
|
||||
self.story.setMetadata('datePublished', date)
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
if 'Concluded' in label or 'Updated' in label:
|
||||
value = b.find_next_sibling('noscript')
|
||||
if '(' in value:
|
||||
date = makeDate(stripHTML(value.split(' (')[0]), self.dateformat)
|
||||
else:
|
||||
date = makeDate(stripHTML(value), self.dateformat)
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
status = lc4.find('span', {'class' : 'ab'})
|
||||
if status != None:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
if "Last Activity" in status.text:
|
||||
self.story.setMetadata('dateUpdated', makeDate(status.text.split('Activity: ')[1].split(')')[0], self.dateformat))
|
||||
else:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('article')
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# some big chapters are split over several pages
|
||||
pager = div.find('span', {'class' : 'pager'})
|
||||
if pager != None:
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
|
||||
for ur in urls:
|
||||
soup = self.make_soup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']))
|
||||
|
||||
div1 = soup.find('article')
|
||||
|
||||
#print("div.contents:%s"%(div.contents,))
|
||||
# appending next section
|
||||
last=div.findAll('p')
|
||||
next=div1.find('span', {'class' : 'conTag'}).nextSibling
|
||||
last[len(last)-1]=last[len(last)-1].append(next)
|
||||
|
||||
self.clean_chapter(div1)
|
||||
#print("div.contents:%s"%(div.contents,))
|
||||
for t in div1.contents:
|
||||
div.append(t)
|
||||
|
||||
self.clean_chapter(div)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
def clean_chapter(self,art):
|
||||
# discard included chapter heading.
|
||||
# discard included date
|
||||
# discard share block
|
||||
# continued/continues
|
||||
# discard next link
|
||||
for tag in art.find_all('h2') + \
|
||||
art.find_all('div', class_="date") + \
|
||||
art.find_all('div', class_="vform") + \
|
||||
art.find_all('span', class_="conTag") + \
|
||||
art.find_all('h3', class_="end"):
|
||||
tag.extract()
|
||||
|
||||
# remove pager blocks.
|
||||
for pager in art.find_all('span', class_="pager"):
|
||||
# remove br tags before and after pager.
|
||||
#print("br list prev: %s"%len(pager.find_previous_siblings('br')))
|
||||
#print("br list next: %s"%len(pager.find_next_siblings('br')))
|
||||
for tag in pager.find_next_siblings('br')[:2] + pager.find_previous_siblings('br')[:2]:
|
||||
tag.extract()
|
||||
pager.extract()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -136,10 +136,11 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
authsoup = self.make_soup(authdata)
|
||||
|
||||
reviewsa = authsoup.find('a', href=re.compile(r"reviews\.php\?sid="+self.story.getMetadata('storyId')+r".*"))
|
||||
reviewsa = authsoup.find_all('a', href=re.compile(r"reviews\.php\?sid="+self.story.getMetadata('storyId')+r".*"))
|
||||
# <table><tr><td><p><b><a ...>
|
||||
metablock = reviewsa.findParent("table")
|
||||
metablock = reviewsa[0].findParent("table")
|
||||
#print("metablock:%s"%metablock)
|
||||
self.story.setMetadata('reviews',stripHTML(reviewsa[-1]))
|
||||
|
||||
## Title
|
||||
titlea = metablock.find('a', href=re.compile("viewstory.php"))
|
||||
@@ -148,6 +149,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.FailedToDownload("Story URL (%s) not found on author's page, can't use chapter URLs"%url)
|
||||
self.story.setMetadata('title',stripHTML(titlea))
|
||||
|
||||
total_reads = 0
|
||||
# Find the chapters: !!! hpfandom.net differs from every other
|
||||
# eFiction site--the sid on viewstory for chapters is
|
||||
# *different* for each chapter
|
||||
@@ -156,10 +158,16 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
#print("====chapter===%s"%m.group(1))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/eff/'+m.group(1)))
|
||||
|
||||
try:
|
||||
total_reads += int(stripHTML((chapter.find_parent('tr').find_all('td')[-1])))
|
||||
except:
|
||||
pass # don't care
|
||||
if len(self.chapterUrls) == 0:
|
||||
self.chapterUrls.append((stripHTML(self.story.getMetadata('title')),url))
|
||||
|
||||
if total_reads > 0:
|
||||
self.story.setMetadata('reads',total_reads)
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta 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)
|
||||
|
||||
@@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
import time
|
||||
|
||||
from bs4.element import Comment
|
||||
from ..htmlcleanup import stripHTML
|
||||
@@ -33,13 +32,15 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
logger.debug("LiteroticaComAdapter:__init__ - url='%s'" % 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.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','litero')
|
||||
|
||||
# normalize to first chapter. Not sure if they ever have more than 2 digits.
|
||||
@@ -62,7 +63,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = '%m/%d/%y'
|
||||
self.dateformat = "%m/%d/%y"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
@@ -96,6 +97,18 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://(www|german|spanish|french|dutch|italian|romanian|portuguese|other)(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
|
||||
|
||||
def getCategories(self, soup):
|
||||
if self.getConfig("use_meta_keywords"):
|
||||
categories = soup.find("meta", {"name":"keywords"})['content'].split(', ')
|
||||
categories = [c for c in categories if not self.story.getMetadata('title') in c]
|
||||
if self.story.getMetadata('author') in categories:
|
||||
categories.remove(self.story.getMetadata('author'))
|
||||
logger.debug("Meta = %s" % categories)
|
||||
for category in categories:
|
||||
# logger.debug("\tCategory=%s" % category)
|
||||
# self.story.addToList('category', category.title())
|
||||
self.story.addToList('eroticatags', category.title())
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
"""
|
||||
NOTE: Some stories can have versions,
|
||||
@@ -119,6 +132,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
logger.debug("Chapter/Story URL: <%s> " % self.url)
|
||||
|
||||
try:
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup1 = self.make_soup(data1)
|
||||
@@ -130,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])
|
||||
@@ -145,6 +162,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
soupAuth = self.make_soup(dataAuth)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soupAuth.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
# logger.debug(soupAuth)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(authorurl)
|
||||
@@ -155,6 +173,15 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
## site has started using //domain.name/asdf urls remove https?: from front
|
||||
## site has started putting https back on again.
|
||||
storyLink = soupAuth.find('a', href=re.compile(r'(https?:)?'+re.escape(self.url[self.url.index(':')+1:])))
|
||||
# storyLink = soupAuth.find('a', href=self.url)#[self.url.index(':')+1:])
|
||||
|
||||
if storyLink is not None:
|
||||
# pull the published date from the author page
|
||||
# default values from single link. Updated below if multiple chapter.
|
||||
logger.debug("Found story on the author page.")
|
||||
date = storyLink.parent.parent.findAll('td')[-1].text
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat))
|
||||
|
||||
if storyLink is not None:
|
||||
urlTr = storyLink.parent.parent
|
||||
@@ -166,101 +193,184 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
raise exceptions.FailedToDownload("Couldn't find story <%s> on author's page <%s>" % (self.url, authorurl))
|
||||
|
||||
if isSingleStory:
|
||||
self.story.setMetadata('title', storyLink.text)
|
||||
# self.chapterUrls = [(soup1.h1.string, self.url)]
|
||||
# self.story.setMetadata('title', soup1.h1.string)
|
||||
|
||||
self.story.setMetadata('title', storyLink.text.strip('/'))
|
||||
logger.debug('Title: "%s"' % storyLink.text.strip('/'))
|
||||
self.story.setMetadata('description', urlTr.findAll("td")[1].text)
|
||||
self.story.addToList('eroticatags', urlTr.findAll("td")[2].text)
|
||||
self.story.addToList('category', urlTr.findAll("td")[2].text)
|
||||
# self.story.addToList('eroticatags', urlTr.findAll("td")[2].text)
|
||||
date = urlTr.findAll('td')[-1].text
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat))
|
||||
self.chapterUrls = [(storyLink.text, self.url)]
|
||||
averrating = stripHTML(storyLink.parent)
|
||||
## title (0.00)
|
||||
averrating = averrating[averrating.rfind('(')+1:averrating.rfind(')')]
|
||||
try:
|
||||
self.story.setMetadata('averrating', float(averrating))
|
||||
except:
|
||||
pass
|
||||
# self.story.setMetadata('averrating',averrating)
|
||||
# parse out the list of chapters
|
||||
else:
|
||||
seriesTr = urlTr.previousSibling
|
||||
while 'ser-ttl' not in seriesTr['class']:
|
||||
seriesTr = seriesTr.previousSibling
|
||||
m = re.match("^(?P<title>.*?):\s(?P<numChapters>\d+)\sPart\sSeries$", seriesTr.find("strong").text)
|
||||
self.story.setMetadata('title', m.group('title'))
|
||||
seriesTitle = m.group('title')
|
||||
|
||||
## Walk the chapters
|
||||
chapterTr = seriesTr.nextSibling
|
||||
self.chapterUrls = []
|
||||
dates = []
|
||||
descriptions = []
|
||||
ratings = []
|
||||
chapters = []
|
||||
while chapterTr is not None and 'sl' in chapterTr['class']:
|
||||
descriptions.append(chapterTr.findAll("td")[1].text)
|
||||
description = "%d. %s" % (len(descriptions)+1,stripHTML(chapterTr.findAll("td")[1]))
|
||||
description = stripHTML(chapterTr.findAll("td")[1])
|
||||
chapterLink = chapterTr.find("td", "fc").find("a")
|
||||
if not chapterLink["href"].startswith('http'):
|
||||
chapterLink["href"] = "http:" + chapterLink["href"]
|
||||
self.chapterUrls.append((chapterLink.text, chapterLink["href"]))
|
||||
self.story.addToList('eroticatags', chapterTr.findAll("td")[2].text)
|
||||
dates.append(makeDate(chapterTr.findAll('td')[-1].text, self.dateformat))
|
||||
pub_date = makeDate(chapterTr.findAll('td')[-1].text, self.dateformat)
|
||||
dates.append(pub_date)
|
||||
chapterTr = chapterTr.nextSibling
|
||||
|
||||
chapter_title = chapterLink.text
|
||||
if self.getConfig("clean_chapter_titles"):
|
||||
logger.debug('\tChapter Name: "%s"' % chapterLink.string)
|
||||
logger.debug('\tChapter Name: "%s"' % chapterLink.text)
|
||||
if chapterLink.text.lower().startswith(seriesTitle.lower()):
|
||||
chapter = chapterLink.text[len(seriesTitle):].strip()
|
||||
logger.debug('\tChapter: "%s"' % chapter)
|
||||
if chapter == '':
|
||||
chapter_title = 'Chapter %d' % (len(self.chapterUrls) + 1)
|
||||
else:
|
||||
separater_char = chapter[0]
|
||||
logger.debug('\tseparater_char: "%s"' % separater_char)
|
||||
chapter = chapter[1:].strip() if separater_char in [":", "-"] else chapter
|
||||
logger.debug('\tChapter: "%s"' % chapter)
|
||||
if chapter.lower().startswith('ch.'):
|
||||
chapter = chapter[len('ch.'):]
|
||||
try:
|
||||
chapter_title = 'Chapter %d' % int(chapter)
|
||||
except:
|
||||
chapter_title = 'Chapter %s' % chapter
|
||||
elif chapter.lower().startswith('pt.'):
|
||||
chapter = chapter[len('pt.'):]
|
||||
try:
|
||||
chapter_title = 'Part %d' % int(chapter)
|
||||
except:
|
||||
chapter_title = 'Part %s' % chapter
|
||||
elif separater_char in [":", "-"]:
|
||||
chapter_title = chapter
|
||||
|
||||
# if chapter_title == '':
|
||||
# chapter_title = chapterLink.string
|
||||
|
||||
## Set description to joint chapter descriptions
|
||||
self.story.setMetadata('description', " / ".join(descriptions))
|
||||
# pages include full URLs.
|
||||
chapurl = chapterLink['href']
|
||||
if chapurl.startswith('//'):
|
||||
chapurl = self.parsedUrl.scheme + ':' + chapurl
|
||||
logger.debug("Chapter URL: " + chapurl)
|
||||
logger.debug("Chapter Title: " + chapter_title)
|
||||
logger.debug("Chapter description: " + description)
|
||||
chapters.append((chapter_title, chapurl, description, pub_date))
|
||||
# self.chapterUrls.append((chapter_title, chapurl))
|
||||
numrating = stripHTML(chapterLink.parent)
|
||||
## title (0.00)
|
||||
numrating = numrating[numrating.rfind('(')+1:numrating.rfind(')')]
|
||||
try:
|
||||
ratings.append(float(numrating))
|
||||
except:
|
||||
pass
|
||||
|
||||
chapters = sorted(chapters, key=lambda chapter: chapter[3])
|
||||
for i, chapter in enumerate(chapters):
|
||||
self.chapterUrls.append((chapter[0], chapter[1]))
|
||||
descriptions.append("%d. %s" % (i + 1, chapter[2]))
|
||||
## Set the oldest date as publication date, the newest as update date
|
||||
dates.sort()
|
||||
self.story.setMetadata('datePublished', dates[0])
|
||||
self.story.setMetadata('dateUpdated', dates[-1])
|
||||
self.story.setMetadata('datePublished', chapters[0][3])
|
||||
self.story.setMetadata('dateUpdated', chapters[-1][3])
|
||||
## Set description to joint chapter descriptions
|
||||
self.setDescription(authorurl,"<p>"+"</p>\n<p>".join(descriptions)+"</p>")
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
if len(ratings) > 0:
|
||||
self.story.setMetadata('averrating','%4.2f' % (sum(ratings) / float(len(ratings))))
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
|
||||
# reset storyId to first chapter.
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
# set storyId to 'title-author' to avoid duplicates
|
||||
# self.story.setMetadata('storyId',
|
||||
# re.sub("[^a-z0-9]", "", self.story.getMetadata('title').lower())
|
||||
# + "-"
|
||||
# + re.sub("[^a-z0-9]", "", self.story.getMetadata('author').lower()))
|
||||
self.story.setMetadata('category', soup1.find('div', 'b-breadcrumbs').findAll('a')[1].string)
|
||||
self.getCategories(soup1)
|
||||
# self.story.setMetadata('description', soup1.find('meta', {'name': 'description'})['content'])
|
||||
|
||||
return
|
||||
|
||||
|
||||
def getPageText(self, raw_page, url):
|
||||
logger.debug('Getting page text')
|
||||
# logger.debug(soup)
|
||||
raw_page = raw_page.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>')
|
||||
# logger.debug("\tChapter text: %s" % raw_page)
|
||||
page_soup = self.make_soup(raw_page)
|
||||
[comment.extract() for comment in page_soup.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
story2 = page_soup.find('div', 'b-story-body-x').div
|
||||
# logger.debug("getPageText- name div div...")
|
||||
# logger.debug(soup)
|
||||
# story2.append(page_soup.new_tag('br'))
|
||||
div = self.utf8FromSoup(url, story2)
|
||||
# logger.debug(div)
|
||||
|
||||
fullhtml = unicode(div)
|
||||
# logger.debug(fullhtml)
|
||||
fullhtml = re.sub(r'<br />\s*<br />', r'</p><p>', fullhtml)
|
||||
fullhtml = re.sub(r'^<div>', r'', fullhtml)
|
||||
fullhtml = re.sub(r'</div>$', r'', fullhtml)
|
||||
fullhtml = re.sub(r'(<p><br/></p>\s+)+$', r'', fullhtml)
|
||||
# logger.debug(fullhtml)
|
||||
return fullhtml
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from <%s>' % url)
|
||||
data1 = self._fetchUrl(url)
|
||||
# brute force approach to replace the wrapping <p> tag. If
|
||||
# done by changing tag name, it causes problems with nested
|
||||
# <p> tags.
|
||||
data1 = data1.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>')
|
||||
soup1 = self.make_soup(data1)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
# get story text
|
||||
story1 = soup1.find('div', 'b-story-body-x').div
|
||||
#print("story1:%s"%story1)
|
||||
# story1.name='div'
|
||||
story1.append(soup1.new_tag('br'))
|
||||
storytext = self.utf8FromSoup(url,story1)
|
||||
raw_page = self._fetchUrl(url)
|
||||
page_soup = self.make_soup(raw_page)
|
||||
pages = page_soup.find('select', {'name' : 'page'})
|
||||
page_nums = [page.text for page in pages.findAll('option')] if pages else 0
|
||||
|
||||
# find num pages
|
||||
pgs = int(soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0])
|
||||
logger.debug("pages: "+unicode(pgs))
|
||||
fullhtml = ""
|
||||
self.getCategories(page_soup)
|
||||
if self.getConfig("description_in_chapter"):
|
||||
chapter_description = page_soup.find("meta", {"name" : "description"})['content']
|
||||
logger.debug("\tChapter description: %s" % chapter_description)
|
||||
fullhtml += '<p><b>Description:</b> %s</p><hr />' % chapter_description
|
||||
fullhtml += self.getPageText(raw_page, url)
|
||||
if pages:
|
||||
for page_no in xrange(2, len(page_nums) + 1):
|
||||
page_url = url + "?page=%s" % page_no
|
||||
logger.debug("page_url= %s" % page_url)
|
||||
raw_page = self._fetchUrl(page_url)
|
||||
fullhtml += self.getPageText(raw_page, url)
|
||||
|
||||
# fullhtml = self.utf8FromSoup(url, bs.BeautifulSoup(fullhtml))
|
||||
# fullhtml = re.sub(r'^<div>', r'', fullhtml)
|
||||
# fullhtml = re.sub(r'</div>$', r'', fullhtml)
|
||||
# if None == div:
|
||||
# raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# get all the pages
|
||||
for i in xrange(2, pgs+1):
|
||||
try:
|
||||
logger.debug("fetching page "+unicode(i))
|
||||
time.sleep(0.5)
|
||||
data2 = self._fetchUrl(url, {'page': i})
|
||||
# brute force approach to replace the wrapping <p> tag. If
|
||||
# done by changing tag name, it causes problems with nested
|
||||
# <p> tags.
|
||||
data2 = data2.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>')
|
||||
soup2 = self.make_soup(data2)
|
||||
[comment.extract() for comment in soup2.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
story2 = soup2.find('div', 'b-story-body-x').div
|
||||
# story2.name='div'
|
||||
story2.append(soup2.new_tag('br'))
|
||||
storytext += self.utf8FromSoup(url,story2)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
return storytext
|
||||
return fullhtml
|
||||
|
||||
|
||||
def getClass():
|
||||
|
||||
+44
-54
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team, 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.
|
||||
@@ -22,18 +22,18 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from bs4.element import Comment
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return GrangerEnchantedCom
|
||||
return Lucifaelff
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
class Lucifaelff(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -50,37 +50,28 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
self.section=self.parsedUrl.path.split('/',)[1]
|
||||
|
||||
# normalized story URL.
|
||||
if "malfoymanor" in self.parsedUrl.netloc:
|
||||
self._setURL('http://malfoymanor.' + self.getSiteDomain() + '/themanor/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self.story.addToList("category","The Manor")
|
||||
else:
|
||||
self._setURL('http://' + self.getSiteDomain() + '/enchant/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','gech')
|
||||
self.story.setMetadata('siteabbrev','luci')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%b/%Y"
|
||||
self.dateformat = "%d/%m/%Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'grangerenchanted.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['grangerenchanted.com','malfoymanor.grangerenchanted.com']
|
||||
return 'fanfiction.lucifael.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234"
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(malfoymanor.)?grangerenchanted.com/(enchant|themanor)?/viewstory.php\?sid=\d+$"
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -103,10 +94,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
if "enchant" in self.section:
|
||||
loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login'
|
||||
else:
|
||||
loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/user.php?action=login'
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
@@ -128,13 +116,13 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=1"
|
||||
addurl = "&ageconsent=ok&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+addurl
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
@@ -145,11 +133,21 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in 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.
|
||||
|
||||
# viewstory.php?sid=1882&warning=4
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
@@ -173,7 +171,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
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)
|
||||
@@ -181,12 +179,14 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'pagetitle'})
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
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 = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
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)
|
||||
@@ -194,7 +194,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# 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+'/'+self.section+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
@@ -217,10 +217,11 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while value and 'label' not in defaultGetattr(value,'class') and '<span class="label">' not in unicode(value):
|
||||
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)
|
||||
@@ -228,9 +229,6 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Read' in label:
|
||||
self.story.setMetadata('read', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
@@ -242,12 +240,12 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
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=4'))
|
||||
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=2'))
|
||||
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)
|
||||
|
||||
@@ -261,35 +259,27 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
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"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
|
||||
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))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('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
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
try:
|
||||
self.story.setMetadata('reviews',
|
||||
stripHTML(soup.find('div',{'id':'sort'}).
|
||||
findAll('a', href=re.compile(r'^reviews.php'))[1]))
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
@@ -301,7 +291,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story1'})
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
@@ -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):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2013 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.
|
||||
@@ -91,7 +91,7 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup1 = self.make_soup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
[comment.extract() for comment in soup1.find_all(text=lambda text:isinstance(text, Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
@@ -112,18 +112,18 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# Description
|
||||
synopsis = soup1.find('section', class_='synopsis')
|
||||
description = "\n\n".join([p.text for p in synopsis.findAll('p')])
|
||||
description = "\n\n".join([p.text for p in synopsis.find_all('p')])
|
||||
self.story.setMetadata('description', description)
|
||||
|
||||
# Tags
|
||||
codesDiv = soup1.find('div', class_="storyCodes")
|
||||
for a in codesDiv.findAll('a'):
|
||||
for a in codesDiv.find_all('a'):
|
||||
self.story.addToList('eroticatags', a.text)
|
||||
|
||||
# Publish and update dates
|
||||
publishdate = None
|
||||
updatedate = None
|
||||
datelines = soup1.findAll('h3', class_='dateline')
|
||||
datelines = soup1.find_all('h3', class_='dateline')
|
||||
for dateline in datelines:
|
||||
if dateline.text.startswith('Added '):
|
||||
publishdate = makeDate(dateline.text, "Added " + self.dateformat)
|
||||
@@ -139,7 +139,7 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
if chapterTable is not None:
|
||||
# Multi-chapter story
|
||||
chapterRows = chapterTable.findAll('tr')
|
||||
chapterRows = chapterTable.find_all('tr')
|
||||
|
||||
for row in chapterRows:
|
||||
chapterCell = row.td
|
||||
@@ -172,13 +172,13 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
soup1 = self.make_soup(data1)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
[comment.extract() for comment in soup1.find_all(text=lambda text:isinstance(text, Comment))]
|
||||
|
||||
# get story text
|
||||
story1 = soup1.find('article', id='mcstories')
|
||||
|
||||
# Remove duplicate name and author headers
|
||||
[h3.extract() for h3 in story1.findAll('h3')]
|
||||
[h3.extract() for h3 in story1.find_all('h3',class_=re.compile(r'(title|chapter|byline)'))]
|
||||
|
||||
storytext = self.utf8FromSoup(url, story1)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -16,58 +16,12 @@
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
# This function is called by the downloader in all adapter_*.py files
|
||||
# in this dir to register the adapter class. So it needs to be
|
||||
# updated to reflect the class below it. That, plus getSiteDomain()
|
||||
# take care of 'Registering'.
|
||||
def getClass():
|
||||
return MuggleNetComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class MuggleNetComAdapter(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.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','mgln') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%y" # XXX
|
||||
class MuggleNetComAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain.
|
||||
return 'fanfiction.mugglenet.com'
|
||||
|
||||
@classmethod
|
||||
@@ -75,251 +29,12 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
return ['fanfiction.mugglenet.com','fanfic.mugglenet.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
def getSiteAbbrev(self):
|
||||
return 'mgln'
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://")+r"fanfic(tion)?\.mugglenet\.com"+re.escape("/viewstory.php?sid=")+r"\d+$"
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if "class='errortext'>Registered Users Only" in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login&sid='+self.story.getMetadata('storyId')
|
||||
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.
|
||||
# http://fanfiction.mugglenet.com/viewstory.php?sid=91079&ageconsent=ok&warning=3
|
||||
addurl = "&ageconsent=ok&warning=3" # XXX &warning=5
|
||||
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)
|
||||
#print("\nurl:%s\ndata:\n%s\n"%(url,data))
|
||||
|
||||
# The actual text that is used to announce you need to be an
|
||||
# adult varies from site to site. Again, print data before
|
||||
# the title search to troubleshoot.
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. nfacommunity uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# viewstory.php?sid=1882&warning=4
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=%s((?:&ageconsent=ok)?&warning=\d+)'"%self.story.getMetadata('storyId'),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.AccessDenied(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)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.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 = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+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+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
|
||||
# Not good enough-- content can contain a ('), which ends the content prematurely.
|
||||
# metadesc = soup.find('meta',{'name':'description'})
|
||||
# print("removeAllEntities(metadesc['content']):\n%s\n"%removeAllEntities(metadesc['content']))
|
||||
start='<span class="label">Summary: </span>'
|
||||
end='<span class="label">Rated:</span>'
|
||||
summarydata = data[data.index(start)+len(start):data.index(end)]
|
||||
#print("summarydata:\n%s\n"%summarydata)
|
||||
self.setDescription(url,self.make_soup(summarydata))
|
||||
|
||||
# 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 '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'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
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"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'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('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)
|
||||
def getClass():
|
||||
return MuggleNetComAdapter
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class NaiceaNilmeNetAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'www.naiceanilme.net'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'nnnet'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
def getClass():
|
||||
return NaiceaNilmeNetAdapter
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2012 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,195 +15,22 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class NCISFictionNetAdapter(BaseEfictionAdapter):
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'ncisfiction.net'
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'ncisfn'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%Y"
|
||||
|
||||
def getClass():
|
||||
return NCISFictionNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class NCISFictionNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["iso-8859-1",
|
||||
"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 sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
+"/chapters.php?stid="+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ncisfn')
|
||||
|
||||
# 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 # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.ncisfiction.net'
|
||||
|
||||
## Changed from www.ncisfiction.com to www.ncisfiction.net Oct
|
||||
## 2012 due to the ncisfiction.com domain expiring. Still accept
|
||||
## .com domains for existing updates, etc.
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.ncisfiction.net','www.ncisfiction.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/story.php?stid=01234 http://"+cls.getSiteDomain()+"/chapters.php?stid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r'http://www\.ncisfiction\.(net|com)/(chapters|story)?.php\?stid=\d+'
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.AccessDenied(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)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title and author
|
||||
a = soup.find('div', {'class' : 'main_title'})
|
||||
|
||||
aut = a.find('a')
|
||||
self.story.setMetadata('authorId',aut['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href'])
|
||||
self.story.setMetadata('author',aut.string)
|
||||
|
||||
aut.extract()
|
||||
self.story.setMetadata('title',stripHTML(a)[:len(stripHTML(a))-2])
|
||||
|
||||
# Find the chapters:
|
||||
i=0
|
||||
chapters=soup.findAll('table', {'class' : 'story_table'})
|
||||
for chapter in chapters:
|
||||
ch=chapter.find('a')
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(ch),'http://'+self.host+'/'+ch['href']))
|
||||
if i == 0:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(chapter.find('td')).split('Added: ')[1], self.dateformat))
|
||||
if i == len(chapters)-1:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(chapter.find('td')).split('Added: ')[1], self.dateformat))
|
||||
i=i+1
|
||||
|
||||
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.
|
||||
|
||||
info = soup.find('table', {'class' : 'story_info'})
|
||||
|
||||
# no convenient way to calculate word count as it is logged differently for stories with and without series
|
||||
|
||||
labels = info.findAll('tr')
|
||||
for tr in labels:
|
||||
value = tr.find('td')
|
||||
label = tr.find('th').string
|
||||
|
||||
if 'Summary' in label:
|
||||
self.setDescription(url,value)
|
||||
|
||||
if 'Rating' in label:
|
||||
self.story.setMetadata('rating', value.string)
|
||||
|
||||
if 'Category' in label:
|
||||
cats = value.findAll('a')
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = value.findAll('a')
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = value.findAll('a')
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = value.findAll('a')
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = value.findAll('a')
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Status' in label:
|
||||
if 'not completed' in value.text:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
else:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('div',{'class' : 'sub_header'})
|
||||
series_name = a.find('a').string
|
||||
i = a.text.split('#')[1]
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+a.find('a')['href'])
|
||||
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', {'class' : 'story_text'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# -*- 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 NickAndGregNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class NickAndGregNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--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 /fanfic part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/desert_archive/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','nag')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y/%m/%d"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.nickngreg.nl'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.nickngreg.nl','www.nickandgreg.net']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/desert_archive/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return "http://("+self.getSiteDomain()+"|www.nickandgreg.net)"+re.escape("/desert_archive/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&i=1'
|
||||
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 "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.AccessDenied(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)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.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 = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/desert_archive/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.find('select')
|
||||
for chapter in chapters.findAll('option'):
|
||||
if chapter.text != 'Story Index' and chapter.text != 'Chapters':
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/desert_archive/'+chapter['value']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
|
||||
for div in asoup.findAll('td', {'class' : 'tblborder6'}):
|
||||
a = div.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
if a != None:
|
||||
break
|
||||
|
||||
self.setDescription(url,div.find('br').nextSibling)
|
||||
|
||||
a=div.text.split('Rating:')
|
||||
if len(a) == 2: self.story.setMetadata('rating', a[1].split(' -')[0])
|
||||
|
||||
a=div.text.split('Characters:')
|
||||
if len(a) == 2:
|
||||
for char in a[1].split(' -')[0].split(', '):
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
a=div.text.split('Genres:')
|
||||
if len(a) == 2:
|
||||
for genre in a[1].split(' -')[0].split(', '):
|
||||
self.story.addToList('genre',genre)
|
||||
|
||||
a=div.text.split('Warnings:')
|
||||
if len(a) == 2:
|
||||
for warn in a[1].split(' -')[0].split(', '):
|
||||
if 'none' not in warn:
|
||||
self.story.addToList('warnings',warn)
|
||||
|
||||
a=div.text.split('Completed:')
|
||||
if len(a) ==2:
|
||||
if 'Yes' in a[1]:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
a=div.text.split('Published:')
|
||||
if len(a) == 2: self.story.setMetadata('datePublished', makeDate(stripHTML(a[1].split(' -')[0]), self.dateformat))
|
||||
|
||||
a=div.text.split('Updated:')
|
||||
if len(a) == 2: self.story.setMetadata('dateUpdated', makeDate(stripHTML(a[1].split(' -')[0]), self.dateformat))
|
||||
|
||||
|
||||
# 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))
|
||||
|
||||
# wrap a div around it.
|
||||
divsoup = self.make_soup('<div class="story"></div>')
|
||||
div = divsoup.find('div')
|
||||
div.append(soup.find('table', {'class' : 'tblborder6'}))
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -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.
|
||||
@@ -270,7 +270,10 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
tag = soup.find('td', {'class' : 'story'})
|
||||
if tag == None and "<center><b>Chapter does not exist!</b></center>" in data:
|
||||
logger.error("Chapter is missing at: %s"%url)
|
||||
return self.utf8FromSoup(url,self.make_soup("<div><p><center><b>Chapter does not exist!</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url)))
|
||||
return self.utf8FromSoup(url,self.make_soup("<div><p><center><b>Site says: Chapter does not exist!</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url)))
|
||||
elif tag == None and "<center><b>This chapter has corrupted or a blank chapter was uploaded. Please contact the author and request that they re-upload the chapter</b></center>" in data:
|
||||
logger.error("Chapter is missing at: %s"%url)
|
||||
return self.utf8FromSoup(url,self.make_soup("<div><p><center><b>Site says: This chapter has corrupted or a blank chapter was uploaded.</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url)))
|
||||
tag.name='div' # force to be a div to avoid problems with nook.
|
||||
|
||||
centers = tag.findAll('center')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import urlparse
|
||||
import urllib2
|
||||
@@ -58,25 +60,25 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
if not element:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
self.story.setMetadata('title', element.find('h1').get_text())
|
||||
|
||||
# quotev html is all about formatting without any content tagging
|
||||
authdiv = soup.find('div', {'style':"text-align:left;"})
|
||||
title = element.find('h1')
|
||||
self.story.setMetadata('title', title.get_text())
|
||||
|
||||
authdiv = soup.find('div', {'class':"quizAuthorList"})
|
||||
if authdiv:
|
||||
#print("div:%s"%authdiv.find_all('a'))
|
||||
print("div:%s"%authdiv)
|
||||
for a in authdiv.find_all('a'):
|
||||
self.story.addToList('author', a.get_text())
|
||||
self.story.addToList('authorId', a['href'].split('/')[-1])
|
||||
self.story.addToList('authorUrl', urlparse.urljoin(self.url, a['href']))
|
||||
else:
|
||||
self.story.setMetadata('author','Anonymous')
|
||||
self.story.setMetadata('authorUrl','http://www.quotev.com')
|
||||
self.story.setMetadata('authorId','0')
|
||||
if not self.story.getList('author'):
|
||||
self.story.addToList('author','Anonymous')
|
||||
self.story.addToList('authorUrl','http://www.quotev.com')
|
||||
self.story.addToList('authorId','0')
|
||||
|
||||
self.setDescription(self.url, soup.find('div', id='qdesct'))
|
||||
imgmeta = soup.find('meta',{'property':"og:image" })
|
||||
if imgmeta:
|
||||
self.setCoverImage(self.url, urlparse.urljoin(self.url, imgmeta['content']))
|
||||
self.coverurl = self.setCoverImage(self.url, urlparse.urljoin(self.url, imgmeta['content']))[1]
|
||||
|
||||
for a in soup.find_all('a', {'href': re.compile(SITE_DOMAIN+'/stories/c/')}):
|
||||
self.story.addToList('category', a.get_text())
|
||||
@@ -89,9 +91,9 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
if len(elements) > 1:
|
||||
self.story.setMetadata('dateUpdated', datetime.datetime.fromtimestamp(float(elements[1]['ts'])))
|
||||
|
||||
metadiv = elements[0].parent
|
||||
|
||||
if 'completed' in stripHTML(metadiv):
|
||||
metadiv = elements[0].parent.parent
|
||||
# print stripHTML(metadiv)
|
||||
if u'· completed ·' in stripHTML(metadiv):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
@@ -102,16 +104,13 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
parts = datum.split()
|
||||
if len(parts) < 2 or parts[1] not in self.getConfig('extra_valid_entries'):
|
||||
continue
|
||||
# Not a valid metadatum
|
||||
# if not len(parts) == 2:
|
||||
# continue
|
||||
|
||||
key, value = parts[1], parts[0]
|
||||
self.story.setMetadata(key, value.replace(',', '').replace('.', ''))
|
||||
|
||||
favspans = soup.find('a',{'id':'fav_btn'}).find_all('span')
|
||||
if len(favspans) > 1:
|
||||
self.story.setMetadata('favorites', stripHTML(favspans[1]).replace(',', ''))
|
||||
self.story.setMetadata('favorites', stripHTML(favspans[-1]).replace(',', ''))
|
||||
|
||||
commentspans = soup.find('a',{'id':'comment_btn'}).find_all('span')
|
||||
#print("commentspans:%s"%commentspans)
|
||||
@@ -119,7 +118,8 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('comments', stripHTML(commentspans[0]).replace(',', ''))
|
||||
|
||||
for a in soup.find('div', id='rselect')('a'):
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
if 'javascript' not in a['href']:
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
@@ -127,8 +127,15 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
soup = self.make_soup(data)
|
||||
|
||||
element = soup.find('div', id='rescontent')
|
||||
for a in element('a'):
|
||||
rescontent = soup.find('div', id='rescontent')
|
||||
|
||||
# attempt to find and include chapter specific images.
|
||||
img = soup.find('div',{'id':'quizHeader'}).find('img')
|
||||
#print("img['src'](%s) != self.coverurl(%s)"%(img['src'],self.coverurl))
|
||||
if img['src'] != self.coverurl:
|
||||
rescontent.insert(0,img)
|
||||
|
||||
for a in rescontent('a'):
|
||||
a.unwrap()
|
||||
|
||||
return self.utf8FromSoup(url, element)
|
||||
return self.utf8FromSoup(url, rescontent)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -36,6 +36,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
logger.debug("StoriesOnlineNetAdapter.__init__ - url='%s'" % url)
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
@@ -50,12 +51,16 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/'+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','strol')
|
||||
self.story.setMetadata('siteabbrev',self.getSiteAbbrev())
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y-%m-%d"
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'strol'
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
@@ -66,7 +71,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)?"
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/(s|library)/(storyInfo.php\?id=)?(?P<id>\d+)((:\d+)?(;\d+)?$|(:i)?$)?"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -92,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']))
|
||||
|
||||
@@ -142,6 +147,8 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
elif "Error! The story you're trying to access is being filtered by your choice of contents filtering." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! The story you're trying to access is being filtered by your choice of contents filtering.")
|
||||
elif "Error! Daily Limit Reached" in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! Daily Limit Reached")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
@@ -178,7 +185,8 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
page=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+unicode(page)))
|
||||
data = self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+unicode(page))
|
||||
asoup = self.make_soup(data)
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
@@ -203,6 +211,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/series/\d+/.*"))
|
||||
# logger.debug("Looking for series - a='{0}'".format(a))
|
||||
if a:
|
||||
# if there's a number after the series name, series_contents is a two element list:
|
||||
# [<a href="...">Title</a>, u' (2)']
|
||||
@@ -211,59 +220,62 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
seriesUrl = 'http://'+self.host+a['href']
|
||||
self.story.setMetadata('seriesUrl',seriesUrl)
|
||||
series_name = stripHTML(a)
|
||||
logger.debug("Series name= %s" % series_name)
|
||||
# logger.debug("Series name= %s" % series_name)
|
||||
series_soup = self.make_soup(self._fetchUrl(seriesUrl))
|
||||
if series_soup:
|
||||
logger.debug("Retrieving Series - looking for name")
|
||||
series_name = series_soup.find('span', {'id' : 'ptitle'}).text.partition(' — ')[0]
|
||||
logger.debug("Series name: '{0}'".format(series_name))
|
||||
# logger.debug("Retrieving Series - looking for name")
|
||||
series_name = stripHTML(series_soup.find('span', {'id' : 'ptitle'}))
|
||||
series_name = re.sub(r' . a series by.*$','',series_name)
|
||||
# logger.debug("Series name: '%s'" % series_name)
|
||||
self.setSeries(series_name, i)
|
||||
desc = lc4.contents[2]
|
||||
# Check if series is in a universe
|
||||
universe_url = self.story.getList('authorUrl')[0] + "&type=uni"
|
||||
universes_soup = self.make_soup(self._fetchUrl(universe_url) )
|
||||
logger.debug("Universe url='{0}'".format(universe_url))
|
||||
# logger.debug("Universe url='{0}'".format(universe_url))
|
||||
if universes_soup:
|
||||
universes = universes_soup.findAll('div', {'class' : 'ser-box'})
|
||||
logger.debug("Number of Universes: %d" % len(universes))
|
||||
# logger.debug("Number of Universes: %d" % len(universes))
|
||||
for universe in universes:
|
||||
logger.debug("universe.find('a')={0}".format(universe.find('a')))
|
||||
# logger.debug("universe.find('a')={0}".format(universe.find('a')))
|
||||
# The universe id is in an "a" tag that has an id but nothing else. It is the first tag.
|
||||
# The id is prefixed with the letter "u".
|
||||
universe_id = universe.find('a')['id'][1:]
|
||||
logger.debug("universe_id='%s'" % universe_id)
|
||||
universe_name = universe.find('div', {'class' : 'ser-name'}).text.partition(' ')[2]
|
||||
logger.debug("universe_name='%s'" % universe_name)
|
||||
# logger.debug("universe_id='%s'" % universe_id)
|
||||
universe_name = stripHTML(universe.find('div', {'class' : 'ser-name'})).partition(' ')[2]
|
||||
# logger.debug("universe_name='%s'" % universe_name)
|
||||
# If there is link to the story, we have the right universe
|
||||
story_a = universe.find('a', href=re.compile('/s/'+self.story.getMetadata('storyId')))
|
||||
if story_a:
|
||||
logger.debug("Story is in a series that is in a universe! The universe is '%s'" % universe_name)
|
||||
# logger.debug("Story is in a series that is in a universe! The universe is '%s'" % universe_name)
|
||||
self.story.setMetadata("universe", universe_name)
|
||||
self.story.setMetadata('universeUrl','http://'+self.host+ '/library/universe.php?id=' + universe_id)
|
||||
break
|
||||
else:
|
||||
logger.debug("No universe page")
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/universe/\d+/.*"))
|
||||
logger.debug("Looking for universe - a='{0}'".format(a))
|
||||
# logger.debug("Looking for universe - a='{0}'".format(a))
|
||||
if a:
|
||||
self.story.setMetadata("universe",stripHTML(a))
|
||||
desc = lc4.contents[2]
|
||||
# Assumed only one universe, but it does have a URL--use universeHTML
|
||||
universe_name = stripHTML(a)
|
||||
universeUrl = 'http://'+self.host+a['href']
|
||||
logger.debug("Retrieving Universe - about to get page - universeUrl='{0}".format(universeUrl))
|
||||
# logger.debug("Retrieving Universe - about to get page - universeUrl='{0}".format(universeUrl))
|
||||
universe_soup = self.make_soup(self._fetchUrl(universeUrl))
|
||||
logger.debug("Retrieving Universe - have page")
|
||||
if universe_soup:
|
||||
logger.debug("Retrieving Universe - looking for name")
|
||||
universe_name = universe_soup.find('h1', {'id' : 'ptitle'}).text.partition('—')[0]
|
||||
logger.debug("Universes name: '{0}'".format(universe_name))
|
||||
universe_name = stripHTML(universe_soup.find('h1', {'id' : 'ptitle'}))
|
||||
universe_name = re.sub(r' . A Universe from the Mind.*$','',universe_name)
|
||||
# logger.debug("Universes name: '{0}'".format(universe_name))
|
||||
|
||||
self.story.setMetadata('universeUrl',universeUrl)
|
||||
logger.debug("Setting universe name: '{0}'".format(universe_name))
|
||||
# logger.debug("Setting universe name: '{0}'".format(universe_name))
|
||||
self.story.setMetadata('universe',universe_name)
|
||||
if self.getConfig("universe_as_series"):
|
||||
self.setSeries(universe_name, 0)
|
||||
@@ -271,6 +283,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
logger.debug("Do not have a universe")
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
|
||||
self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),desc)
|
||||
@@ -290,7 +303,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
if 'Tags' in label or 'Codes' in label:
|
||||
for code in re.split(r'\s*,\s*', value.strip()):
|
||||
self.story.addToList('sitetags',code)
|
||||
self.story.addToList('sitetags',code)
|
||||
|
||||
if 'Posted' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
@@ -307,12 +320,12 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
# http://storiesonline.net/s/11999
|
||||
# http://storiesonline.net/s/10823
|
||||
if get_cover:
|
||||
logger.debug("Looking for the cover image...")
|
||||
# logger.debug("Looking for the cover image...")
|
||||
cover_url = ""
|
||||
img = soup.find('img')
|
||||
if img:
|
||||
cover_url=img['src']
|
||||
logger.debug("cover_url: %s"%cover_url)
|
||||
# logger.debug("cover_url: %s"%cover_url)
|
||||
if cover_url:
|
||||
self.setCoverImage(url,cover_url)
|
||||
|
||||
@@ -350,7 +363,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
logger.debug("pager urls:%s"%urls)
|
||||
# logger.debug("pager urls:%s"%urls)
|
||||
pager.extract()
|
||||
chaptertag.contents = chaptertag.contents[2:]
|
||||
|
||||
@@ -359,7 +372,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
pagetag = soup.find('div', {'id' : 'story'})
|
||||
if not pagetag:
|
||||
logger.debug("div id=story not found, try article")
|
||||
# logger.debug("div id=story not found, try article")
|
||||
pagetag = soup.find('article', {'id' : 'story'})
|
||||
|
||||
self.cleanPage(pagetag)
|
||||
|
||||
@@ -34,6 +34,8 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
self.username=''
|
||||
self.is_adult=False
|
||||
# happens inside BaseSiteAdapter.__init__
|
||||
# self._setURL(url)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
@@ -117,7 +119,6 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
self.story.setMetadata(u'title',"Test Story Title "+idstr)
|
||||
self.story.setMetadata('author','Test Author aa')
|
||||
self.story.setMetadata('storyUrl',self.url)
|
||||
self.setDescription(self.url,u'Description '+self.crazystring+u''' Done
|
||||
<p>
|
||||
Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic"
|
||||
|
||||
@@ -193,14 +193,10 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
if None == soup:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# Ugh. chapter html doesn't haven't anything useful around it to demarcate.
|
||||
for a in soup.findAll('table'):
|
||||
content = soup.find('table',{'class':'table'}).find('td') # td inside <table class='table'>
|
||||
content.name='div'
|
||||
|
||||
for a in content.findAll('table'):
|
||||
a.extract()
|
||||
|
||||
for a in soup.findAll('head'):
|
||||
a.extract()
|
||||
|
||||
html = soup.find('html')
|
||||
html.name='div'
|
||||
|
||||
return self.utf8FromSoup(url,soup)
|
||||
return self.utf8FromSoup(url,content)
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
# -*- 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 TokraFandomnetComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class TokraFandomnetComAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','tokra')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it. But it
|
||||
# doesn't matter too much anymore.
|
||||
return 'tokra.fandomnet.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=3"
|
||||
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
|
||||
|
||||
m = re.search(r"'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.AccessDenied(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)
|
||||
#print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.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 = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Rating
|
||||
rate = stripHTML(soup.find('div',{'id':'pagetitle'}))
|
||||
rate = rate[rate.rindex('[')+1:rate.rindex(']')]
|
||||
self.story.setMetadata('rating', rate)
|
||||
|
||||
# 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+'/'+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.
|
||||
|
||||
metadiv = soup.find('div',{'class':'content'})
|
||||
smalldiv = metadiv.find('div',{'class':'small'})
|
||||
|
||||
# tokra categories -> genre
|
||||
# categories will be filled from ini.
|
||||
genres = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
chars = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
metatext = stripHTML(smalldiv)
|
||||
|
||||
if 'Completed: Yes' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
wordstart=metatext.rindex('Word count:')+12
|
||||
words = metatext[wordstart:metatext.index(' ',wordstart)]
|
||||
self.story.setMetadata('numWords', words)
|
||||
|
||||
datesdiv = soup.find('div',{'class':'bottom'})
|
||||
dates = stripHTML(datesdiv).split()
|
||||
# Published: 04/26/2011 Updated: 03/06/2013
|
||||
self.story.setMetadata('datePublished', makeDate(dates[1], self.dateformat))
|
||||
self.story.setMetadata('dateUpdated', makeDate(dates[3], self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"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))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('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
|
||||
|
||||
# remove 'small' leaving only summary.
|
||||
smalldiv.extract()
|
||||
self.setDescription(url,metadiv)
|
||||
|
||||
# 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' : 'content'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# remove some decorations while keeping notes.
|
||||
remove = div.find('div', {'id' : 'pagetitle'})
|
||||
remove.extract()
|
||||
|
||||
for remove in div.findAll('div', {'class' : 'right'}):
|
||||
remove.extract()
|
||||
|
||||
for remove in div.findAll('div', {'class' : 'left'}):
|
||||
remove.extract()
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -131,7 +131,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
#print("data:%s"%data)
|
||||
soup = self.make_soup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
if e.code in (404,410):
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
@@ -257,10 +257,11 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
for cat in verticaltable.findAll('a', href=re.compile(r"^/Category-")):
|
||||
# assumes only one -Centered and one Pairing: cat can ever
|
||||
# be applied to one story.
|
||||
if self.getConfig('centeredcat_to_characters') and cat.string.endswith('-Centered'):
|
||||
# Seen at least once: incorrect (empty) cat link, thus "and cat.string"
|
||||
if self.getConfig('centeredcat_to_characters') and cat.string and cat.string.endswith('-Centered'):
|
||||
char = cat.string[:-len('-Centered')]
|
||||
self.story.addToList('characters',char)
|
||||
elif self.getConfig('pairingcat_to_characters_ships') and cat.string.startswith('Pairing: '):
|
||||
elif self.getConfig('pairingcat_to_characters_ships') and cat.string and cat.string.startswith('Pairing: '):
|
||||
pair = cat.string[len('Pairing: '):]
|
||||
self.story.addToList('characters',pair)
|
||||
if char:
|
||||
|
||||
@@ -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.
|
||||
@@ -16,7 +16,8 @@
|
||||
#
|
||||
|
||||
import re
|
||||
import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import time
|
||||
import logging
|
||||
import urllib
|
||||
@@ -99,7 +100,7 @@ class BaseSiteAdapter(Configurable):
|
||||
self.metadataDone = False
|
||||
self.story = Story(configuration)
|
||||
self.story.setMetadata('site',self.getConfigSection())
|
||||
self.story.setMetadata('dateCreated',datetime.datetime.now())
|
||||
self.story.setMetadata('dateCreated',datetime.now())
|
||||
self.chapterUrls = [] # tuples of (chapter title,chapter url)
|
||||
self.chapterFirst = None
|
||||
self.chapterLast = None
|
||||
@@ -131,9 +132,9 @@ 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):
|
||||
'''
|
||||
@@ -187,7 +188,7 @@ class BaseSiteAdapter(Configurable):
|
||||
self.parsedUrl = up.urlparse(url)
|
||||
self.host = self.parsedUrl.netloc
|
||||
self.path = self.parsedUrl.path
|
||||
self.story.setMetadata('storyUrl',self.url)
|
||||
self.story.setMetadata('storyUrl',self.url,condremoveentities=False)
|
||||
|
||||
## website encoding(s)--in theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
@@ -236,11 +237,11 @@ class BaseSiteAdapter(Configurable):
|
||||
'''
|
||||
cachekey=self._get_cachekey(url, parameters, headers)
|
||||
if usecache and self._has_cachekey(cachekey):
|
||||
logger.debug("#####################################\npagecache HIT: %s"%cachekey)
|
||||
logger.debug("#####################################\npagecache HIT: %s"%safe_url(cachekey))
|
||||
data,redirecturl = self._get_from_pagecache(cachekey)
|
||||
return data
|
||||
|
||||
logger.debug("#####################################\npagecache MISS: %s"%cachekey)
|
||||
logger.debug("#####################################\npagecache MISS: %s"%safe_url(cachekey))
|
||||
self.do_sleep(extrasleep)
|
||||
|
||||
## u2.Request assumes POST when data!=None. Also assumes data
|
||||
@@ -279,7 +280,7 @@ class BaseSiteAdapter(Configurable):
|
||||
'''
|
||||
cachekey=self._get_cachekey(url, parameters)
|
||||
if usecache and self._has_cachekey(cachekey):
|
||||
logger.debug("#####################################\npagecache HIT: %s"%cachekey)
|
||||
logger.debug("#####################################\npagecache HIT: %s"%safe_url(cachekey))
|
||||
data,redirecturl = self._get_from_pagecache(cachekey)
|
||||
class FakeOpened:
|
||||
def __init__(self,data,url):
|
||||
@@ -289,7 +290,7 @@ class BaseSiteAdapter(Configurable):
|
||||
def read(self): return self.data
|
||||
return (data,FakeOpened(data,redirecturl))
|
||||
|
||||
logger.debug("#####################################\npagecache MISS: %s"%cachekey)
|
||||
logger.debug("#####################################\npagecache MISS: %s"%safe_url(cachekey))
|
||||
self.do_sleep(extrasleep)
|
||||
if parameters != None:
|
||||
opened = self.opener.open(url.replace(' ','%20'),urllib.urlencode(parameters),float(self.getConfig('connect_timeout',30.0)))
|
||||
@@ -338,15 +339,15 @@ class BaseSiteAdapter(Configurable):
|
||||
return (self._decode(data),opened)
|
||||
except u2.HTTPError, he:
|
||||
excpt=he
|
||||
if he.code in (403,404):
|
||||
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(he)))
|
||||
if he.code in (403,404,410):
|
||||
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(safe_url(url)),unicode(he)))
|
||||
break # break out on 404
|
||||
except Exception, e:
|
||||
excpt=e
|
||||
logger.warn("Caught an exception reading URL: %s sleeptime(%s) Exception %s."%(unicode(url),sleeptime,unicode(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" %url)
|
||||
logger.exception(excpt)
|
||||
logger.error("Giving up on %s" %safe_url(url))
|
||||
logger.debug(excpt, exc_info=True)
|
||||
raise(excpt)
|
||||
|
||||
# Limit chapters to download. Input starts at 1, list starts at 0
|
||||
@@ -392,6 +393,13 @@ class BaseSiteAdapter(Configurable):
|
||||
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),
|
||||
@@ -571,11 +579,14 @@ class BaseSiteAdapter(Configurable):
|
||||
#print("include_images:"+self.getConfig('include_images'))
|
||||
if self.getConfig('include_images'):
|
||||
acceptable_attributes.extend(('src','alt','longdesc'))
|
||||
for img in soup.findAll('img'):
|
||||
# some pre-existing epubs have img tags that had src stripped off.
|
||||
if img.has_attr('src'):
|
||||
(img['src'],img['longdesc'])=self.story.addImgUrl(url,img['src'],fetch,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
try:
|
||||
for img in soup.find_all('img'):
|
||||
# some pre-existing epubs have img tags that had src stripped off.
|
||||
if img.has_attr('src'):
|
||||
(img['src'],img['longdesc'])=self.story.addImgUrl(url,img['src'],fetch,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
except AttributeError as ae:
|
||||
logger.info("Parsing for img tags failed--probably poor input HTML. Skipping images.")
|
||||
|
||||
for attr in self.get_attr_keys(soup):
|
||||
if attr not in acceptable_attributes:
|
||||
@@ -685,6 +696,26 @@ def makeDate(string,dateform):
|
||||
if name in string:
|
||||
string = string.replace(name,num)
|
||||
break
|
||||
|
||||
return datetime.datetime.strptime(string.encode('utf-8'),dateform.encode('utf-8'))
|
||||
|
||||
# Many locales don't define %p for AM/PM. So if %p, remove from
|
||||
# dateform, look for 'pm' in string, remove am/pm from string and
|
||||
# add 12 hours if pm found.
|
||||
add_hours = False
|
||||
if u"%p" in dateform:
|
||||
dateform = dateform.replace(u"%p",u"")
|
||||
if 'pm' in string or 'PM' in string:
|
||||
add_hours = True
|
||||
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.
|
||||
safe_url_re = re.compile(r'(?P<attr>(password|name|login).?=)[^&]*(?P<amp>&|$)',flags=re.MULTILINE)
|
||||
def safe_url(url):
|
||||
# return url with password attr (if present) obscured.
|
||||
return re.sub(safe_url_re,r'\g<attr>XXXXXXXX\g<amp>',url)
|
||||
|
||||
@@ -51,7 +51,7 @@ _WRONGPASSWORD = "That password doesn't match the one in our database"
|
||||
_USERACCOUNT = 'Member Account'
|
||||
|
||||
# Regular expressions
|
||||
_REGEX_WARING_PARAM = re.compile("warning=(?P<warningId>\d+)")
|
||||
_REGEX_WARNING_PARAM = re.compile("warning=(?P<warningId>\d+)")
|
||||
_REGEX_CHAPTER_B = re.compile("^(?P<chapterId>\d+)\.")
|
||||
_REGEX_CHAPTER_PARAM = re.compile("chapter=(?P<chapterId>\d+)$")
|
||||
_REGEX_CHAPTER_FRAGMENT = re.compile("^#(?P<chapterId>\d+)$")
|
||||
@@ -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):
|
||||
@@ -345,6 +345,8 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
self.triedLoggingIn = True
|
||||
else:
|
||||
raise exceptions.FailedToLogin(self.url, unicode(errorDiv))
|
||||
elif "This story has not been validated" in stripHTML(errorDiv):
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: "+stripHTML(errorDiv))
|
||||
else:
|
||||
warningLink = errorDiv.find("a")
|
||||
if warningLink is not None and ( \
|
||||
@@ -354,15 +356,15 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
# XXX Using this method, we're independent of # getHighestWarningLevel
|
||||
printUrl += "&ageconsent=ok&warning=%s" % (_REGEX_WARING_PARAM.search(warningLink['href']).group(1))
|
||||
printUrl += "&ageconsent=ok&warning=%s" % (_REGEX_WARNING_PARAM.search(warningLink['href']).group(1))
|
||||
# printUrl += "&ageconsent=ok&warning=%s" % self.getHighestWarningLevel()
|
||||
soup = self._fetch_to_soup(printUrl)
|
||||
errorDiv = soup.find("div", "errortext")
|
||||
self.triedAcceptWarnings = True
|
||||
else:
|
||||
raise exception.FailedToDownload(self.url, unicode(errorDiv))
|
||||
raise exceptions.FailedToDownload("Error with URL: %s (%s)" % (self.url,stripHTML(errorDiv)))
|
||||
else:
|
||||
raise exception.FailedToDownload(self.url, unicode(errorDiv))
|
||||
raise exceptions.FailedToDownload("Error with URL: %s (%s)" % (self.url,stripHTML(errorDiv)))
|
||||
|
||||
# title and author
|
||||
pagetitleDiv = soup.find("div", {"id": "pagetitle"})
|
||||
@@ -382,7 +384,7 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
while nextEl is not None and not (\
|
||||
type(nextEl) is bs.Tag \
|
||||
and nextEl.name == "span" \
|
||||
and 'label' in nextEl['class'] \
|
||||
and 'label' in nextEl.get('class',[]) \
|
||||
):
|
||||
## must string copy nextEl or nextEl will change trees
|
||||
if (type(nextEl) is bs.Tag):
|
||||
|
||||
@@ -49,9 +49,9 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
#logger.debug("groupdict:%s"%m.groupdict())
|
||||
if m.group('post'):
|
||||
self.story.setMetadata('storyId',m.group('post'))
|
||||
self._setURL(self.getURLPrefix() + '/posts/'+m.group('post')+'/')
|
||||
if m.group('anchorpost'):
|
||||
self.story.setMetadata('storyId',m.group('anchorpost'))
|
||||
self._setURL(self.getURLPrefix() + '/posts/'+m.group('anchorpost')+'/')
|
||||
else:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
# normalized story URL.
|
||||
@@ -83,7 +83,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
return cls.getURLPrefix()+"/threads/some-story-name.123456/ "+cls.getURLPrefix()+"/posts/123456/"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?[^#]*?(#post-(?P<post>\d+))?$"
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?[^#]*?(#post-(?P<anchorpost>\d+))?$"
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
@@ -150,7 +150,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
raise
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
topsoup = soup = self.make_soup(data)
|
||||
|
||||
a = soup.find('h3',{'class':'userText'}).find('a')
|
||||
self.story.addToList('authorId',a['href'].split('/')[1])
|
||||
@@ -160,6 +160,9 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
h1 = soup.find('div',{'class':'titleBar'}).h1
|
||||
self.story.setMetadata('title',stripHTML(h1))
|
||||
|
||||
first_post_title = self.getConfig('first_post_title','First Post')
|
||||
|
||||
threadmark_chaps = False
|
||||
if '#' in useurl:
|
||||
anchorid = useurl.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
@@ -176,7 +179,11 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
## SV changed their threadmarks. Not isolated to
|
||||
## SV only incase SB or QQ make the same change.
|
||||
markas = soupmarks.find('div',{'class':'threadmarks'}).find_all('a',{'class':'PreviewTooltip'})
|
||||
if len(markas) > 1:
|
||||
if len(markas) >= int(self.getConfig('minimum_threadmarks',2)):
|
||||
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'):
|
||||
@@ -186,16 +193,16 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
|
||||
self.chapterUrls.append((name,self.getURLPrefix()+'/'+url))
|
||||
|
||||
## only use tags if threadmarks for chapters.
|
||||
## a bit arbitrary, but likely.
|
||||
for tag in soup.findAll('a',{'class':'tag'}):
|
||||
tstr = stripHTML(tag)
|
||||
if self.getConfig('capitalize_forumtags'):
|
||||
tstr = tstr.title()
|
||||
self.story.addToList('forumtags',tstr)
|
||||
|
||||
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 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.
|
||||
|
||||
@@ -211,8 +218,9 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
|
||||
# otherwise, use first post links--include first post since
|
||||
# that's often also the first chapter.
|
||||
|
||||
if not self.chapterUrls:
|
||||
self.chapterUrls.append(("First Post",useurl))
|
||||
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'):
|
||||
@@ -220,19 +228,26 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
|
||||
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):
|
||||
url.startswith('https://'+self.getSiteDomain()) ) and \
|
||||
( '/posts/' in url or '/threads/' in url or 'showpost.php' in url or 'goto/post' in url):
|
||||
|
||||
# brute force way to deal with SB's http->https change when hardcoded http urls.
|
||||
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
|
||||
|
||||
# http://forums.spacebattles.com/showpost.php?p=4755532&postcount=9
|
||||
url = re.sub(r'showpost\.php\?p=([0-9]+)(&postcount=[0-9]+)?',r'/posts/\1/',url)
|
||||
|
||||
# http://forums.spacebattles.com/goto/post?id=15222406#post-15222406
|
||||
url = re.sub(r'/goto/post\?id=([0-9]+)(#post-[0-9]+)?',r'/posts/\1/',url)
|
||||
|
||||
url = re.sub(r'(^[\'"]+|[\'"]+$)','',url) # strip leading or trailing '" from incorrect quoting.
|
||||
url = re.sub(r'like$','',url) # strip 'like' if incorrect 'like' link instead of proper post URL.
|
||||
|
||||
logger.debug("(ch:%s)used chapurl:%s"%(len(self.chapterUrls)+1,url))
|
||||
self.chapterUrls.append((name,url))
|
||||
if url == useurl and 'First Post' == self.chapterUrls[0][0]:
|
||||
if url == useurl and first_post_title == self.chapterUrls[0][0] \
|
||||
and not self.getConfig('always_include_first_post',False):
|
||||
# remove "First Post" if included in list.
|
||||
logger.debug("delete dup 'First Post' chapter: %s %s"%self.chapterUrls[0])
|
||||
del self.chapterUrls[0]
|
||||
|
||||
# Didn't use threadmarks, so take created/updated dates
|
||||
@@ -264,7 +279,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
datestr = re.sub(r' (\d[^\d])',r' 0\1',datestr) # add leading 0 for single digit day & hours.
|
||||
return makeDate(datestr, self.dateformat)
|
||||
except:
|
||||
logger.debug('No date found in %s'%parenttag)
|
||||
logger.debug('No date found in %s'%parenttag,exc_info=True)
|
||||
return None
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
|
||||
+242
-159
@@ -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.
|
||||
@@ -26,12 +26,14 @@ import pprint
|
||||
import string
|
||||
import sys
|
||||
|
||||
version="2.4.0"
|
||||
|
||||
if sys.version_info < (2, 5):
|
||||
print 'This program requires Python 2.5 or newer.'
|
||||
sys.exit(1)
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
# suppresses default logger. Logging is setup in fanficdownload/__init__.py so it works in calibre, too.
|
||||
# suppresses default logger. Logging is setup in fanficfare/__init__.py so it works in calibre, too.
|
||||
rootlogger = logging.getLogger()
|
||||
loghandler = logging.NullHandler()
|
||||
loghandler.setFormatter(logging.Formatter('(=====)(levelname)s:%(message)s'))
|
||||
@@ -39,17 +41,17 @@ if sys.version_info >= (2, 7):
|
||||
|
||||
try:
|
||||
# running under calibre
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficfare import adapters, writers, exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficfare.configurable import Configuration
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficfare.epubutils import (
|
||||
from calibre_plugins.fanficfare_plugin.fanficfare import adapters, writers, exceptions
|
||||
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.fanfictiondownloader_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,84 @@ 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.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 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 +254,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,11 +269,137 @@ def do_download(arg,
|
||||
url = arg
|
||||
else:
|
||||
url = arg
|
||||
|
||||
|
||||
configuration = get_configuration(url,
|
||||
passed_defaultsini,
|
||||
passed_personalini,
|
||||
options,
|
||||
chaptercount)
|
||||
|
||||
try:
|
||||
adapter = adapters.getAdapter(configuration, url)
|
||||
|
||||
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)
|
||||
if options.update and not chaptercount:
|
||||
try:
|
||||
writer = writers.getWriter('epub', configuration, adapter)
|
||||
output_filename = writer.getOutputFileName()
|
||||
noturl, chaptercount = get_dcsource_chaptercount(output_filename)
|
||||
print 'Updating %s, URL: %s' % (output_filename, url)
|
||||
except Exception:
|
||||
options.update = False
|
||||
pass
|
||||
|
||||
# Check for include_images without no_image_processing. In absence of PIL, give warning.
|
||||
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:
|
||||
## Pillow is a more current fork of PIL library
|
||||
from PIL import Image
|
||||
logging.debug('Using Pillow')
|
||||
except ImportError:
|
||||
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
|
||||
for x in range(0, 2):
|
||||
try:
|
||||
adapter.getStoryMetadataOnly()
|
||||
except exceptions.FailedToLogin, f:
|
||||
if f.passwdonly:
|
||||
print 'Story requires a password.'
|
||||
else:
|
||||
print 'Login Failed, Need Username/Password.'
|
||||
sys.stdout.write('Username: ')
|
||||
adapter.username = sys.stdin.readline().strip()
|
||||
adapter.password = getpass.getpass(prompt='Password: ')
|
||||
# print('Login: `%s`, Password: `%s`' % (adapter.username, adapter.password))
|
||||
except exceptions.AdultCheckRequired:
|
||||
print 'Please confirm you are an adult in your locale: (y/n)?'
|
||||
if sys.stdin.readline().strip().lower().startswith('y'):
|
||||
adapter.is_adult = True
|
||||
|
||||
if options.update and not options.force:
|
||||
urlchaptercount = int(adapter.getStoryMetadataOnly().getMetadata('numChapters'))
|
||||
|
||||
if chaptercount == urlchaptercount and not options.metaonly:
|
||||
print '%s already contains %d chapters.' % (output_filename, chaptercount)
|
||||
elif chaptercount > urlchaptercount:
|
||||
print '%s contains %d chapters, more than source: %d.' % (output_filename, chaptercount, urlchaptercount)
|
||||
elif chaptercount == 0:
|
||||
print "%s doesn't contain any recognizable chapters, probably from a different source. Not updating." % output_filename
|
||||
else:
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile,
|
||||
adapter.oldchaptersmap,
|
||||
adapter.oldchaptersdata) = (get_update_data(output_filename))[0:9]
|
||||
|
||||
print 'Do update - epub(%d) vs url(%d)' % (chaptercount, urlchaptercount)
|
||||
|
||||
if not options.update and chaptercount == urlchaptercount and adapter.getConfig('do_update_hook'):
|
||||
adapter.hookForUpdates(chaptercount)
|
||||
|
||||
write_story(configuration, adapter, 'epub')
|
||||
|
||||
else:
|
||||
# regular download
|
||||
if options.metaonly:
|
||||
pprint.pprint(adapter.getStoryMetadataOnly().getAllMetadata())
|
||||
pprint.pprint(adapter.chapterUrls)
|
||||
|
||||
output_filename = write_story(configuration, adapter, options.format, options.metaonly)
|
||||
|
||||
if not options.metaonly and adapter.getConfig('post_process_cmd'):
|
||||
metadata = adapter.story.metadata
|
||||
metadata['output_filename'] = output_filename
|
||||
call(string.Template(adapter.getConfig('post_process_cmd')).substitute(metadata), shell=True)
|
||||
|
||||
del adapter
|
||||
|
||||
except exceptions.InvalidStoryURL as isu:
|
||||
print isu
|
||||
except exceptions.StoryDoesNotExist as dne:
|
||||
print dne
|
||||
except exceptions.UnknownSite as us:
|
||||
print us
|
||||
except exceptions.AccessDenied as ad:
|
||||
print ad
|
||||
|
||||
def get_configuration(url,
|
||||
passed_defaultsini,
|
||||
passed_personalini,
|
||||
options,
|
||||
chaptercount=None):
|
||||
try:
|
||||
configuration = Configuration(adapters.getConfigSectionsFor(url), options.format)
|
||||
except exceptions.UnknownSite, e:
|
||||
if options.list or options.normalize:
|
||||
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:
|
||||
@@ -258,122 +456,7 @@ def do_download(arg,
|
||||
(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
|
||||
|
||||
try:
|
||||
adapter = adapters.getAdapter(configuration, url)
|
||||
|
||||
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)
|
||||
if options.update and not chaptercount:
|
||||
try:
|
||||
writer = writers.getWriter('epub', configuration, adapter)
|
||||
output_filename = writer.getOutputFileName()
|
||||
noturl, chaptercount = get_dcsource_chaptercount(output_filename)
|
||||
print 'Updating %s, URL: %s' % (output_filename, url)
|
||||
except Exception:
|
||||
options.update = False
|
||||
pass
|
||||
|
||||
# Check for include_images without no_image_processing. In absence of PIL, give warning.
|
||||
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')
|
||||
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
|
||||
for x in range(0, 2):
|
||||
try:
|
||||
adapter.getStoryMetadataOnly()
|
||||
except exceptions.FailedToLogin, f:
|
||||
if f.passwdonly:
|
||||
print 'Story requires a password.'
|
||||
else:
|
||||
print 'Login Failed, Need Username/Password.'
|
||||
sys.stdout.write('Username: ')
|
||||
adapter.username = sys.stdin.readline().strip()
|
||||
adapter.password = getpass.getpass(prompt='Password: ')
|
||||
# print('Login: `%s`, Password: `%s`' % (adapter.username, adapter.password))
|
||||
except exceptions.AdultCheckRequired:
|
||||
print 'Please confirm you are an adult in your locale: (y/n)?'
|
||||
if sys.stdin.readline().strip().lower().startswith('y'):
|
||||
adapter.is_adult = True
|
||||
|
||||
if options.update and not options.force:
|
||||
urlchaptercount = int(adapter.getStoryMetadataOnly().getMetadata('numChapters'))
|
||||
|
||||
if chaptercount == urlchaptercount and not options.metaonly:
|
||||
print '%s already contains %d chapters.' % (output_filename, chaptercount)
|
||||
elif chaptercount > urlchaptercount:
|
||||
print '%s contains %d chapters, more than source: %d.' % (output_filename, chaptercount, urlchaptercount)
|
||||
elif chaptercount == 0:
|
||||
print "%s doesn't contain any recognizable chapters, probably from a different source. Not updating." % output_filename
|
||||
else:
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile,
|
||||
adapter.oldchaptersmap,
|
||||
adapter.oldchaptersdata) = (get_update_data(output_filename))[0:9]
|
||||
|
||||
print 'Do update - epub(%d) vs url(%d)' % (chaptercount, urlchaptercount)
|
||||
|
||||
if not options.update and chaptercount == urlchaptercount and adapter.getConfig('do_update_hook'):
|
||||
adapter.hookForUpdates(chaptercount)
|
||||
|
||||
write_story(configuration, adapter, 'epub')
|
||||
|
||||
else:
|
||||
# regular download
|
||||
if options.metaonly:
|
||||
pprint.pprint(adapter.getStoryMetadataOnly().getAllMetadata())
|
||||
|
||||
output_filename = write_story(configuration, adapter, options.format, options.metaonly)
|
||||
|
||||
if not options.metaonly and adapter.getConfig('post_process_cmd'):
|
||||
metadata = adapter.story.metadata
|
||||
metadata['output_filename'] = output_filename
|
||||
call(string.Template(adapter.getConfig('post_process_cmd')).substitute(metadata), shell=True)
|
||||
|
||||
del adapter
|
||||
|
||||
except exceptions.InvalidStoryURL as isu:
|
||||
print isu
|
||||
except exceptions.StoryDoesNotExist as dne:
|
||||
print dne
|
||||
except exceptions.UnknownSite as us:
|
||||
print us
|
||||
except exceptions.AccessDenied as ad:
|
||||
print ad
|
||||
|
||||
return configuration
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+74
-27
@@ -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.
|
||||
@@ -111,8 +111,6 @@ def get_valid_list_entries():
|
||||
'authorId',
|
||||
'authorUrl',
|
||||
'lastupdate',
|
||||
'keep_html_attrs',
|
||||
'replace_tags_with_spans',
|
||||
])
|
||||
|
||||
boollist=['true','false']
|
||||
@@ -125,6 +123,10 @@ def get_valid_set_options():
|
||||
'''
|
||||
dict() of names of boolean options, but as a tuple with
|
||||
valid sites, valid formats and valid values (None==all)
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
valdict = {'collect_series':(None,None,boollist),
|
||||
@@ -158,18 +160,23 @@ def get_valid_set_options():
|
||||
|
||||
'force_login':(['phoenixsong.net'],None,boollist),
|
||||
'non_breaking_spaces':(['fictionmania.tv'],None,boollist),
|
||||
'universe_as_series':(['storiesonline.net'],None,boollist),
|
||||
'universe_as_series':(['storiesonline.net','finestories.com'],None,boollist),
|
||||
'strip_text_links':(['bloodshedverse.com'],None,boollist),
|
||||
'centeredcat_to_characters':(['tthfanfic.org'],None,boollist),
|
||||
'pairingcat_to_characters_ships':(['tthfanfic.org'],None,boollist),
|
||||
'romancecat_to_characters_ships':(['tthfanfic.org'],None,boollist),
|
||||
|
||||
'use_meta_keywords':(['literotica.com'],None,boollist),
|
||||
'clean_chapter_titles':(['literotica.com'],None,boollist),
|
||||
'description_in_chapter':(['literotica.com'],None,boollist),
|
||||
|
||||
# eFiction Base adapters allow bulk_load
|
||||
# kept forgetting to add them, so now it's automatic.
|
||||
'bulk_load':(adapters.get_bulk_load_sites(),
|
||||
None,boollist),
|
||||
|
||||
'include_logpage':(None,['epub'],boollist+['smart']),
|
||||
'logpage_at_end':(None,['epub'],boollist),
|
||||
|
||||
'windows_eol':(None,['txt'],boollist),
|
||||
|
||||
@@ -177,8 +184,12 @@ def get_valid_set_options():
|
||||
'grayscale_images':(None,['epub','html'],boollist),
|
||||
'no_image_processing':(None,['epub','html'],boollist),
|
||||
|
||||
'capitalize_forumtags':(base_xenforo_list,None,boollist),
|
||||
'continue_on_chapter_error':(base_xenforo_list,None,boollist),
|
||||
'':(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)
|
||||
@@ -273,6 +284,7 @@ def get_valid_keywords():
|
||||
'image_max_size',
|
||||
'include_images',
|
||||
'include_logpage',
|
||||
'logpage_at_end',
|
||||
'include_subject_tags',
|
||||
'include_titlepage',
|
||||
'include_tocpage',
|
||||
@@ -318,6 +330,9 @@ def get_valid_keywords():
|
||||
'centeredcat_to_characters',
|
||||
'pairingcat_to_characters_ships',
|
||||
'romancecat_to_characters_ships',
|
||||
'use_meta_keywords',
|
||||
'clean_chapter_titles',
|
||||
'description_in_chapter',
|
||||
'titlepage_end',
|
||||
'titlepage_entries',
|
||||
'titlepage_entry',
|
||||
@@ -339,8 +354,12 @@ def get_valid_keywords():
|
||||
'wrap_width',
|
||||
'zip_filename',
|
||||
'zip_output',
|
||||
'continue_on_chapter_error',
|
||||
'capitalize_forumtags',
|
||||
'continue_on_chapter_error',
|
||||
'minimum_threadmarks',
|
||||
'first_post_title',
|
||||
'always_include_first_post',
|
||||
'',
|
||||
])
|
||||
|
||||
# *known* entry keywords -- or rather regexps for them.
|
||||
@@ -365,10 +384,12 @@ def make_generate_cover_settings(param):
|
||||
|
||||
class Configuration(ConfigParser.SafeConfigParser):
|
||||
|
||||
def __init__(self, sections, fileform):
|
||||
def __init__(self, sections, fileform, lightweight=False):
|
||||
site = sections[-1] # first section is site DN.
|
||||
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]
|
||||
@@ -403,8 +424,25 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
|
||||
self.validEntries = get_valid_entries()
|
||||
|
||||
def addConfigSection(self,section):
|
||||
self.sectionslist.insert(0,section)
|
||||
self.url_config_set = False
|
||||
|
||||
def addUrlConfigSection(self,url):
|
||||
if not self.lightweight: # don't need when just checking for normalized URL.
|
||||
# replace if already set once.
|
||||
if self.url_config_set:
|
||||
self.sectionslist[self.sectionslist.index('overrides')+1]=url
|
||||
else:
|
||||
self.addConfigSection(url,'overrides')
|
||||
self.url_config_set=True
|
||||
|
||||
def addConfigSection(self,section,before=None):
|
||||
if section not in self.sectionslist: # don't add if already present.
|
||||
if before is None:
|
||||
self.sectionslist.insert(0,section)
|
||||
else:
|
||||
## because sectionslist is hi-pri first, lo-pri last,
|
||||
## 'before' means after in the list.
|
||||
self.sectionslist.insert(self.sectionslist.index(before)+1,section)
|
||||
|
||||
def isListType(self,key):
|
||||
return key in self.listTypeEntries or self.hasConfig("include_in_"+key)
|
||||
@@ -604,7 +642,10 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
def test_config(self):
|
||||
errors=[]
|
||||
|
||||
teststory_re = re.compile(r'^teststory:(defaults|[0-9]+)$')
|
||||
## too complicated right now to enforce
|
||||
## get_valid_set_options() warnings on teststory and
|
||||
## [storyUrl] sections.
|
||||
allow_all_sections_re = re.compile(r'^(teststory:(defaults|[0-9]+)|https?://.*)$')
|
||||
allowedsections = get_valid_sections()
|
||||
|
||||
clude_metadata_re = re.compile(r'(add_to_)?(in|ex)clude_metadata_(pre|post)')
|
||||
@@ -614,12 +655,13 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
|
||||
custom_columns_settings_re = re.compile(r'(add_to_)?custom_columns_settings')
|
||||
|
||||
generate_cover_settings_re = re.compile(r'(add_to_)?generate_cover_settings')
|
||||
|
||||
generate_cover_settings_re = re.compile(r'(add_to_)?generate_cover_settings')
|
||||
|
||||
valdict = get_valid_set_options()
|
||||
|
||||
for section in self.sections():
|
||||
if section not in allowedsections and not teststory_re.match(section):
|
||||
allow_all_section = allow_all_sections_re.match(section)
|
||||
if section not in allowedsections and not allow_all_section:
|
||||
errors.append((self.get_lineno(section),"Bad Section Name: [%s]"%section))
|
||||
else:
|
||||
sitename = section.replace('www.','')
|
||||
@@ -657,26 +699,25 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
# timeline=>#ccolumn,n
|
||||
# "FanFiction"=>#collection
|
||||
|
||||
def make_sections(x):
|
||||
return '['+'], ['.join(x)+']'
|
||||
if keyword in valdict:
|
||||
(valsites,valformats,vals)=valdict[keyword]
|
||||
if valsites != None and sitename != None and sitename not in valsites:
|
||||
errors.append((self.get_lineno(section,keyword),"%s not valid in section [%s] -- only valid in %s sections."%(keyword,section,make_sections(valsites))))
|
||||
if valformats != None and formatname != None and formatname not in valformats:
|
||||
errors.append((self.get_lineno(section,keyword),"%s not valid in section [%s] -- only valid in %s sections."%(keyword,section,make_sections(valformats))))
|
||||
if value not in vals:
|
||||
errors.append((self.get_lineno(section,keyword),"%s not a valid value for %s"%(value,keyword)))
|
||||
if not allow_all_section:
|
||||
def make_sections(x):
|
||||
return '['+'], ['.join(x)+']'
|
||||
if keyword in valdict:
|
||||
(valsites,valformats,vals)=valdict[keyword]
|
||||
if valsites != None and sitename != None and sitename not in valsites:
|
||||
errors.append((self.get_lineno(section,keyword),"%s not valid in section [%s] -- only valid in %s sections."%(keyword,section,make_sections(valsites))))
|
||||
if valformats != None and formatname != None and formatname not in valformats:
|
||||
errors.append((self.get_lineno(section,keyword),"%s not valid in section [%s] -- only valid in %s sections."%(keyword,section,make_sections(valformats))))
|
||||
if vals != None and value not in vals:
|
||||
errors.append((self.get_lineno(section,keyword),"%s not a valid value for %s"%(value,keyword)))
|
||||
|
||||
|
||||
## skipping output_filename_safepattern
|
||||
## regex--not used with plugin and this isn't
|
||||
## 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
|
||||
|
||||
# extended by adapter, writer and story for ease of calling configuration.
|
||||
@@ -685,6 +726,12 @@ class Configurable(object):
|
||||
def __init__(self, configuration):
|
||||
self.configuration = configuration
|
||||
|
||||
def is_lightweight(self):
|
||||
return self.configuration.lightweight
|
||||
|
||||
def addUrlConfigSection(self,url):
|
||||
self.configuration.addUrlConfigSection(url)
|
||||
|
||||
def isListType(self,key):
|
||||
return self.configuration.isListType(key)
|
||||
|
||||
|
||||
+316
-107
@@ -1,4 +1,4 @@
|
||||
# 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.
|
||||
@@ -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
|
||||
@@ -404,6 +404,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'
|
||||
@@ -473,8 +507,8 @@ add_to_include_subject_tags:,tagsfromtitle.SPLIT,forumtags
|
||||
## base_xenforoforum reads Published and Updated datetimes from
|
||||
## Threadmarks if used, or from the posted & updated times of the
|
||||
## 'first' post if no threadmarks.
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M
|
||||
|
||||
## Only take the first X characters of the 'first' post to use as
|
||||
## the description.
|
||||
@@ -487,6 +521,47 @@ description_limit:500
|
||||
## in the ebook for that chapter.
|
||||
continue_on_chapter_error:false
|
||||
|
||||
## When given a thread URL, use threadmarks as chapter links when
|
||||
## there are at least this many threadmarks. A number of older
|
||||
## threads have a single threadmark to an 'index' post. Set to 1 to
|
||||
## use threadmarks whenever they exist.
|
||||
minimum_threadmarks:2
|
||||
|
||||
## When 'first post' (or post URL) is being added as a chapter, give
|
||||
## the chapter this title.
|
||||
first_post_title:First Post
|
||||
|
||||
## In normal operation, if given a post URL or a thread URL with less
|
||||
## than minimum_threadmarks, the given post or the first post of the
|
||||
## thread will be included as the first chapter (with chapter title
|
||||
## from first_post_title) unless that post is explicitly linked to in
|
||||
## the collected chapter list. First post is not included when using
|
||||
## thread marks.
|
||||
##
|
||||
## If always_include_first_post:true, then the given or first post
|
||||
## will be included as above even if it is a link in the post or even
|
||||
## 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.
|
||||
always_use_forumtags:false
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
|
||||
@@ -563,6 +638,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
|
||||
@@ -727,6 +807,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]
|
||||
@@ -826,18 +919,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
|
||||
@@ -848,6 +929,12 @@ extracategories:The Sentinel
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:Windows-1252,ISO-8859-1,auto
|
||||
|
||||
## dateUpdate doesn't usually have time, but it does on
|
||||
## bloodshedverse.com. See
|
||||
## http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
@@ -877,15 +964,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
|
||||
@@ -1112,13 +1190,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
|
||||
@@ -1176,7 +1261,7 @@ views_label:Views
|
||||
likes_label:Likes
|
||||
dislikes_label:Dislikes
|
||||
|
||||
[finestories.com]
|
||||
[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
|
||||
@@ -1184,11 +1269,56 @@ dislikes_label:Dislikes
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
# shows size as "10 KB", not word count
|
||||
extra_valid_entries:size
|
||||
[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
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
## finestories.com has started requiring login by email rather than
|
||||
## pen name.
|
||||
#username:youremail@yourdomain.dom
|
||||
#password:yourpassword
|
||||
|
||||
# don't show twitter icon.
|
||||
cover_exclusion_regexp:/res/css/bir.png
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,sitetags,notice,codes,score
|
||||
#extra_titlepage_entries:size,universeHTML,sitetags,notice,score
|
||||
include_in_codes:sitetags
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:sitetags
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
sitetags_label:Site Tags
|
||||
notice_label:Notice
|
||||
score_label:Score
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[forum.questionablequesting.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
## 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
|
||||
|
||||
[forums.spacebattles.com]
|
||||
## see [base_xenforoforum]
|
||||
@@ -1196,7 +1326,12 @@ cover_exclusion_regexp:/res/css/bir.png
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[grangerenchanted.com]
|
||||
[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
|
||||
|
||||
## 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
|
||||
@@ -1204,19 +1339,6 @@ cover_exclusion_regexp:/res/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.
|
||||
#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
|
||||
|
||||
[hlfiction.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Highlander
|
||||
@@ -1268,9 +1390,21 @@ crossoverfandom_label:Crossover Fandom
|
||||
extra_titlepage_entries:universe,crossoverfandom
|
||||
|
||||
[literotica.com]
|
||||
extra_valid_entries:eroticatags
|
||||
extra_valid_entries:eroticatags,averrating
|
||||
eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
averrating_label:Average Rating
|
||||
extra_titlepage_entries:eroticatags,averrating
|
||||
|
||||
## Extract more erotica_tags from the meta tag of each chapter
|
||||
use_meta_keywords: true
|
||||
|
||||
## For multiple chapter stories, attempt to clean up the chapter title. This will
|
||||
## remove the story title and change "Ch. 01" to "Chapter 1", "Pt. 01" to "Part 1"
|
||||
## or just use the text. If this can't be done, the full title is used.
|
||||
clean_chapter_titles: false
|
||||
|
||||
## Add the chapter description at the start of each chapter.
|
||||
description_in_chapter: false
|
||||
|
||||
[lotrfanfiction.com]
|
||||
extra_valid_entries: readings
|
||||
@@ -1317,6 +1451,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
|
||||
@@ -1418,15 +1565,16 @@ 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
|
||||
|
||||
[samandjack.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
@@ -1452,22 +1600,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
|
||||
@@ -1609,15 +1741,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
|
||||
@@ -1685,6 +1808,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,
|
||||
@@ -1770,6 +1899,10 @@ check_next_chapter:false
|
||||
#password:yourpassword
|
||||
|
||||
[www.ficbook.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.fictionalley.org]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
@@ -1891,6 +2024,9 @@ extracategories:Harry Potter
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
extra_valid_entries:reads,reviews
|
||||
reads_label:Total Read Count
|
||||
|
||||
[www.hpfanficarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -1972,11 +2108,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
|
||||
@@ -2031,6 +2162,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
|
||||
@@ -2107,23 +2240,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.
|
||||
@@ -2158,6 +2277,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
|
||||
@@ -2209,6 +2344,80 @@ extracategories:Stargate: Atlantis
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
[buffygiles.velocitygrass.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
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Buffy,Giles
|
||||
|
||||
[fanfiction.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
|
||||
|
||||
## 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.andromeda-web.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
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Andromeda
|
||||
|
||||
[www.artemis-fowl.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
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Artemis Fowl
|
||||
|
||||
[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,
|
||||
## 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
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
+28
-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.
|
||||
@@ -85,7 +95,6 @@ def get_update_data(inputio,
|
||||
except Exception as e:
|
||||
logger.warn("Cover Image %s not found"%src)
|
||||
logger.warn("Exception: %s"%(unicode(e)))
|
||||
traceback.print_exc()
|
||||
|
||||
filecount = 0
|
||||
soups = [] # list of xhmtl blocks
|
||||
@@ -127,8 +136,7 @@ def get_update_data(inputio,
|
||||
# originally.
|
||||
if newsrc != u'OEBPS/failedtoload':
|
||||
logger.warn("Image %s not found!\n(originally:%s)"%(newsrc,longdesc))
|
||||
logger.warn("Exception: %s"%(unicode(e)))
|
||||
traceback.print_exc()
|
||||
logger.warn("Exception: %s"%(unicode(e)),exc_info=True)
|
||||
bodysoup = soup.find('body')
|
||||
# ffdl epubs have chapter title h3
|
||||
h3 = bodysoup.find('h3')
|
||||
|
||||
@@ -35,7 +35,7 @@ from exceptions import UnknownSite
|
||||
def get_urls_from_page(url,configuration=None,normalize=False):
|
||||
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
configuration = Configuration(["test1.com"],"EPUB",lightweight=True)
|
||||
|
||||
data = None
|
||||
adapter = None
|
||||
@@ -84,7 +84,7 @@ def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrict
|
||||
urls = collections.OrderedDict()
|
||||
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
configuration = Configuration(["test1.com"],"EPUB",lightweight=True)
|
||||
|
||||
soup = BeautifulSoup(data,"html5lib")
|
||||
if restrictsearch:
|
||||
@@ -125,10 +125,13 @@ 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")
|
||||
configuration = Configuration(["test1.com"],"EPUB",lightweight=True)
|
||||
|
||||
for href in re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', data):
|
||||
# this (should) catch normal story links, some javascript
|
||||
@@ -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:
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""html2text: Turn HTML into equivalent Markdown-structured text."""
|
||||
__version__ = "2.37"
|
||||
__author__ = "Aaron Swartz (me@aaronsw.com)"
|
||||
__copyright__ = "(C) 2004-2008 Aaron Swartz. GNU GPL 3."
|
||||
__contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
|
||||
|
||||
# TODO:
|
||||
# Support decoded entities with unifiable.
|
||||
|
||||
if not hasattr(__builtins__, 'True'): True, False = 1, 0
|
||||
import re, sys, urllib, htmlentitydefs, codecs, StringIO, types
|
||||
import sgmllib
|
||||
import urlparse
|
||||
sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]')
|
||||
|
||||
try: from textwrap import wrap
|
||||
except: pass
|
||||
|
||||
# Use Unicode characters instead of their ascii psuedo-replacements
|
||||
UNICODE_SNOB = 0
|
||||
|
||||
# Put the links after each paragraph instead of at the end.
|
||||
LINKS_EACH_PARAGRAPH = 0
|
||||
|
||||
# Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.)
|
||||
BODY_WIDTH = 78
|
||||
|
||||
# Don't show internal links (href="#local-anchor") -- corresponding link targets
|
||||
# won't be visible in the plain text file anyway.
|
||||
SKIP_INTERNAL_LINKS = False
|
||||
|
||||
### Entity Nonsense ###
|
||||
|
||||
def name2cp(k):
|
||||
if k == 'apos': return ord("'")
|
||||
if hasattr(htmlentitydefs, "name2codepoint"): # requires Python 2.3
|
||||
return htmlentitydefs.name2codepoint[k]
|
||||
else:
|
||||
k = htmlentitydefs.entitydefs[k]
|
||||
if k.startswith("&#") and k.endswith(";"): return int(k[2:-1]) # not in latin-1
|
||||
return ord(codecs.latin_1_decode(k)[0])
|
||||
|
||||
unifiable = {'rsquo':"'", 'lsquo':"'", 'rdquo':'"', 'ldquo':'"',
|
||||
'copy':'(C)', 'mdash':'--', 'nbsp':' ', 'rarr':'->', 'larr':'<-', 'middot':'*',
|
||||
'ndash':'-', 'oelig':'oe', 'aelig':'ae',
|
||||
'agrave':'a', 'aacute':'a', 'acirc':'a', 'atilde':'a', 'auml':'a', 'aring':'a',
|
||||
'egrave':'e', 'eacute':'e', 'ecirc':'e', 'euml':'e',
|
||||
'igrave':'i', 'iacute':'i', 'icirc':'i', 'iuml':'i',
|
||||
'ograve':'o', 'oacute':'o', 'ocirc':'o', 'otilde':'o', 'ouml':'o',
|
||||
'ugrave':'u', 'uacute':'u', 'ucirc':'u', 'uuml':'u'}
|
||||
|
||||
unifiable_n = {}
|
||||
|
||||
for k in unifiable.keys():
|
||||
unifiable_n[name2cp(k)] = unifiable[k]
|
||||
|
||||
def charref(name):
|
||||
if name[0] in ['x','X']:
|
||||
c = int(name[1:], 16)
|
||||
else:
|
||||
c = int(name)
|
||||
|
||||
if not UNICODE_SNOB and c in unifiable_n.keys():
|
||||
return unifiable_n[c]
|
||||
else:
|
||||
return unichr(c)
|
||||
|
||||
def entityref(c):
|
||||
if not UNICODE_SNOB and c in unifiable.keys():
|
||||
return unifiable[c]
|
||||
else:
|
||||
try: name2cp(c)
|
||||
except KeyError: return "&" + c
|
||||
else: return unichr(name2cp(c))
|
||||
|
||||
def replaceEntities(s):
|
||||
s = s.group(1)
|
||||
if s[0] == "#":
|
||||
return charref(s[1:])
|
||||
else: return entityref(s)
|
||||
|
||||
r_unescape = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
|
||||
def unescape(s):
|
||||
return r_unescape.sub(replaceEntities, s)
|
||||
|
||||
def fixattrs(attrs):
|
||||
# Fix bug in sgmllib.py
|
||||
if not attrs: return attrs
|
||||
newattrs = []
|
||||
for attr in attrs:
|
||||
newattrs.append((attr[0], unescape(attr[1])))
|
||||
return newattrs
|
||||
|
||||
### End Entity Nonsense ###
|
||||
|
||||
def onlywhite(line):
|
||||
"""Return true if the line does only consist of whitespace characters."""
|
||||
for c in line:
|
||||
if c is not ' ' and c is not ' ':
|
||||
return c is ' '
|
||||
return line
|
||||
|
||||
def optwrap(text,wrap_width=BODY_WIDTH):
|
||||
"""Wrap all paragraphs in the provided text."""
|
||||
|
||||
if not wrap_width:
|
||||
return text
|
||||
|
||||
assert wrap, "Requires Python 2.3."
|
||||
result = ''
|
||||
newlines = 0
|
||||
for para in text.split("\n"):
|
||||
if len(para) > 0:
|
||||
if para[0] is not ' ' and para[0] is not '-' and para[0] is not '*':
|
||||
for line in wrap(para, wrap_width):
|
||||
result += line + "\n"
|
||||
result += "\n"
|
||||
newlines = 2
|
||||
else:
|
||||
if not onlywhite(para):
|
||||
result += para + "\n"
|
||||
newlines = 1
|
||||
else:
|
||||
if newlines < 2:
|
||||
result += "\n"
|
||||
newlines += 1
|
||||
return result
|
||||
|
||||
def hn(tag):
|
||||
if tag[0] == 'h' and len(tag) == 2:
|
||||
try:
|
||||
n = int(tag[1])
|
||||
if n in range(1, 10): return n
|
||||
except ValueError: return 0
|
||||
|
||||
class _html2text(sgmllib.SGMLParser):
|
||||
def __init__(self, out=None, baseurl=''):
|
||||
sgmllib.SGMLParser.__init__(self)
|
||||
|
||||
if out is None: self.out = self.outtextf
|
||||
else: self.out = out
|
||||
self.outtext = u''
|
||||
self.quiet = 0
|
||||
self.p_p = 0
|
||||
self.outcount = 0
|
||||
self.start = 1
|
||||
self.space = 0
|
||||
self.a = []
|
||||
self.astack = []
|
||||
self.acount = 0
|
||||
self.list = []
|
||||
self.blockquote = 0
|
||||
self.pre = 0
|
||||
self.startpre = 0
|
||||
self.lastWasNL = 0
|
||||
self.abbr_title = None # current abbreviation definition
|
||||
self.abbr_data = None # last inner HTML (for abbr being defined)
|
||||
self.abbr_list = {} # stack of abbreviations to write later
|
||||
self.baseurl = baseurl
|
||||
|
||||
def outtextf(self, s):
|
||||
self.outtext += s
|
||||
|
||||
def close(self):
|
||||
sgmllib.SGMLParser.close(self)
|
||||
|
||||
self.pbr()
|
||||
self.o('', 0, 'end')
|
||||
|
||||
return self.outtext
|
||||
|
||||
def handle_charref(self, c):
|
||||
self.o(charref(c))
|
||||
|
||||
def handle_entityref(self, c):
|
||||
self.o(entityref(c))
|
||||
|
||||
def unknown_starttag(self, tag, attrs):
|
||||
self.handle_tag(tag, attrs, 1)
|
||||
|
||||
def unknown_endtag(self, tag):
|
||||
self.handle_tag(tag, None, 0)
|
||||
|
||||
def previousIndex(self, attrs):
|
||||
""" returns the index of certain set of attributes (of a link) in the
|
||||
self.a list
|
||||
|
||||
If the set of attributes is not found, returns None
|
||||
"""
|
||||
if not attrs.has_attr('href'): return None
|
||||
|
||||
i = -1
|
||||
for a in self.a:
|
||||
i += 1
|
||||
match = 0
|
||||
|
||||
if a.has_attr('href') and a['href'] == attrs['href']:
|
||||
if a.has_attr('title') or attrs.has_attr('title'):
|
||||
if (a.has_attr('title') and attrs.has_attr('title') and
|
||||
a['title'] == attrs['title']):
|
||||
match = True
|
||||
else:
|
||||
match = True
|
||||
|
||||
if match: return i
|
||||
|
||||
def handle_tag(self, tag, attrs, start):
|
||||
attrs = fixattrs(attrs)
|
||||
|
||||
if hn(tag):
|
||||
self.p()
|
||||
if start: self.o(hn(tag)*"#" + ' ')
|
||||
|
||||
if tag in ['p', 'div']: self.p()
|
||||
|
||||
if tag == "br" and start: self.o(" \n")
|
||||
|
||||
if tag == "hr" and start:
|
||||
self.p()
|
||||
self.o("* * *")
|
||||
self.p()
|
||||
|
||||
if tag in ["head", "style", 'script']:
|
||||
if start: self.quiet += 1
|
||||
else: self.quiet -= 1
|
||||
|
||||
if tag in ["body"]:
|
||||
self.quiet = 0 # sites like 9rules.com never close <head>
|
||||
|
||||
if tag == "blockquote":
|
||||
if start:
|
||||
self.p(); self.o('> ', 0, 1); self.start = 1
|
||||
self.blockquote += 1
|
||||
else:
|
||||
self.blockquote -= 1
|
||||
self.p()
|
||||
|
||||
if tag in ['em', 'i', 'u']: self.o("_")
|
||||
if tag in ['strong', 'b']: self.o("**")
|
||||
if tag == "code" and not self.pre: self.o('`') #TODO: `` `this` ``
|
||||
if tag == "abbr":
|
||||
if start:
|
||||
attrsD = {}
|
||||
for (x, y) in attrs: attrsD[x] = y
|
||||
attrs = attrsD
|
||||
|
||||
self.abbr_title = None
|
||||
self.abbr_data = ''
|
||||
if attrs.has_attr('title'):
|
||||
self.abbr_title = attrs['title']
|
||||
else:
|
||||
if self.abbr_title != None:
|
||||
self.abbr_list[self.abbr_data] = self.abbr_title
|
||||
self.abbr_title = None
|
||||
self.abbr_data = ''
|
||||
|
||||
if tag == "a":
|
||||
if start:
|
||||
attrsD = {}
|
||||
for (x, y) in attrs: attrsD[x] = y
|
||||
attrs = attrsD
|
||||
if attrs.has_attr('href') and not (SKIP_INTERNAL_LINKS and attrs['href'].startswith('#')):
|
||||
self.astack.append(attrs)
|
||||
self.o("[")
|
||||
else:
|
||||
self.astack.append(None)
|
||||
else:
|
||||
if self.astack:
|
||||
a = self.astack.pop()
|
||||
if a:
|
||||
i = self.previousIndex(a)
|
||||
if i is not None:
|
||||
a = self.a[i]
|
||||
else:
|
||||
self.acount += 1
|
||||
a['count'] = self.acount
|
||||
a['outcount'] = self.outcount
|
||||
self.a.append(a)
|
||||
self.o("][" + `a['count']` + "]")
|
||||
|
||||
if tag == "img" and start:
|
||||
attrsD = {}
|
||||
for (x, y) in attrs: attrsD[x] = y
|
||||
attrs = attrsD
|
||||
if attrs.has_attr('src'):
|
||||
attrs['href'] = attrs['src']
|
||||
alt = attrs.get('alt', '')
|
||||
i = self.previousIndex(attrs)
|
||||
if i is not None:
|
||||
attrs = self.a[i]
|
||||
else:
|
||||
self.acount += 1
|
||||
attrs['count'] = self.acount
|
||||
attrs['outcount'] = self.outcount
|
||||
self.a.append(attrs)
|
||||
self.o("![")
|
||||
self.o(alt)
|
||||
self.o("]["+`attrs['count']`+"]")
|
||||
|
||||
if tag == 'dl' and start: self.p()
|
||||
if tag == 'dt' and not start: self.pbr()
|
||||
if tag == 'dd' and start: self.o(' ')
|
||||
if tag == 'dd' and not start: self.pbr()
|
||||
|
||||
if tag in ["ol", "ul"]:
|
||||
if start:
|
||||
self.list.append({'name':tag, 'num':0})
|
||||
else:
|
||||
if self.list: self.list.pop()
|
||||
|
||||
self.p()
|
||||
|
||||
if tag == 'li':
|
||||
if start:
|
||||
self.pbr()
|
||||
if self.list: li = self.list[-1]
|
||||
else: li = {'name':'ul', 'num':0}
|
||||
self.o(" "*len(self.list)) #TODO: line up <ol><li>s > 9 correctly.
|
||||
if li['name'] == "ul": self.o("* ")
|
||||
elif li['name'] == "ol":
|
||||
li['num'] += 1
|
||||
self.o(`li['num']`+". ")
|
||||
self.start = 1
|
||||
else:
|
||||
self.pbr()
|
||||
|
||||
if tag in ["table", "tr"] and start: self.p()
|
||||
if tag == 'td': self.pbr()
|
||||
|
||||
if tag == "pre":
|
||||
if start:
|
||||
self.startpre = 1
|
||||
self.pre = 1
|
||||
else:
|
||||
self.pre = 0
|
||||
self.p()
|
||||
|
||||
def pbr(self):
|
||||
if self.p_p == 0: self.p_p = 1
|
||||
|
||||
def p(self): self.p_p = 2
|
||||
|
||||
def o(self, data, puredata=0, force=0):
|
||||
if self.abbr_data is not None: self.abbr_data += data
|
||||
|
||||
if not self.quiet:
|
||||
if puredata and not self.pre:
|
||||
data = re.sub('\s+', ' ', data)
|
||||
if data and data[0] == ' ':
|
||||
self.space = 1
|
||||
data = data[1:]
|
||||
if not data and not force: return
|
||||
|
||||
if self.startpre:
|
||||
#self.out(" :") #TODO: not output when already one there
|
||||
self.startpre = 0
|
||||
|
||||
bq = (">" * self.blockquote)
|
||||
if not (force and data and data[0] == ">") and self.blockquote: bq += " "
|
||||
|
||||
if self.pre:
|
||||
bq += " "
|
||||
data = data.replace("\n", "\n"+bq)
|
||||
|
||||
if self.start:
|
||||
self.space = 0
|
||||
self.p_p = 0
|
||||
self.start = 0
|
||||
|
||||
if force == 'end':
|
||||
# It's the end.
|
||||
self.p_p = 0
|
||||
self.out("\n")
|
||||
self.space = 0
|
||||
|
||||
|
||||
if self.p_p:
|
||||
self.out(('\n'+bq)*self.p_p)
|
||||
self.space = 0
|
||||
|
||||
if self.space:
|
||||
if not self.lastWasNL: self.out(' ')
|
||||
self.space = 0
|
||||
|
||||
if self.a and ((self.p_p == 2 and LINKS_EACH_PARAGRAPH) or force == "end"):
|
||||
if force == "end": self.out("\n")
|
||||
|
||||
newa = []
|
||||
for link in self.a:
|
||||
if self.outcount > link['outcount']:
|
||||
self.out(" ["+`link['count']`+"]: " + urlparse.urljoin(self.baseurl, link['href']))
|
||||
if link.has_attr('title'): self.out(" ("+link['title']+")")
|
||||
self.out("\n")
|
||||
else:
|
||||
newa.append(link)
|
||||
|
||||
if self.a != newa: self.out("\n") # Don't need an extra line when nothing was done.
|
||||
|
||||
self.a = newa
|
||||
|
||||
if self.abbr_list and force == "end":
|
||||
for abbr, definition in self.abbr_list.items():
|
||||
self.out(" *[" + abbr + "]: " + definition + "\n")
|
||||
|
||||
self.p_p = 0
|
||||
self.out(data)
|
||||
self.lastWasNL = data and data[-1] == '\n'
|
||||
self.outcount += 1
|
||||
|
||||
def handle_data(self, data):
|
||||
if r'\/script>' in data: self.quiet -= 1
|
||||
self.o(data, 1)
|
||||
|
||||
def unknown_decl(self, data): pass
|
||||
|
||||
def wrapwrite(text): sys.stdout.write(text.encode('utf8'))
|
||||
|
||||
def html2text_file(html, out=wrapwrite, baseurl=''):
|
||||
h = _html2text(out, baseurl)
|
||||
h.feed(html)
|
||||
h.feed("")
|
||||
return h.close()
|
||||
|
||||
def html2text(html, baseurl='', wrap_width=BODY_WIDTH):
|
||||
return optwrap(html2text_file(html, None, baseurl),wrap_width)
|
||||
|
||||
if __name__ == "__main__":
|
||||
baseurl = ''
|
||||
if sys.argv[1:]:
|
||||
arg = sys.argv[1]
|
||||
if arg.startswith('http://'):
|
||||
baseurl = arg
|
||||
j = urllib.urlopen(baseurl)
|
||||
try:
|
||||
from feedparser import _getCharacterEncoding as enc
|
||||
except ImportError:
|
||||
enc = lambda x, y: ('utf-8', 1)
|
||||
text = j.read()
|
||||
encoding = enc(j.headers, text)[0]
|
||||
if encoding == 'us-ascii': encoding = 'utf-8'
|
||||
data = text.decode(encoding)
|
||||
|
||||
else:
|
||||
encoding = 'utf8'
|
||||
if len(sys.argv) > 2:
|
||||
encoding = sys.argv[2]
|
||||
data = open(arg, 'r').read().decode(encoding)
|
||||
else:
|
||||
data = sys.stdin.read().decode('utf8')
|
||||
wrapwrite(html2text(data, baseurl))
|
||||
+85
-39
@@ -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.
|
||||
@@ -380,8 +380,8 @@ def set_in_ex_clude(setting):
|
||||
## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp]
|
||||
def make_replacements(replace):
|
||||
retval=[]
|
||||
for fullline in replace.splitlines():
|
||||
line=fullline
|
||||
for repl_line in replace.splitlines():
|
||||
line=repl_line
|
||||
try:
|
||||
(metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None)
|
||||
if "&&" in line:
|
||||
@@ -403,10 +403,10 @@ def make_replacements(replace):
|
||||
# replacement string. The .ini parser eats any
|
||||
# trailing spaces.
|
||||
replacement=replacement.replace(SPACE_REPLACE,' ')
|
||||
retval.append([metakeys,regexp,replacement,condkey,condregexp])
|
||||
retval.append([repl_line,metakeys,regexp,replacement,condkey,condregexp])
|
||||
except Exception as e:
|
||||
logger.error("Problem with Replacement Line:%s"%fullline)
|
||||
raise exceptions.PersonalIniFailed(e,'replace_metadata unpacking failed',fullline)
|
||||
logger.error("Problem with Replacement Line:%s"%repl_line)
|
||||
raise exceptions.PersonalIniFailed(e,'replace_metadata unpacking failed',repl_line)
|
||||
# raise
|
||||
return retval
|
||||
|
||||
@@ -419,38 +419,51 @@ class Story(Configurable):
|
||||
self.metadata = {'version':os.environ['CURRENT_VERSION_ID']}
|
||||
except:
|
||||
self.metadata = {'version':'4.4'}
|
||||
self.replacements = []
|
||||
self.in_ex_cludes = {}
|
||||
self.chapters = [] # chapters will be namedtuple of Chapter(url,title,html,etc)
|
||||
self.chapter_first = None
|
||||
self.chapter_last = None
|
||||
self.imgurls = []
|
||||
self.imgtuples = []
|
||||
# save processed metadata, dicts keyed by 'key', then (removeentities,dorepl)
|
||||
# {'key':{(removeentities,dorepl):"value",(...):"value"},'key':... }
|
||||
self.processed_metadata_cache = {}
|
||||
|
||||
self.cover=None # *href* of new cover image--need to create html.
|
||||
self.oldcover=None # (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata)
|
||||
self.calibrebookmark=None # cheesy way to carry calibre bookmark file forward across update.
|
||||
self.logfile=None # cheesy way to carry log file forward across update.
|
||||
|
||||
## Look for config parameter, split and add each to metadata field.
|
||||
for (config,metadata) in [("extracategories","category"),
|
||||
("extragenres","genre"),
|
||||
("extracharacters","characters"),
|
||||
("extraships","ships"),
|
||||
("extrawarnings","warnings")]:
|
||||
for val in self.getConfigList(config):
|
||||
self.addToList(metadata,val)
|
||||
self.replacements_prepped = False
|
||||
|
||||
self.replacements = make_replacements(self.getConfig('replace_metadata'))
|
||||
|
||||
in_ex_clude_list = ['include_metadata_pre','exclude_metadata_pre',
|
||||
'include_metadata_post','exclude_metadata_post']
|
||||
for ie in in_ex_clude_list:
|
||||
ies = self.getConfig(ie)
|
||||
# print("%s %s"%(ie,ies))
|
||||
if ies:
|
||||
iel = []
|
||||
self.in_ex_cludes[ie] = set_in_ex_clude(ies)
|
||||
def prepare_replacements(self):
|
||||
if not self.replacements_prepped and not self.is_lightweight():
|
||||
# logger.debug("prepare_replacements")
|
||||
# logger.debug("sections:%s"%self.configuration.sectionslist)
|
||||
|
||||
## Look for config parameter, split and add each to metadata field.
|
||||
for (config,metadata) in [("extracategories","category"),
|
||||
("extragenres","genre"),
|
||||
("extracharacters","characters"),
|
||||
("extraships","ships"),
|
||||
("extrawarnings","warnings")]:
|
||||
for val in self.getConfigList(config):
|
||||
self.addToList(metadata,val)
|
||||
|
||||
self.replacements = make_replacements(self.getConfig('replace_metadata'))
|
||||
|
||||
in_ex_clude_list = ['include_metadata_pre','exclude_metadata_pre',
|
||||
'include_metadata_post','exclude_metadata_post']
|
||||
for ie in in_ex_clude_list:
|
||||
ies = self.getConfig(ie)
|
||||
# print("%s %s"%(ie,ies))
|
||||
if ies:
|
||||
iel = []
|
||||
self.in_ex_cludes[ie] = set_in_ex_clude(ies)
|
||||
self.replacements_prepped = True
|
||||
|
||||
|
||||
def set_chapters_range(self,first=None,last=None):
|
||||
self.chapter_first=first
|
||||
self.chapter_last=last
|
||||
@@ -460,6 +473,9 @@ class Story(Configurable):
|
||||
|
||||
def setMetadata(self, key, value, condremoveentities=True):
|
||||
|
||||
# delete
|
||||
if key in self.processed_metadata_cache:
|
||||
del self.processed_metadata_cache[key]
|
||||
# keep as list type, but set as only value.
|
||||
if self.isList(key):
|
||||
self.addToList(key,value,condremoveentities=condremoveentities,clear=True)
|
||||
@@ -482,8 +498,18 @@ class Story(Configurable):
|
||||
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"),clear=True)
|
||||
self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d"))
|
||||
|
||||
if key == 'storyUrl' and value:
|
||||
self.addUrlConfigSection(value) # adapter/writer share the
|
||||
# same configuration.
|
||||
# ignored if config
|
||||
# is_lightweight()
|
||||
self.replacements_prepped = False
|
||||
|
||||
def do_in_ex_clude(self,which,value,key):
|
||||
# sets self.replacements and self.in_ex_cludes if needed
|
||||
# do_in_ex_clude is always called from doReplacements, so redundant.
|
||||
# self.prepare_replacements()
|
||||
|
||||
if value and which in self.in_ex_cludes:
|
||||
include = 'include' in which
|
||||
keyfound = False
|
||||
@@ -512,8 +538,10 @@ class Story(Configurable):
|
||||
value = None
|
||||
return value
|
||||
|
||||
|
||||
def doReplacements(self,value,key,return_list=False,seen_list=[]):
|
||||
# sets self.replacements and self.in_ex_cludes if needed
|
||||
self.prepare_replacements()
|
||||
|
||||
value = self.do_in_ex_clude('include_metadata_pre',value,key)
|
||||
value = self.do_in_ex_clude('exclude_metadata_pre',value,key)
|
||||
|
||||
@@ -523,7 +551,7 @@ class Story(Configurable):
|
||||
# print("bailing on %s"%replaceline)
|
||||
continue
|
||||
#print("replacement tuple:%s"%replaceline)
|
||||
(metakeys,regexp,replacement,condkey,condregexp) = replaceline
|
||||
(repl_line,metakeys,regexp,replacement,condkey,condregexp) = replaceline
|
||||
if (metakeys == None or key in metakeys) \
|
||||
and isinstance(value,basestring) \
|
||||
and regexp.search(value):
|
||||
@@ -541,21 +569,28 @@ class Story(Configurable):
|
||||
if SPLIT_META in replacement:
|
||||
retlist = []
|
||||
for splitrepl in replacement.split(SPLIT_META):
|
||||
retlist.extend(self.doReplacements(regexp.sub(splitrepl,value),
|
||||
try:
|
||||
tval = regexp.sub(splitrepl,value)
|
||||
except:
|
||||
logger.error("Exception with replacement line,value:(%s),(%s)"%(repl_line,value))
|
||||
raise
|
||||
retlist.extend(self.doReplacements(tval,
|
||||
key,
|
||||
return_list=True,
|
||||
seen_list=seen_list+[replaceline]))
|
||||
break
|
||||
else:
|
||||
# print("replacement,value:%s,%s->%s"%(replacement,value,regexp.sub(replacement,value)))
|
||||
value = regexp.sub(replacement,value)
|
||||
retlist = [value]
|
||||
try:
|
||||
value = regexp.sub(replacement,value)
|
||||
retlist = [value]
|
||||
except:
|
||||
logger.error("Exception with replacement line,value:(%s),(%s)"%(repl_line,value))
|
||||
raise
|
||||
|
||||
for val in retlist:
|
||||
retlist = map(partial(self.do_in_ex_clude,'include_metadata_post',key=key),retlist)
|
||||
retlist = map(partial(self.do_in_ex_clude,'exclude_metadata_post',key=key),retlist)
|
||||
# value = self.do_in_ex_clude('include_metadata_post',value,key)
|
||||
# value = self.do_in_ex_clude('exclude_metadata_post',value,key)
|
||||
|
||||
if return_list:
|
||||
return retlist
|
||||
@@ -636,13 +671,16 @@ class Story(Configurable):
|
||||
if not self.isValidMetaEntry(key):
|
||||
return value
|
||||
|
||||
if self.isList(key):
|
||||
# check for a cached value to speed processing
|
||||
if key in self.processed_metadata_cache \
|
||||
and (removeallentities,doreplacements) in self.processed_metadata_cache[key]:
|
||||
return self.processed_metadata_cache[key][(removeallentities,doreplacements)]
|
||||
elif self.isList(key):
|
||||
# join_string = self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ')
|
||||
# value = join_string.join(self.getList(key, removeallentities, doreplacements=True))
|
||||
value = self.join_list(key,self.getList(key, removeallentities, doreplacements=True))
|
||||
if doreplacements:
|
||||
value = self.doReplacements(value,key+"_LIST")
|
||||
return value
|
||||
elif self.metadata.has_key(key):
|
||||
value = self.metadata[key]
|
||||
if value:
|
||||
@@ -667,11 +705,16 @@ class Story(Configurable):
|
||||
if doreplacements:
|
||||
value=self.doReplacements(value,key)
|
||||
if removeallentities and value != None:
|
||||
return removeAllEntities(value)
|
||||
else:
|
||||
return value
|
||||
value = removeAllEntities(value)
|
||||
else: #if self.getConfig("default_value_"+key):
|
||||
return self.getConfig("default_value_"+key)
|
||||
value = self.getConfig("default_value_"+key)
|
||||
|
||||
# save a cached value to speed processing
|
||||
if key not in self.processed_metadata_cache:
|
||||
self.processed_metadata_cache[key] = {}
|
||||
self.processed_metadata_cache[key][(removeallentities,doreplacements)] = value
|
||||
|
||||
return value
|
||||
|
||||
def getAllMetadata(self,
|
||||
removeallentities=False,
|
||||
@@ -1024,9 +1067,12 @@ class Story(Configurable):
|
||||
|
||||
prefix='ffdl'
|
||||
if imgurl not in self.imgurls:
|
||||
parsedUrl = urlparse.urlparse(imgurl)
|
||||
|
||||
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,
|
||||
fetch(imgurl))
|
||||
@@ -1118,5 +1164,5 @@ def unique_list(seq):
|
||||
try:
|
||||
return [x for x in seq if not (x in seen or seen_add(x))]
|
||||
except:
|
||||
print("unique_list exception seq:%s"%seq)
|
||||
logger.debug("unique_list exception seq:%s"%seq)
|
||||
raise
|
||||
|
||||
@@ -46,7 +46,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 +117,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 +156,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 +184,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,7 +201,7 @@ 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:
|
||||
@@ -234,7 +234,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"))
|
||||
@@ -275,16 +275,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 +305,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 +332,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 +374,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 +387,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 +395,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 +415,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 +427,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 +441,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 +459,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 +467,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 +480,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 +495,13 @@ 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")
|
||||
|
||||
for index, chap in enumerate(self.story.getChapters(fortoc=True)):
|
||||
if chap.html:
|
||||
i=index+1
|
||||
@@ -512,6 +511,13 @@ div { margin: 0pt; padding: 0pt; }
|
||||
chap.title))
|
||||
itemrefs.append("file%04d"%i)
|
||||
|
||||
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)
|
||||
for item in items:
|
||||
@@ -520,7 +526,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 +536,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 +563,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 +592,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 +618,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 +630,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,12 +651,12 @@ 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:
|
||||
#logger.debug('Writing chapter text for: %s' % chap.title)
|
||||
|
||||
@@ -21,7 +21,7 @@ from textwrap import wrap
|
||||
|
||||
from base_writer import *
|
||||
|
||||
from ..html2text import html2text
|
||||
from html2text import html2text
|
||||
|
||||
## In BaseStoryWriter, we define _write to encode <unicode> objects
|
||||
## back into <string> for true output. But txt needs to write the
|
||||
@@ -109,7 +109,7 @@ End file.
|
||||
|
||||
self.wrap_width = self.getConfig('wrap_width')
|
||||
if self.wrap_width == '' or self.wrap_width == '0':
|
||||
self.wrap_width = None
|
||||
self.wrap_width = 0
|
||||
else:
|
||||
self.wrap_width = int(self.wrap_width)
|
||||
|
||||
@@ -159,7 +159,7 @@ End file.
|
||||
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,self.lineends(self.wraplines(removeAllEntities(CHAPTER_START.substitute(vals)))))
|
||||
self._write(out,self.lineends(html2text(chap.html,wrap_width=self.wrap_width)))
|
||||
self._write(out,self.lineends(html2text(chap.html,bodywidth=self.wrap_width)))
|
||||
self._write(out,self.lineends(self.wraplines(removeAllEntities(CHAPTER_END.substitute(vals)))))
|
||||
|
||||
self._write(out,self.lineends(self.wraplines(FILE_END.substitute(self.story.getAllMetadata()))))
|
||||
|
||||
@@ -0,0 +1,857 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
"""html2text: Turn HTML into equivalent Markdown-structured text."""
|
||||
from __future__ import division
|
||||
import re
|
||||
import sys
|
||||
import cgi
|
||||
|
||||
try:
|
||||
from textwrap import wrap
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
from html2text.compat import urlparse, HTMLParser
|
||||
from html2text import config
|
||||
|
||||
from html2text.utils import (
|
||||
name2cp,
|
||||
unifiable_n,
|
||||
google_text_emphasis,
|
||||
google_fixed_width_font,
|
||||
element_style,
|
||||
hn,
|
||||
google_has_height,
|
||||
escape_md,
|
||||
google_list_style,
|
||||
list_numbering_start,
|
||||
dumb_css_parser,
|
||||
escape_md_section,
|
||||
skipwrap
|
||||
)
|
||||
|
||||
__version__ = (2016, 4, 2)
|
||||
|
||||
|
||||
# TODO:
|
||||
# Support decoded entities with UNIFIABLE.
|
||||
|
||||
|
||||
class HTML2Text(HTMLParser.HTMLParser):
|
||||
def __init__(self, out=None, baseurl='', bodywidth=config.BODY_WIDTH):
|
||||
"""
|
||||
Input parameters:
|
||||
out: possible custom replacement for self.outtextf (which
|
||||
appends lines of text).
|
||||
baseurl: base URL of the document we process
|
||||
"""
|
||||
kwargs = {}
|
||||
if sys.version_info >= (3, 4):
|
||||
kwargs['convert_charrefs'] = False
|
||||
HTMLParser.HTMLParser.__init__(self, **kwargs)
|
||||
|
||||
# Config options
|
||||
self.split_next_td = False
|
||||
self.td_count = 0
|
||||
self.table_start = False
|
||||
self.unicode_snob = config.UNICODE_SNOB # covered in cli
|
||||
self.escape_snob = config.ESCAPE_SNOB # covered in cli
|
||||
self.links_each_paragraph = config.LINKS_EACH_PARAGRAPH
|
||||
self.body_width = bodywidth # covered in cli
|
||||
self.skip_internal_links = config.SKIP_INTERNAL_LINKS # covered in cli
|
||||
self.inline_links = config.INLINE_LINKS # covered in cli
|
||||
self.protect_links = config.PROTECT_LINKS # covered in cli
|
||||
self.google_list_indent = config.GOOGLE_LIST_INDENT # covered in cli
|
||||
self.ignore_links = config.IGNORE_ANCHORS # covered in cli
|
||||
self.ignore_images = config.IGNORE_IMAGES # covered in cli
|
||||
self.images_to_alt = config.IMAGES_TO_ALT # covered in cli
|
||||
self.images_with_size = config.IMAGES_WITH_SIZE # covered in cli
|
||||
self.ignore_emphasis = config.IGNORE_EMPHASIS # covered in cli
|
||||
self.bypass_tables = config.BYPASS_TABLES # covered in cli
|
||||
self.google_doc = False # covered in cli
|
||||
self.ul_item_mark = '*' # covered in cli
|
||||
self.emphasis_mark = '_' # covered in cli
|
||||
self.strong_mark = '**'
|
||||
self.single_line_break = config.SINGLE_LINE_BREAK # covered in cli
|
||||
self.use_automatic_links = config.USE_AUTOMATIC_LINKS # covered in cli
|
||||
self.hide_strikethrough = False # covered in cli
|
||||
self.mark_code = config.MARK_CODE
|
||||
self.wrap_links = config.WRAP_LINKS # covered in cli
|
||||
self.tag_callback = None
|
||||
|
||||
if out is None: # pragma: no cover
|
||||
self.out = self.outtextf
|
||||
else: # pragma: no cover
|
||||
self.out = out
|
||||
|
||||
# empty list to store output characters before they are "joined"
|
||||
self.outtextlist = []
|
||||
|
||||
self.quiet = 0
|
||||
self.p_p = 0 # number of newline character to print before next output
|
||||
self.outcount = 0
|
||||
self.start = 1
|
||||
self.space = 0
|
||||
self.a = []
|
||||
self.astack = []
|
||||
self.maybe_automatic_link = None
|
||||
self.empty_link = False
|
||||
self.absolute_url_matcher = re.compile(r'^[a-zA-Z+]+://')
|
||||
self.acount = 0
|
||||
self.list = []
|
||||
self.blockquote = 0
|
||||
self.pre = 0
|
||||
self.startpre = 0
|
||||
self.code = False
|
||||
self.br_toggle = ''
|
||||
self.lastWasNL = 0
|
||||
self.lastWasList = False
|
||||
self.style = 0
|
||||
self.style_def = {}
|
||||
self.tag_stack = []
|
||||
self.emphasis = 0
|
||||
self.drop_white_space = 0
|
||||
self.inheader = False
|
||||
self.abbr_title = None # current abbreviation definition
|
||||
self.abbr_data = None # last inner HTML (for abbr being defined)
|
||||
self.abbr_list = {} # stack of abbreviations to write later
|
||||
self.baseurl = baseurl
|
||||
|
||||
try:
|
||||
del unifiable_n[name2cp('nbsp')]
|
||||
except KeyError:
|
||||
pass
|
||||
config.UNIFIABLE['nbsp'] = ' _place_holder;'
|
||||
|
||||
def feed(self, data):
|
||||
data = data.replace("</' + 'script>", "</ignore>")
|
||||
HTMLParser.HTMLParser.feed(self, data)
|
||||
|
||||
def handle(self, data):
|
||||
self.feed(data)
|
||||
self.feed("")
|
||||
return self.optwrap(self.close())
|
||||
|
||||
def outtextf(self, s):
|
||||
self.outtextlist.append(s)
|
||||
if s:
|
||||
self.lastWasNL = s[-1] == '\n'
|
||||
|
||||
def close(self):
|
||||
HTMLParser.HTMLParser.close(self)
|
||||
|
||||
try:
|
||||
nochr = unicode('')
|
||||
except NameError:
|
||||
nochr = str('')
|
||||
|
||||
self.pbr()
|
||||
self.o('', 0, 'end')
|
||||
|
||||
outtext = nochr.join(self.outtextlist)
|
||||
if self.unicode_snob:
|
||||
try:
|
||||
nbsp = unichr(name2cp('nbsp'))
|
||||
except NameError:
|
||||
nbsp = chr(name2cp('nbsp'))
|
||||
else:
|
||||
try:
|
||||
nbsp = unichr(32)
|
||||
except NameError:
|
||||
nbsp = chr(32)
|
||||
try:
|
||||
outtext = outtext.replace(unicode(' _place_holder;'), nbsp)
|
||||
except NameError:
|
||||
outtext = outtext.replace(' _place_holder;', nbsp)
|
||||
|
||||
# Clear self.outtextlist to avoid memory leak of its content to
|
||||
# the next handling.
|
||||
self.outtextlist = []
|
||||
|
||||
return outtext
|
||||
|
||||
def handle_charref(self, c):
|
||||
charref = self.charref(c)
|
||||
if not self.code and not self.pre:
|
||||
charref = cgi.escape(charref)
|
||||
self.handle_data(charref, True)
|
||||
|
||||
def handle_entityref(self, c):
|
||||
entityref = self.entityref(c)
|
||||
if (not self.code and not self.pre
|
||||
and entityref != ' _place_holder;'):
|
||||
entityref = cgi.escape(entityref)
|
||||
self.handle_data(entityref, True)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self.handle_tag(tag, attrs, 1)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
self.handle_tag(tag, None, 0)
|
||||
|
||||
def previousIndex(self, attrs):
|
||||
"""
|
||||
:type attrs: dict
|
||||
|
||||
:returns: The index of certain set of attributes (of a link) in the
|
||||
self.a list. If the set of attributes is not found, returns None
|
||||
:rtype: int
|
||||
"""
|
||||
if 'href' not in attrs: # pragma: no cover
|
||||
return None
|
||||
i = -1
|
||||
for a in self.a:
|
||||
i += 1
|
||||
match = 0
|
||||
|
||||
if ('href' in a) and a['href'] == attrs['href']:
|
||||
if ('title' in a) or ('title' in attrs):
|
||||
if (('title' in a) and ('title' in attrs) and
|
||||
a['title'] == attrs['title']):
|
||||
match = True
|
||||
else:
|
||||
match = True
|
||||
|
||||
if match:
|
||||
return i
|
||||
|
||||
def handle_emphasis(self, start, tag_style, parent_style):
|
||||
"""
|
||||
Handles various text emphases
|
||||
"""
|
||||
tag_emphasis = google_text_emphasis(tag_style)
|
||||
parent_emphasis = google_text_emphasis(parent_style)
|
||||
|
||||
# handle Google's text emphasis
|
||||
strikethrough = 'line-through' in \
|
||||
tag_emphasis and self.hide_strikethrough
|
||||
bold = 'bold' in tag_emphasis and not 'bold' in parent_emphasis
|
||||
italic = 'italic' in tag_emphasis and not 'italic' in parent_emphasis
|
||||
fixed = google_fixed_width_font(tag_style) and not \
|
||||
google_fixed_width_font(parent_style) and not self.pre
|
||||
|
||||
if start:
|
||||
# crossed-out text must be handled before other attributes
|
||||
# in order not to output qualifiers unnecessarily
|
||||
if bold or italic or fixed:
|
||||
self.emphasis += 1
|
||||
if strikethrough:
|
||||
self.quiet += 1
|
||||
if italic:
|
||||
self.o(self.emphasis_mark)
|
||||
self.drop_white_space += 1
|
||||
if bold:
|
||||
self.o(self.strong_mark)
|
||||
self.drop_white_space += 1
|
||||
if fixed:
|
||||
self.o('`')
|
||||
self.drop_white_space += 1
|
||||
self.code = True
|
||||
else:
|
||||
if bold or italic or fixed:
|
||||
# there must not be whitespace before closing emphasis mark
|
||||
self.emphasis -= 1
|
||||
self.space = 0
|
||||
if fixed:
|
||||
if self.drop_white_space:
|
||||
# empty emphasis, drop it
|
||||
self.drop_white_space -= 1
|
||||
else:
|
||||
self.o('`')
|
||||
self.code = False
|
||||
if bold:
|
||||
if self.drop_white_space:
|
||||
# empty emphasis, drop it
|
||||
self.drop_white_space -= 1
|
||||
else:
|
||||
self.o(self.strong_mark)
|
||||
if italic:
|
||||
if self.drop_white_space:
|
||||
# empty emphasis, drop it
|
||||
self.drop_white_space -= 1
|
||||
else:
|
||||
self.o(self.emphasis_mark)
|
||||
# space is only allowed after *all* emphasis marks
|
||||
if (bold or italic) and not self.emphasis:
|
||||
self.o(" ")
|
||||
if strikethrough:
|
||||
self.quiet -= 1
|
||||
|
||||
def handle_tag(self, tag, attrs, start):
|
||||
# attrs is None for endtags
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
else:
|
||||
attrs = dict(attrs)
|
||||
|
||||
if self.tag_callback is not None:
|
||||
if self.tag_callback(self, tag, attrs, start) is True:
|
||||
return
|
||||
|
||||
# first thing inside the anchor tag is another tag that produces some output
|
||||
if (start and not self.maybe_automatic_link is None
|
||||
and tag not in ['p', 'div', 'style', 'dl', 'dt']
|
||||
and (tag != "img" or self.ignore_images)):
|
||||
self.o("[")
|
||||
self.maybe_automatic_link = None
|
||||
self.empty_link = False
|
||||
|
||||
if self.google_doc:
|
||||
# the attrs parameter is empty for a closing tag. in addition, we
|
||||
# need the attributes of the parent nodes in order to get a
|
||||
# complete style description for the current element. we assume
|
||||
# that google docs export well formed html.
|
||||
parent_style = {}
|
||||
if start:
|
||||
if self.tag_stack:
|
||||
parent_style = self.tag_stack[-1][2]
|
||||
tag_style = element_style(attrs, self.style_def, parent_style)
|
||||
self.tag_stack.append((tag, attrs, tag_style))
|
||||
else:
|
||||
dummy, attrs, tag_style = self.tag_stack.pop() if self.tag_stack else (None, {}, {})
|
||||
if self.tag_stack:
|
||||
parent_style = self.tag_stack[-1][2]
|
||||
|
||||
if hn(tag):
|
||||
self.p()
|
||||
if start:
|
||||
self.inheader = True
|
||||
self.o(hn(tag) * "#" + ' ')
|
||||
else:
|
||||
self.inheader = False
|
||||
return # prevent redundant emphasis marks on headers
|
||||
|
||||
if tag in ['p', 'div']:
|
||||
if self.google_doc:
|
||||
if start and google_has_height(tag_style):
|
||||
self.p()
|
||||
else:
|
||||
self.soft_br()
|
||||
else:
|
||||
self.p()
|
||||
|
||||
if tag == "br" and start:
|
||||
self.o(" \n")
|
||||
|
||||
if tag == "hr" and start:
|
||||
self.p()
|
||||
self.o("* * *")
|
||||
self.p()
|
||||
|
||||
if tag in ["head", "style", 'script']:
|
||||
if start:
|
||||
self.quiet += 1
|
||||
else:
|
||||
self.quiet -= 1
|
||||
|
||||
if tag == "style":
|
||||
if start:
|
||||
self.style += 1
|
||||
else:
|
||||
self.style -= 1
|
||||
|
||||
if tag in ["body"]:
|
||||
self.quiet = 0 # sites like 9rules.com never close <head>
|
||||
|
||||
if tag == "blockquote":
|
||||
if start:
|
||||
self.p()
|
||||
self.o('> ', 0, 1)
|
||||
self.start = 1
|
||||
self.blockquote += 1
|
||||
else:
|
||||
self.blockquote -= 1
|
||||
self.p()
|
||||
|
||||
if tag in ['em', 'i', 'u'] and not self.ignore_emphasis:
|
||||
self.o(self.emphasis_mark)
|
||||
if tag in ['strong', 'b'] and not self.ignore_emphasis:
|
||||
self.o(self.strong_mark)
|
||||
if tag in ['del', 'strike', 's']:
|
||||
if start:
|
||||
self.o('~~')
|
||||
else:
|
||||
self.o('~~')
|
||||
|
||||
if self.google_doc:
|
||||
if not self.inheader:
|
||||
# handle some font attributes, but leave headers clean
|
||||
self.handle_emphasis(start, tag_style, parent_style)
|
||||
|
||||
if tag in ["code", "tt"] and not self.pre:
|
||||
self.o('`') # TODO: `` `this` ``
|
||||
self.code = not self.code
|
||||
if tag == "abbr":
|
||||
if start:
|
||||
self.abbr_title = None
|
||||
self.abbr_data = ''
|
||||
if ('title' in attrs):
|
||||
self.abbr_title = attrs['title']
|
||||
else:
|
||||
if self.abbr_title is not None:
|
||||
self.abbr_list[self.abbr_data] = self.abbr_title
|
||||
self.abbr_title = None
|
||||
self.abbr_data = ''
|
||||
|
||||
if tag == "a" and not self.ignore_links:
|
||||
if start:
|
||||
if ('href' in attrs) and \
|
||||
(attrs['href'] is not None) and \
|
||||
not (self.skip_internal_links and
|
||||
attrs['href'].startswith('#')):
|
||||
self.astack.append(attrs)
|
||||
self.maybe_automatic_link = attrs['href']
|
||||
self.empty_link = True
|
||||
if self.protect_links:
|
||||
attrs['href'] = '<'+attrs['href']+'>'
|
||||
else:
|
||||
self.astack.append(None)
|
||||
else:
|
||||
if self.astack:
|
||||
a = self.astack.pop()
|
||||
if self.maybe_automatic_link and not self.empty_link:
|
||||
self.maybe_automatic_link = None
|
||||
elif a:
|
||||
if self.empty_link:
|
||||
self.o("[")
|
||||
self.empty_link = False
|
||||
self.maybe_automatic_link = None
|
||||
if self.inline_links:
|
||||
try:
|
||||
title = escape_md(a['title'])
|
||||
except KeyError:
|
||||
self.o("](" + escape_md(urlparse.urljoin(self.baseurl, a['href'])) + ")")
|
||||
else:
|
||||
self.o("](" + escape_md(urlparse.urljoin(self.baseurl, a['href']))
|
||||
+ ' "' + title + '" )')
|
||||
else:
|
||||
i = self.previousIndex(a)
|
||||
if i is not None:
|
||||
a = self.a[i]
|
||||
else:
|
||||
self.acount += 1
|
||||
a['count'] = self.acount
|
||||
a['outcount'] = self.outcount
|
||||
self.a.append(a)
|
||||
self.o("][" + str(a['count']) + "]")
|
||||
|
||||
if tag == "img" and start and not self.ignore_images:
|
||||
if 'src' in attrs:
|
||||
if not self.images_to_alt:
|
||||
attrs['href'] = attrs['src']
|
||||
alt = attrs.get('alt') or ''
|
||||
|
||||
# If we have images_with_size, write raw html including width,
|
||||
# height, and alt attributes
|
||||
if self.images_with_size and \
|
||||
("width" in attrs or "height" in attrs):
|
||||
self.o("<img src='" + attrs["src"] + "' ")
|
||||
if "width" in attrs:
|
||||
self.o("width='" + attrs["width"] + "' ")
|
||||
if "height" in attrs:
|
||||
self.o("height='" + attrs["height"] + "' ")
|
||||
if alt:
|
||||
self.o("alt='" + alt + "' ")
|
||||
self.o("/>")
|
||||
return
|
||||
|
||||
# If we have a link to create, output the start
|
||||
if not self.maybe_automatic_link is None:
|
||||
href = self.maybe_automatic_link
|
||||
if self.images_to_alt and escape_md(alt) == href and \
|
||||
self.absolute_url_matcher.match(href):
|
||||
self.o("<" + escape_md(alt) + ">")
|
||||
self.empty_link = False
|
||||
return
|
||||
else:
|
||||
self.o("[")
|
||||
self.maybe_automatic_link = None
|
||||
self.empty_link = False
|
||||
|
||||
# If we have images_to_alt, we discard the image itself,
|
||||
# considering only the alt text.
|
||||
if self.images_to_alt:
|
||||
self.o(escape_md(alt))
|
||||
else:
|
||||
self.o("![" + escape_md(alt) + "]")
|
||||
if self.inline_links:
|
||||
href = attrs.get('href') or ''
|
||||
self.o("(" + escape_md(urlparse.urljoin(self.baseurl, href)) + ")")
|
||||
else:
|
||||
i = self.previousIndex(attrs)
|
||||
if i is not None:
|
||||
attrs = self.a[i]
|
||||
else:
|
||||
self.acount += 1
|
||||
attrs['count'] = self.acount
|
||||
attrs['outcount'] = self.outcount
|
||||
self.a.append(attrs)
|
||||
self.o("[" + str(attrs['count']) + "]")
|
||||
|
||||
if tag == 'dl' and start:
|
||||
self.p()
|
||||
if tag == 'dt' and not start:
|
||||
self.pbr()
|
||||
if tag == 'dd' and start:
|
||||
self.o(' ')
|
||||
if tag == 'dd' and not start:
|
||||
self.pbr()
|
||||
|
||||
if tag in ["ol", "ul"]:
|
||||
# Google Docs create sub lists as top level lists
|
||||
if (not self.list) and (not self.lastWasList):
|
||||
self.p()
|
||||
if start:
|
||||
if self.google_doc:
|
||||
list_style = google_list_style(tag_style)
|
||||
else:
|
||||
list_style = tag
|
||||
numbering_start = list_numbering_start(attrs)
|
||||
self.list.append({
|
||||
'name': list_style,
|
||||
'num': numbering_start
|
||||
})
|
||||
else:
|
||||
if self.list:
|
||||
self.list.pop()
|
||||
if (not self.google_doc) and (not self.list):
|
||||
self.o('\n')
|
||||
self.lastWasList = True
|
||||
else:
|
||||
self.lastWasList = False
|
||||
|
||||
if tag == 'li':
|
||||
self.pbr()
|
||||
if start:
|
||||
if self.list:
|
||||
li = self.list[-1]
|
||||
else:
|
||||
li = {'name': 'ul', 'num': 0}
|
||||
if self.google_doc:
|
||||
nest_count = self.google_nest_count(tag_style)
|
||||
else:
|
||||
nest_count = len(self.list)
|
||||
# TODO: line up <ol><li>s > 9 correctly.
|
||||
self.o(" " * nest_count)
|
||||
if li['name'] == "ul":
|
||||
self.o(self.ul_item_mark + " ")
|
||||
elif li['name'] == "ol":
|
||||
li['num'] += 1
|
||||
self.o(str(li['num']) + ". ")
|
||||
self.start = 1
|
||||
|
||||
if tag in ["table", "tr", "td", "th"]:
|
||||
if self.bypass_tables:
|
||||
if start:
|
||||
self.soft_br()
|
||||
if tag in ["td", "th"]:
|
||||
if start:
|
||||
self.o('<{0}>\n\n'.format(tag))
|
||||
else:
|
||||
self.o('\n</{0}>'.format(tag))
|
||||
else:
|
||||
if start:
|
||||
self.o('<{0}>'.format(tag))
|
||||
else:
|
||||
self.o('</{0}>'.format(tag))
|
||||
|
||||
else:
|
||||
if tag == "table" and start:
|
||||
self.table_start = True
|
||||
if tag in ["td", "th"] and start:
|
||||
if self.split_next_td:
|
||||
self.o("| ")
|
||||
self.split_next_td = True
|
||||
|
||||
if tag == "tr" and start:
|
||||
self.td_count = 0
|
||||
if tag == "tr" and not start:
|
||||
self.split_next_td = False
|
||||
self.soft_br()
|
||||
if tag == "tr" and not start and self.table_start:
|
||||
# Underline table header
|
||||
self.o("|".join(["---"] * self.td_count))
|
||||
self.soft_br()
|
||||
self.table_start = False
|
||||
if tag in ["td", "th"] and start:
|
||||
self.td_count += 1
|
||||
|
||||
if tag == "pre":
|
||||
if start:
|
||||
self.startpre = 1
|
||||
self.pre = 1
|
||||
else:
|
||||
self.pre = 0
|
||||
if self.mark_code:
|
||||
self.out("\n[/code]")
|
||||
self.p()
|
||||
|
||||
# TODO: Add docstring for these one letter functions
|
||||
def pbr(self):
|
||||
"Pretty print has a line break"
|
||||
if self.p_p == 0:
|
||||
self.p_p = 1
|
||||
|
||||
def p(self):
|
||||
"Set pretty print to 1 or 2 lines"
|
||||
self.p_p = 1 if self.single_line_break else 2
|
||||
|
||||
def soft_br(self):
|
||||
"Soft breaks"
|
||||
self.pbr()
|
||||
self.br_toggle = ' '
|
||||
|
||||
def o(self, data, puredata=0, force=0):
|
||||
"""
|
||||
Deal with indentation and whitespace
|
||||
"""
|
||||
if self.abbr_data is not None:
|
||||
self.abbr_data += data
|
||||
|
||||
if not self.quiet:
|
||||
if self.google_doc:
|
||||
# prevent white space immediately after 'begin emphasis'
|
||||
# marks ('**' and '_')
|
||||
lstripped_data = data.lstrip()
|
||||
if self.drop_white_space and not (self.pre or self.code):
|
||||
data = lstripped_data
|
||||
if lstripped_data != '':
|
||||
self.drop_white_space = 0
|
||||
|
||||
if puredata and not self.pre:
|
||||
# This is a very dangerous call ... it could mess up
|
||||
# all handling of when not handled properly
|
||||
# (see entityref)
|
||||
data = re.sub(r'\s+', r' ', data)
|
||||
if data and data[0] == ' ':
|
||||
self.space = 1
|
||||
data = data[1:]
|
||||
if not data and not force:
|
||||
return
|
||||
|
||||
if self.startpre:
|
||||
#self.out(" :") #TODO: not output when already one there
|
||||
if not data.startswith("\n"): # <pre>stuff...
|
||||
data = "\n" + data
|
||||
if self.mark_code:
|
||||
self.out("\n[code]")
|
||||
self.p_p = 0
|
||||
|
||||
bq = (">" * self.blockquote)
|
||||
if not (force and data and data[0] == ">") and self.blockquote:
|
||||
bq += " "
|
||||
|
||||
if self.pre:
|
||||
if not self.list:
|
||||
bq += " "
|
||||
#else: list content is already partially indented
|
||||
for i in range(len(self.list)):
|
||||
bq += " "
|
||||
data = data.replace("\n", "\n" + bq)
|
||||
|
||||
if self.startpre:
|
||||
self.startpre = 0
|
||||
if self.list:
|
||||
# use existing initial indentation
|
||||
data = data.lstrip("\n")
|
||||
|
||||
if self.start:
|
||||
self.space = 0
|
||||
self.p_p = 0
|
||||
self.start = 0
|
||||
|
||||
if force == 'end':
|
||||
# It's the end.
|
||||
self.p_p = 0
|
||||
self.out("\n")
|
||||
self.space = 0
|
||||
|
||||
if self.p_p:
|
||||
self.out((self.br_toggle + '\n' + bq) * self.p_p)
|
||||
self.space = 0
|
||||
self.br_toggle = ''
|
||||
|
||||
if self.space:
|
||||
if not self.lastWasNL:
|
||||
self.out(' ')
|
||||
self.space = 0
|
||||
|
||||
if self.a and ((self.p_p == 2 and self.links_each_paragraph)
|
||||
or force == "end"):
|
||||
if force == "end":
|
||||
self.out("\n")
|
||||
|
||||
newa = []
|
||||
for link in self.a:
|
||||
if self.outcount > link['outcount']:
|
||||
self.out(" [" + str(link['count']) + "]: " +
|
||||
urlparse.urljoin(self.baseurl, link['href']))
|
||||
if 'title' in link:
|
||||
self.out(" (" + link['title'] + ")")
|
||||
self.out("\n")
|
||||
else:
|
||||
newa.append(link)
|
||||
|
||||
# Don't need an extra line when nothing was done.
|
||||
if self.a != newa:
|
||||
self.out("\n")
|
||||
|
||||
self.a = newa
|
||||
|
||||
if self.abbr_list and force == "end":
|
||||
for abbr, definition in self.abbr_list.items():
|
||||
self.out(" *[" + abbr + "]: " + definition + "\n")
|
||||
|
||||
self.p_p = 0
|
||||
self.out(data)
|
||||
self.outcount += 1
|
||||
|
||||
def handle_data(self, data, entity_char=False):
|
||||
if r'\/script>' in data:
|
||||
self.quiet -= 1
|
||||
|
||||
if self.style:
|
||||
self.style_def.update(dumb_css_parser(data))
|
||||
|
||||
if not self.maybe_automatic_link is None:
|
||||
href = self.maybe_automatic_link
|
||||
if (href == data and self.absolute_url_matcher.match(href)
|
||||
and self.use_automatic_links):
|
||||
self.o("<" + data + ">")
|
||||
self.empty_link = False
|
||||
return
|
||||
else:
|
||||
self.o("[")
|
||||
self.maybe_automatic_link = None
|
||||
self.empty_link = False
|
||||
|
||||
if not self.code and not self.pre and not entity_char:
|
||||
data = escape_md_section(data, snob=self.escape_snob)
|
||||
self.o(data, 1)
|
||||
|
||||
def unknown_decl(self, data): # pragma: no cover
|
||||
# TODO: what is this doing here?
|
||||
pass
|
||||
|
||||
def charref(self, name):
|
||||
if name[0] in ['x', 'X']:
|
||||
c = int(name[1:], 16)
|
||||
else:
|
||||
c = int(name)
|
||||
|
||||
if not self.unicode_snob and c in unifiable_n.keys():
|
||||
return unifiable_n[c]
|
||||
else:
|
||||
try:
|
||||
try:
|
||||
return unichr(c)
|
||||
except NameError: # Python3
|
||||
return chr(c)
|
||||
except ValueError: # invalid unicode
|
||||
return ''
|
||||
|
||||
def entityref(self, c):
|
||||
if not self.unicode_snob and c in config.UNIFIABLE.keys():
|
||||
return config.UNIFIABLE[c]
|
||||
else:
|
||||
try:
|
||||
name2cp(c)
|
||||
except KeyError:
|
||||
return "&" + c + ';'
|
||||
else:
|
||||
if c == 'nbsp':
|
||||
return config.UNIFIABLE[c]
|
||||
else:
|
||||
try:
|
||||
return unichr(name2cp(c))
|
||||
except NameError: # Python3
|
||||
return chr(name2cp(c))
|
||||
|
||||
def replaceEntities(self, s):
|
||||
s = s.group(1)
|
||||
if s[0] == "#":
|
||||
return self.charref(s[1:])
|
||||
else:
|
||||
return self.entityref(s)
|
||||
|
||||
def unescape(self, s):
|
||||
return config.RE_UNESCAPE.sub(self.replaceEntities, s)
|
||||
|
||||
def google_nest_count(self, style):
|
||||
"""
|
||||
Calculate the nesting count of google doc lists
|
||||
|
||||
:type style: dict
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
nest_count = 0
|
||||
if 'margin-left' in style:
|
||||
nest_count = int(style['margin-left'][:-2]) \
|
||||
// self.google_list_indent
|
||||
|
||||
return nest_count
|
||||
|
||||
def optwrap(self, text):
|
||||
"""
|
||||
Wrap all paragraphs in the provided text.
|
||||
|
||||
:type text: str
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.body_width:
|
||||
return text
|
||||
|
||||
assert wrap, "Requires Python 2.3."
|
||||
result = ''
|
||||
newlines = 0
|
||||
# I cannot think of a better solution for now.
|
||||
# To avoid the non-wrap behaviour for entire paras
|
||||
# because of the presence of a link in it
|
||||
if not self.wrap_links:
|
||||
self.inline_links = False
|
||||
for para in text.split("\n"):
|
||||
if len(para) > 0:
|
||||
if not skipwrap(para, self.wrap_links):
|
||||
result += "\n".join(wrap(para, self.body_width))
|
||||
if para.endswith(' '):
|
||||
result += " \n"
|
||||
newlines = 1
|
||||
else:
|
||||
result += "\n\n"
|
||||
newlines = 2
|
||||
else:
|
||||
# Warning for the tempted!!!
|
||||
# Be aware that obvious replacement of this with
|
||||
# line.isspace()
|
||||
# DOES NOT work! Explanations are welcome.
|
||||
if not config.RE_SPACE.match(para):
|
||||
result += para + "\n"
|
||||
newlines = 1
|
||||
else:
|
||||
if newlines < 2:
|
||||
result += "\n"
|
||||
newlines += 1
|
||||
return result
|
||||
|
||||
|
||||
def html2text(html, baseurl='', bodywidth=None):
|
||||
if bodywidth is None:
|
||||
bodywidth = config.BODY_WIDTH
|
||||
h = HTML2Text(baseurl=baseurl, bodywidth=bodywidth)
|
||||
|
||||
return h.handle(html)
|
||||
|
||||
|
||||
def unescape(s, unicode_snob=False):
|
||||
h = HTML2Text()
|
||||
h.unicode_snob = unicode_snob
|
||||
|
||||
return h.unescape(s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from html2text.cli import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
import htmlentitydefs
|
||||
import urlparse
|
||||
import HTMLParser
|
||||
import urllib
|
||||
else:
|
||||
import urllib.parse as urlparse
|
||||
import html.entities as htmlentitydefs
|
||||
import html.parser as HTMLParser
|
||||
import urllib.request as urllib
|
||||
@@ -0,0 +1,123 @@
|
||||
import re
|
||||
|
||||
# Use Unicode characters instead of their ascii psuedo-replacements
|
||||
UNICODE_SNOB = 0
|
||||
|
||||
# Escape all special characters. Output is less readable, but avoids
|
||||
# corner case formatting issues.
|
||||
ESCAPE_SNOB = 0
|
||||
|
||||
# Put the links after each paragraph instead of at the end.
|
||||
LINKS_EACH_PARAGRAPH = 0
|
||||
|
||||
# Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.)
|
||||
BODY_WIDTH = 78
|
||||
|
||||
# Don't show internal links (href="#local-anchor") -- corresponding link
|
||||
# targets won't be visible in the plain text file anyway.
|
||||
SKIP_INTERNAL_LINKS = True
|
||||
|
||||
# Use inline, rather than reference, formatting for images and links
|
||||
INLINE_LINKS = True
|
||||
|
||||
# Protect links from line breaks surrounding them with angle brackets (in
|
||||
# addition to their square brackets)
|
||||
PROTECT_LINKS = False
|
||||
# WRAP_LINKS = True
|
||||
WRAP_LINKS = True
|
||||
|
||||
# Number of pixels Google indents nested lists
|
||||
GOOGLE_LIST_INDENT = 36
|
||||
|
||||
IGNORE_ANCHORS = False
|
||||
IGNORE_IMAGES = False
|
||||
IMAGES_TO_ALT = False
|
||||
IMAGES_WITH_SIZE = False
|
||||
IGNORE_EMPHASIS = False
|
||||
MARK_CODE = False
|
||||
DECODE_ERRORS = 'strict'
|
||||
|
||||
# Convert links with same href and text to <href> format if they are absolute links
|
||||
USE_AUTOMATIC_LINKS = True
|
||||
|
||||
# For checking space-only lines on line 771
|
||||
RE_SPACE = re.compile(r'\s\+')
|
||||
|
||||
RE_UNESCAPE = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
|
||||
RE_ORDERED_LIST_MATCHER = re.compile(r'\d+\.\s')
|
||||
RE_UNORDERED_LIST_MATCHER = re.compile(r'[-\*\+]\s')
|
||||
RE_MD_CHARS_MATCHER = re.compile(r"([\\\[\]\(\)])")
|
||||
RE_MD_CHARS_MATCHER_ALL = re.compile(r"([`\*_{}\[\]\(\)#!])")
|
||||
RE_LINK = re.compile(r"(\[.*?\] ?\(.*?\))|(\[.*?\]:.*?)") # to find links in the text
|
||||
RE_MD_DOT_MATCHER = re.compile(r"""
|
||||
^ # start of line
|
||||
(\s*\d+) # optional whitespace and a number
|
||||
(\.) # dot
|
||||
(?=\s) # lookahead assert whitespace
|
||||
""", re.MULTILINE | re.VERBOSE)
|
||||
RE_MD_PLUS_MATCHER = re.compile(r"""
|
||||
^
|
||||
(\s*)
|
||||
(\+)
|
||||
(?=\s)
|
||||
""", flags=re.MULTILINE | re.VERBOSE)
|
||||
RE_MD_DASH_MATCHER = re.compile(r"""
|
||||
^
|
||||
(\s*)
|
||||
(-)
|
||||
(?=\s|\-) # followed by whitespace (bullet list, or spaced out hr)
|
||||
# or another dash (header or hr)
|
||||
""", flags=re.MULTILINE | re.VERBOSE)
|
||||
RE_SLASH_CHARS = r'\`*_{}[]()#+-.!'
|
||||
RE_MD_BACKSLASH_MATCHER = re.compile(r'''
|
||||
(\\) # match one slash
|
||||
(?=[%s]) # followed by a char that requires escaping
|
||||
''' % re.escape(RE_SLASH_CHARS),
|
||||
flags=re.VERBOSE)
|
||||
|
||||
UNIFIABLE = {
|
||||
'rsquo': "'",
|
||||
'lsquo': "'",
|
||||
'rdquo': '"',
|
||||
'ldquo': '"',
|
||||
'copy': '(C)',
|
||||
'mdash': '--',
|
||||
'nbsp': ' ',
|
||||
'rarr': '->',
|
||||
'larr': '<-',
|
||||
'middot': '*',
|
||||
'ndash': '-',
|
||||
'oelig': 'oe',
|
||||
'aelig': 'ae',
|
||||
'agrave': 'a',
|
||||
'aacute': 'a',
|
||||
'acirc': 'a',
|
||||
'atilde': 'a',
|
||||
'auml': 'a',
|
||||
'aring': 'a',
|
||||
'egrave': 'e',
|
||||
'eacute': 'e',
|
||||
'ecirc': 'e',
|
||||
'euml': 'e',
|
||||
'igrave': 'i',
|
||||
'iacute': 'i',
|
||||
'icirc': 'i',
|
||||
'iuml': 'i',
|
||||
'ograve': 'o',
|
||||
'oacute': 'o',
|
||||
'ocirc': 'o',
|
||||
'otilde': 'o',
|
||||
'ouml': 'o',
|
||||
'ugrave': 'u',
|
||||
'uacute': 'u',
|
||||
'ucirc': 'u',
|
||||
'uuml': 'u',
|
||||
'lrm': '',
|
||||
'rlm': ''
|
||||
}
|
||||
|
||||
BYPASS_TABLES = False
|
||||
|
||||
# Use a single line break after a block element rather an two line breaks.
|
||||
# NOTE: Requires body width setting to be 0.
|
||||
SINGLE_LINE_BREAK = False
|
||||
@@ -0,0 +1,246 @@
|
||||
import sys
|
||||
|
||||
from html2text import config
|
||||
from html2text.compat import htmlentitydefs
|
||||
|
||||
|
||||
def name2cp(k):
|
||||
"""Return sname to codepoint"""
|
||||
if k == 'apos':
|
||||
return ord("'")
|
||||
return htmlentitydefs.name2codepoint[k]
|
||||
|
||||
|
||||
unifiable_n = {}
|
||||
|
||||
for k in config.UNIFIABLE.keys():
|
||||
unifiable_n[name2cp(k)] = config.UNIFIABLE[k]
|
||||
|
||||
|
||||
def hn(tag):
|
||||
if tag[0] == 'h' and len(tag) == 2:
|
||||
try:
|
||||
n = int(tag[1])
|
||||
if n in range(1, 10): # pragma: no branch
|
||||
return n
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def dumb_property_dict(style):
|
||||
"""
|
||||
:returns: A hash of css attributes
|
||||
"""
|
||||
out = dict([(x.strip(), y.strip()) for x, y in
|
||||
[z.split(':', 1) for z in
|
||||
style.split(';') if ':' in z
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def dumb_css_parser(data):
|
||||
"""
|
||||
:type data: str
|
||||
|
||||
:returns: A hash of css selectors, each of which contains a hash of
|
||||
css attributes.
|
||||
:rtype: dict
|
||||
"""
|
||||
# remove @import sentences
|
||||
data += ';'
|
||||
importIndex = data.find('@import')
|
||||
while importIndex != -1:
|
||||
data = data[0:importIndex] + data[data.find(';', importIndex) + 1:]
|
||||
importIndex = data.find('@import')
|
||||
|
||||
# parse the css. reverted from dictionary comprehension in order to
|
||||
# support older pythons
|
||||
elements = [x.split('{') for x in data.split('}') if '{' in x.strip()]
|
||||
try:
|
||||
elements = dict([(a.strip(), dumb_property_dict(b))
|
||||
for a, b in elements])
|
||||
except ValueError: # pragma: no cover
|
||||
elements = {} # not that important
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
def element_style(attrs, style_def, parent_style):
|
||||
"""
|
||||
:type attrs: dict
|
||||
:type style_def: dict
|
||||
:type style_def: dict
|
||||
|
||||
:returns: A hash of the 'final' style attributes of the element
|
||||
:rtype: dict
|
||||
"""
|
||||
style = parent_style.copy()
|
||||
if 'class' in attrs:
|
||||
for css_class in attrs['class'].split():
|
||||
css_style = style_def.get('.' + css_class, {})
|
||||
style.update(css_style)
|
||||
if 'style' in attrs:
|
||||
immediate_style = dumb_property_dict(attrs['style'])
|
||||
style.update(immediate_style)
|
||||
|
||||
return style
|
||||
|
||||
|
||||
def google_list_style(style):
|
||||
"""
|
||||
Finds out whether this is an ordered or unordered list
|
||||
|
||||
:type style: dict
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if 'list-style-type' in style:
|
||||
list_style = style['list-style-type']
|
||||
if list_style in ['disc', 'circle', 'square', 'none']:
|
||||
return 'ul'
|
||||
|
||||
return 'ol'
|
||||
|
||||
|
||||
def google_has_height(style):
|
||||
"""
|
||||
Check if the style of the element has the 'height' attribute
|
||||
explicitly defined
|
||||
|
||||
:type style: dict
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
if 'height' in style:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def google_text_emphasis(style):
|
||||
"""
|
||||
:type style: dict
|
||||
|
||||
:returns: A list of all emphasis modifiers of the element
|
||||
:rtype: list
|
||||
"""
|
||||
emphasis = []
|
||||
if 'text-decoration' in style:
|
||||
emphasis.append(style['text-decoration'])
|
||||
if 'font-style' in style:
|
||||
emphasis.append(style['font-style'])
|
||||
if 'font-weight' in style:
|
||||
emphasis.append(style['font-weight'])
|
||||
|
||||
return emphasis
|
||||
|
||||
|
||||
def google_fixed_width_font(style):
|
||||
"""
|
||||
Check if the css of the current element defines a fixed width font
|
||||
|
||||
:type style: dict
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
font_family = ''
|
||||
if 'font-family' in style:
|
||||
font_family = style['font-family']
|
||||
if 'Courier New' == font_family or 'Consolas' == font_family:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def list_numbering_start(attrs):
|
||||
"""
|
||||
Extract numbering from list element attributes
|
||||
|
||||
:type attrs: dict
|
||||
|
||||
:rtype: int or None
|
||||
"""
|
||||
if 'start' in attrs:
|
||||
try:
|
||||
return int(attrs['start']) - 1
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def skipwrap(para, wrap_links):
|
||||
# If it appears to contain a link
|
||||
# don't wrap
|
||||
if (len(config.RE_LINK.findall(para)) > 0) and not wrap_links:
|
||||
return True
|
||||
# If the text begins with four spaces or one tab, it's a code block;
|
||||
# don't wrap
|
||||
if para[0:4] == ' ' or para[0] == '\t':
|
||||
return True
|
||||
|
||||
# If the text begins with only two "--", possibly preceded by
|
||||
# whitespace, that's an emdash; so wrap.
|
||||
stripped = para.lstrip()
|
||||
if stripped[0:2] == "--" and len(stripped) > 2 and stripped[2] != "-":
|
||||
return False
|
||||
|
||||
# I'm not sure what this is for; I thought it was to detect lists,
|
||||
# but there's a <br>-inside-<span> case in one of the tests that
|
||||
# also depends upon it.
|
||||
if stripped[0:1] == '-' or stripped[0:1] == '*':
|
||||
return True
|
||||
|
||||
# If the text begins with a single -, *, or +, followed by a space,
|
||||
# or an integer, followed by a ., followed by a space (in either
|
||||
# case optionally proceeded by whitespace), it's a list; don't wrap.
|
||||
if config.RE_ORDERED_LIST_MATCHER.match(stripped) or \
|
||||
config.RE_UNORDERED_LIST_MATCHER.match(stripped):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def wrapwrite(text):
|
||||
text = text.encode('utf-8')
|
||||
try: # Python3
|
||||
sys.stdout.buffer.write(text)
|
||||
except AttributeError:
|
||||
sys.stdout.write(text)
|
||||
|
||||
|
||||
def wrap_read(): # pragma: no cover
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
try:
|
||||
return sys.stdin.read()
|
||||
except AttributeError:
|
||||
return sys.stdin.buffer.read()
|
||||
|
||||
|
||||
def escape_md(text):
|
||||
"""
|
||||
Escapes markdown-sensitive characters within other markdown
|
||||
constructs.
|
||||
"""
|
||||
return config.RE_MD_CHARS_MATCHER.sub(r"\\\1", text)
|
||||
|
||||
|
||||
def escape_md_section(text, snob=False):
|
||||
"""
|
||||
Escapes markdown-sensitive characters across whole document sections.
|
||||
"""
|
||||
text = config.RE_MD_BACKSLASH_MATCHER.sub(r"\\\1", text)
|
||||
|
||||
if snob:
|
||||
text = config.RE_MD_CHARS_MATCHER_ALL.sub(r"\\\1", text)
|
||||
|
||||
text = config.RE_MD_DOT_MATCHER.sub(r"\1\\\2", text)
|
||||
text = config.RE_MD_PLUS_MATCHER.sub(r"\1\\\2", text)
|
||||
text = config.RE_MD_DASH_MATCHER.sub(r"\1\\\2", text)
|
||||
|
||||
return text
|
||||
+1
-1
@@ -36,7 +36,7 @@ if __name__=="__main__":
|
||||
|
||||
os.chdir('../included_dependencies')
|
||||
# 'a' for append
|
||||
files=['gif.py','six.py','bs4','html5lib','chardet']
|
||||
files=['gif.py','six.py','bs4','html5lib','chardet','html2text']
|
||||
createZipFile("../"+filename,"a",
|
||||
files,
|
||||
exclude=exclude)
|
||||
|
||||
@@ -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.2.17",
|
||||
# Versions should comply with PEP440.
|
||||
version="2.4.0",
|
||||
|
||||
description='A tool for downloading fanfiction to eBook formats',
|
||||
long_description=long_description,
|
||||
@@ -81,7 +79,7 @@ setup(
|
||||
# your project is installed. For an analysis of "install_requires" vs pip's
|
||||
# requirements files see:
|
||||
# https://packaging.python.org/en/latest/requirements.html
|
||||
install_requires=['beautifulsoup4','chardet','html5lib'], # html5lib requires 'six'.
|
||||
install_requires=['beautifulsoup4','chardet','html5lib','html2text'], # html5lib requires 'six'.
|
||||
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
# dependencies). You can install these using the following syntax,
|
||||
|
||||
@@ -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-2-17
|
||||
version: 2-4-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"
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
<img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif"
|
||||
alt="Powered by Google App Engine" />
|
||||
<br/><br/>
|
||||
This is a web front-end to <A href="http://code.google.com/p/fanficdownloader/">FanFicFare</a><br/>
|
||||
Copyright © Fanficdownloader team
|
||||
This is a web front-end to <a href="https://github.com/JimmXinu/FanFicFare/">FanFicFare</a><br/>
|
||||
Copyright © FanFicFare team
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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-2-16.fanficfare.appspot.com">previous version
|
||||
<a href="http://2-3-06.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>.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif"
|
||||
alt="Powered by Google App Engine" />
|
||||
<br/><br/>
|
||||
This is a web front-end to <A href="http://code.google.com/p/fanficdownloader/">FanFicFare</a><br/>
|
||||
This is a web front-end to <a href="https://github.com/JimmXinu/FanFicFare/">FanFicFare</a><br/>
|
||||
Copyright © FanFicFare team
|
||||
</div>
|
||||
|
||||
|
||||
+29
-49
@@ -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
|
||||
@@ -210,18 +196,18 @@ class FileServer(webapp2.RequestHandler):
|
||||
# to hold the whole in memory just for the
|
||||
# compress/uncompress
|
||||
if download.format != 'epub':
|
||||
def dc(data):
|
||||
def decompress(data):
|
||||
try:
|
||||
return zlib.decompress(data)
|
||||
# if error, assume it's a chunk from before we started compessing.
|
||||
except zlib.error:
|
||||
return data
|
||||
else:
|
||||
def dc(data):
|
||||
def decompress(data):
|
||||
return data
|
||||
|
||||
for datum in data:
|
||||
self.response.out.write(dc(datum.blob))
|
||||
self.response.out.write(decompress(datum.blob))
|
||||
|
||||
except Exception, e:
|
||||
fic = DownloadMeta()
|
||||
@@ -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"
|
||||
@@ -286,8 +272,8 @@ class ClearRecentServer(webapp2.RequestHandler):
|
||||
if results:
|
||||
for d in results:
|
||||
d.delete()
|
||||
for c in d.data_chunks:
|
||||
c.delete()
|
||||
for chunk in d.data_chunks:
|
||||
chunk.delete()
|
||||
num = num + 1
|
||||
logging.debug('Delete '+d.url)
|
||||
else:
|
||||
@@ -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)
|
||||
@@ -494,8 +483,8 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
# use existing record if available.
|
||||
# fileId should have record from /fdown.
|
||||
download = getDownloadMeta(id=fileId,url=url,user=user,format=format,new=True)
|
||||
for c in download.data_chunks:
|
||||
c.delete()
|
||||
for chunk in download.data_chunks:
|
||||
chunk.delete()
|
||||
download.put()
|
||||
|
||||
logging.info('Creating adapter...')
|
||||
@@ -542,21 +531,22 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
# compressed individually to avoid having to hold the
|
||||
# whole in memory just for the compress/uncompress.
|
||||
if format != 'epub':
|
||||
def c(data):
|
||||
def compress(data):
|
||||
return zlib.compress(data)
|
||||
else:
|
||||
def c(data):
|
||||
def compress(data):
|
||||
return data
|
||||
|
||||
# delete existing chunks first
|
||||
for c in download.data_chunks:
|
||||
c.delete()
|
||||
|
||||
for chunk in download.data_chunks:
|
||||
chunk.delete()
|
||||
|
||||
index=0
|
||||
while( len(data) > 0 ):
|
||||
# logging.info("len(data): %s" % len(data))
|
||||
DownloadData(download=download,
|
||||
index=index,
|
||||
blob=c(data[:1000000])).put()
|
||||
blob=compress(data[:1000000])).put()
|
||||
index += 1
|
||||
data = data[1000000:]
|
||||
download.completed=True
|
||||
@@ -576,7 +566,7 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
smeta.meta = allmeta
|
||||
smeta.date = datetime.datetime.now()
|
||||
smeta.put()
|
||||
|
||||
|
||||
logging.info("Download finished OK")
|
||||
del data
|
||||
|
||||
@@ -629,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),
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif"
|
||||
alt="Powered by Google App Engine" />
|
||||
<br/><br/>
|
||||
This is a web front-end to <A href="http://code.google.com/p/fanficdownloader/">FanFicFare</a><br/>
|
||||
This is a web front-end to <a href="https://github.com/JimmXinu/FanFicFare/">FanFicFare</a><br/>
|
||||
Copyright © FanFicFare team
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user