mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-14 11:14:10 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b28482514 | ||
|
|
b210007f59 | ||
|
|
08bc200c5c | ||
|
|
43ec9c0232 | ||
|
|
a0c752c61f | ||
|
|
ed84f10bfb | ||
|
|
1d1e43f16f | ||
|
|
ba4a29d646 | ||
|
|
7ba523f20d | ||
|
|
0e42f758ab | ||
|
|
e2995a7614 | ||
|
|
3abb78a15e | ||
|
|
3b9b9c1eb7 | ||
|
|
235c5ae62f | ||
|
|
b2238e10e7 | ||
|
|
a5bc7bd757 | ||
|
|
36d848fde9 | ||
|
|
793ec37782 | ||
|
|
49b9994a3b | ||
|
|
6d1798c29e | ||
|
|
897976b4bd | ||
|
|
ed4f7a4c1d | ||
|
|
ab5c930248 | ||
|
|
6dd1cc7daa | ||
|
|
44325ef0ce | ||
|
|
c55553bbbf | ||
|
|
7956a1a7e1 | ||
|
|
afa14c568e | ||
|
|
df9065b1ba | ||
|
|
a4cc07c8ff | ||
|
|
5261decb02 | ||
|
|
9993836eca | ||
|
|
33916543e3 | ||
|
|
ede81a458e | ||
|
|
f3af754c71 | ||
|
|
801e14e028 | ||
|
|
2440f002fd | ||
|
|
a992829c04 | ||
|
|
55e434e9fb | ||
|
|
0c0e439cc6 | ||
|
|
3e431c843a | ||
|
|
be04eed180 | ||
|
|
37a084a699 | ||
|
|
5bc5c85248 | ||
|
|
63eed56f23 | ||
|
|
a2491be351 | ||
|
|
289398dc66 | ||
|
|
5040c44572 | ||
|
|
e2d1a693dd | ||
|
|
1f52833732 | ||
|
|
b708a715a8 | ||
|
|
35d7db0319 | ||
|
|
c3b3e94bfc | ||
|
|
b41fc45c1c | ||
|
|
07d3e4a603 | ||
|
|
340fe65bf6 | ||
|
|
5d481b1d2a | ||
|
|
95b799663e | ||
|
|
51ea8f18a7 | ||
|
|
2943de51e0 | ||
|
|
7f00c56ecf | ||
|
|
ba1c439d28 | ||
|
|
a182d16a6c |
@@ -42,7 +42,7 @@ class FanFicFareBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (2, 2, 13)
|
||||
version = (2, 2, 15)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
@@ -243,6 +243,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['fileform'] = unicode(self.basic_tab.fileform.currentText())
|
||||
prefs['collision'] = save_collisions[unicode(self.basic_tab.collision.currentText())]
|
||||
prefs['updatemeta'] = self.basic_tab.updatemeta.isChecked()
|
||||
prefs['bgmeta'] = self.basic_tab.bgmeta.isChecked()
|
||||
prefs['updateepubcover'] = self.basic_tab.updateepubcover.isChecked()
|
||||
prefs['keeptags'] = self.basic_tab.keeptags.isChecked()
|
||||
prefs['suppressauthorsort'] = self.basic_tab.suppressauthorsort.isChecked()
|
||||
@@ -430,6 +431,12 @@ 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'])
|
||||
horz.addWidget(self.bgmeta)
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
cali_gb = groupbox = QGroupBox(_("Updating Calibre Options"))
|
||||
@@ -457,7 +464,7 @@ 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."))
|
||||
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.setChecked(prefs['checkforseriesurlid'])
|
||||
self.l.addWidget(self.checkforseriesurlid)
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ save_collisions={
|
||||
OVERWRITE:SAVE_OVERWRITE,
|
||||
OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS,
|
||||
CALIBREONLY:SAVE_CALIBREONLY,
|
||||
CALIBREONLYSAVECOL:SAVE_CALIBREONLYSAVECOL,
|
||||
SAVE_SKIP:SKIP,
|
||||
SAVE_ADDNEW:ADDNEW,
|
||||
SAVE_UPDATE:UPDATE,
|
||||
@@ -335,6 +336,18 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
self.gbl.addLayout(horz)
|
||||
|
||||
## bgmeta not used with Add New because of stories that change
|
||||
## story URL and for title/author collision matching.
|
||||
# horz = QHBoxLayout()
|
||||
# self.bgmeta = QCheckBox(_('Background Metadata?'),self)
|
||||
# self.bgmeta.setToolTip(_("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."))
|
||||
# self.bgmeta.setChecked(self.prefs['bgmeta'])
|
||||
# horz.addWidget(self.bgmeta)
|
||||
# self.mergehide.append(self.bgmeta)
|
||||
# self.mergeupdateshow.append(self.bgmeta)
|
||||
|
||||
# self.gbl.addLayout(horz)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.ok_clicked)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
@@ -417,6 +430,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
self.updatemeta.setChecked(self.prefs['updatemeta'])
|
||||
# self.bgmeta.setChecked(self.prefs['bgmeta'])
|
||||
|
||||
if not self.merge:
|
||||
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
|
||||
@@ -457,6 +471,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
'fileform': unicode(self.fileform.currentText()),
|
||||
'collision': unicode(self.collision.currentText()),
|
||||
'updatemeta': self.updatemeta.isChecked(),
|
||||
'bgmeta': False, # self.bgmeta.isChecked(),
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation']
|
||||
}
|
||||
@@ -787,15 +802,18 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
gbl = QVBoxLayout()
|
||||
gbl.addWidget(gbf)
|
||||
groupbox.setLayout(gbl)
|
||||
gbl = QHBoxLayout()
|
||||
gbl = QVBoxLayout()
|
||||
gbf.setLayout(gbl)
|
||||
options_layout.addWidget(groupbox)
|
||||
|
||||
gbf.setVisible(False)
|
||||
groupbox.toggled.connect(gbf.setVisible)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
gbl.addLayout(horz)
|
||||
|
||||
label = QLabel(_('Output &Format:'))
|
||||
gbl.addWidget(label)
|
||||
horz.addWidget(label)
|
||||
self.fileform = QComboBox(self)
|
||||
self.fileform.addItem('epub')
|
||||
self.fileform.addItem('mobi')
|
||||
@@ -805,10 +823,10 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
self.fileform.setToolTip(_('Choose output format to create. May set default from plugin configuration.'))
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
label.setBuddy(self.fileform)
|
||||
gbl.addWidget(self.fileform)
|
||||
horz.addWidget(self.fileform)
|
||||
|
||||
label = QLabel(_('Update Mode:'))
|
||||
gbl.addWidget(label)
|
||||
horz.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
self.collision.setToolTip(_("What sort of update to perform. May set default from plugin configuration."))
|
||||
# add collision options
|
||||
@@ -817,19 +835,25 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
label.setBuddy(self.collision)
|
||||
gbl.addWidget(self.collision)
|
||||
horz.addWidget(self.collision)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
gbl.addLayout(horz)
|
||||
|
||||
self.updatemeta = QCheckBox(_('Update Calibre &Metadata?'),self)
|
||||
self.updatemeta.setToolTip(_("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)"))
|
||||
self.updatemeta.setChecked(self.prefs['updatemeta'])
|
||||
gbl.addWidget(self.updatemeta)
|
||||
horz.addWidget(self.updatemeta)
|
||||
|
||||
self.updateepubcover = QCheckBox(_('Update EPUB Cover?'),self)
|
||||
self.updateepubcover.setToolTip(_('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.'))
|
||||
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
|
||||
gbl.addWidget(self.updateepubcover)
|
||||
|
||||
horz.addWidget(self.updateepubcover)
|
||||
|
||||
self.bgmeta = QCheckBox(_('Background Metadata?'),self)
|
||||
self.bgmeta.setToolTip(_("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."))
|
||||
self.bgmeta.setChecked(self.prefs['bgmeta'])
|
||||
horz.addWidget(self.bgmeta)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
@@ -872,6 +896,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
'fileform': unicode(self.fileform.currentText()),
|
||||
'collision': unicode(self.collision.currentText()),
|
||||
'updatemeta': self.updatemeta.isChecked(),
|
||||
'bgmeta': self.bgmeta.isChecked(),
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation']
|
||||
}
|
||||
@@ -1432,6 +1457,8 @@ class ViewLog(SizePersistedDialog):
|
||||
## error = (lineno, msg)
|
||||
for (lineno, error_msg) in errors:
|
||||
# print('adding label for error:%s: %s'%(lineno, error_msg))
|
||||
if len(error_msg) > 200:
|
||||
error_msg=error_msg[:200]+" ..."
|
||||
label = QLabel('%s: %s'%(lineno, error_msg))
|
||||
label.setWordWrap(True)
|
||||
label.setStyleSheet("QLabel { margin-left: 2em; color : blue; } QLabel:hover { color: red; }");
|
||||
|
||||
+137
-103
@@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import time, os, copy, threading, re, platform, sys
|
||||
import os, copy, threading, re, platform, sys
|
||||
from StringIO import StringIO
|
||||
from functools import partial
|
||||
from datetime import datetime, time, date
|
||||
@@ -470,8 +470,14 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if prefs['download_from_email_immediately']:
|
||||
## do imap fetch w/o GUI elements
|
||||
if url_list:
|
||||
self.prep_downloads(self.add_new_dialog.get_fff_options(),
|
||||
"\n".join(url_list))
|
||||
self.prep_downloads({
|
||||
'fileform': prefs['fileform'],
|
||||
'collision': prefs['collision'],
|
||||
'updatemeta': prefs['updatemeta'],
|
||||
'bgmeta': False,
|
||||
'updateepubcover': prefs['updateepubcover'],
|
||||
'smarten_punctuation':prefs['smarten_punctuation']
|
||||
},"\n".join(url_list))
|
||||
else:
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs from Email.'),3000)
|
||||
|
||||
@@ -923,11 +929,25 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
options['tdir']=tdir
|
||||
|
||||
if 0 < len(filter(lambda x : x['good'], books)):
|
||||
self.gui.status_bar.show_message(_('Started fetching metadata for %s stories.')%len(books), 3000)
|
||||
if options['bgmeta']:
|
||||
status_bar=_('Start queuing downloading for %s stories.')%len(books)
|
||||
init_label=_("Queuing download for stories...")
|
||||
win_title=_("Queuing download for stories")
|
||||
status_prefix=_("Queued download for")
|
||||
else:
|
||||
status_bar=_('Started fetching metadata for %s stories.')%len(books)
|
||||
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,
|
||||
partial(self.prep_download_loop, options = options, merge=merge),
|
||||
partial(self.start_download_job, options = options, merge=merge))
|
||||
partial(self.start_download_job, options = options, merge=merge),
|
||||
init_label=init_label,
|
||||
win_title=win_title,
|
||||
status_prefix=status_prefix)
|
||||
else:
|
||||
self.gui.status_bar.show_message(_('No valid story URLs entered.'), 3000)
|
||||
# LoopProgressDialog calls prep_download_loop for each 'good' story,
|
||||
@@ -976,6 +996,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
options={'fileform':'epub',
|
||||
'collision':ADDNEW,
|
||||
'updatemeta':True,
|
||||
'bgmeta':False,
|
||||
'updateepubcover':True},
|
||||
merge=False):
|
||||
'''
|
||||
@@ -1003,6 +1024,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
fileform = options['fileform']
|
||||
collision = options['collision']
|
||||
updatemeta= options['updatemeta']
|
||||
bgmeta= options['bgmeta']
|
||||
updateepubcover= options['updateepubcover']
|
||||
|
||||
# Dialogs should prevent this case now.
|
||||
@@ -1048,9 +1070,11 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if savedmetadata:
|
||||
# sets flag inside story so getStoryMetadataOnly won't hit server.
|
||||
adapter.setStoryMetadata(savedmetadata)
|
||||
|
||||
|
||||
# let other exceptions percolate up.
|
||||
# bgmeta doesn't work with CALIBREONLY.
|
||||
story = adapter.getStoryMetadataOnly(get_cover=False)
|
||||
bgmeta = False
|
||||
else:
|
||||
# reduce foreground sleep time for ffnet when few books.
|
||||
if 'ffnetcount' in options and \
|
||||
@@ -1064,101 +1088,105 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
slp = min(maxslp,m*float(options['ffnetcount'])+b)
|
||||
#print("m:%s b:%s = %s"%(m,b,slp))
|
||||
adapter.set_sleep(slp)
|
||||
|
||||
## 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(get_cover=False)
|
||||
except exceptions.FailedToLogin, f:
|
||||
logger.warn("Login Failed, Need Username/Password.")
|
||||
userpass = UserPassDialog(self.gui,url,f)
|
||||
userpass.exec_() # exec_ will make it act modal
|
||||
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')
|
||||
|
||||
|
||||
if not bgmeta:
|
||||
## 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(get_cover=False)
|
||||
except exceptions.FailedToLogin, f:
|
||||
logger.warn("Login Failed, Need Username/Password.")
|
||||
userpass = UserPassDialog(self.gui,url,f)
|
||||
userpass.exec_() # exec_ will make it act modal
|
||||
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.
|
||||
if self.reject_url(merge,book):
|
||||
return
|
||||
|
||||
series = story.getMetadata('series')
|
||||
if not merge and series and prefs['checkforseriesurlid']:
|
||||
# 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?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>%s</p>
|
||||
<p>%s</p>
|
||||
'''%(
|
||||
_('Skip Anthology Story?'),
|
||||
_('"<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):
|
||||
book['comment'] = _("Story in Series Anthology(%s).")%series
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['url'] = story.getMetadata('storyUrl')
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Skipped')
|
||||
return
|
||||
|
||||
if not bgmeta:
|
||||
series = story.getMetadata('series')
|
||||
if not merge and series and prefs['checkforseriesurlid']:
|
||||
# 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?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>%s</p>
|
||||
<p>%s</p>
|
||||
'''%(
|
||||
_('Skip Anthology Story?'),
|
||||
_('"<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):
|
||||
book['comment'] = _("Story in Series Anthology(%s).")%series
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['url'] = story.getMetadata('storyUrl')
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Skipped')
|
||||
return
|
||||
|
||||
################################################################################################################################################33
|
||||
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
story.setMetadata('version',options['version'])
|
||||
|
||||
# all_metadata duplicates some data, but also includes extra_entries, etc.
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
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")
|
||||
book['url'] = story.getMetadata("storyUrl")
|
||||
book['tags'] = story.getSubjectTags(removeallentities=True)
|
||||
if story.getMetadata("description"):
|
||||
book['comments'] = sanitize_comments_html(story.getMetadata("description"))
|
||||
else:
|
||||
book['comments']=''
|
||||
book['series'] = story.getMetadata("series", removeallentities=True)
|
||||
|
||||
book['is_adult'] = adapter.is_adult
|
||||
book['username'] = adapter.username
|
||||
book['password'] = adapter.password
|
||||
|
||||
book['icon'] = 'plus.png'
|
||||
book['status'] = _('Add')
|
||||
if story.getMetadataRaw('datePublished'):
|
||||
book['pubdate'] = story.getMetadataRaw('datePublished').replace(tzinfo=local_tz)
|
||||
if story.getMetadataRaw('dateUpdated'):
|
||||
book['updatedate'] = story.getMetadataRaw('dateUpdated').replace(tzinfo=local_tz)
|
||||
if story.getMetadataRaw('dateCreated'):
|
||||
book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz)
|
||||
else:
|
||||
book['timestamp'] = None # need *something* there for calibre.
|
||||
|
||||
if not bgmeta:
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
story.setMetadata('version',options['version'])
|
||||
|
||||
# all_metadata duplicates some data, but also includes extra_entries, etc.
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
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")
|
||||
book['url'] = story.getMetadata("storyUrl")
|
||||
book['tags'] = story.getSubjectTags(removeallentities=True)
|
||||
if story.getMetadata("description"):
|
||||
book['comments'] = sanitize_comments_html(story.getMetadata("description"))
|
||||
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'):
|
||||
book['updatedate'] = story.getMetadataRaw('dateUpdated').replace(tzinfo=local_tz)
|
||||
if story.getMetadataRaw('dateCreated'):
|
||||
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'
|
||||
@@ -1182,9 +1210,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
if len(identicalbooks) < 1 and prefs['matchtitleauth']:
|
||||
# find dups
|
||||
authlist = story.getList("author", removeallentities=True)
|
||||
mi = MetaInformation(story.getMetadata("title", removeallentities=True),
|
||||
authlist)
|
||||
mi = MetaInformation(book['title'],book['author'])
|
||||
identicalbooks = db.find_identical_books(mi)
|
||||
if len(identicalbooks) > 0:
|
||||
logger.debug("existing found by title/author(s)")
|
||||
@@ -1261,7 +1287,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
## newer/chaptercount checks are the same for both:
|
||||
# Update epub, but only if more chapters.
|
||||
if collision in (UPDATE,UPDATEALWAYS): # collision == UPDATE
|
||||
if not bgmeta and collision in (UPDATE,UPDATEALWAYS): # collision == UPDATE
|
||||
# 'book' can exist without epub. If there's no existing epub,
|
||||
# let it go and it will download it.
|
||||
if db.has_format(book_id,fileform,index_is_id=True):
|
||||
@@ -1279,17 +1305,17 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
if collision == OVERWRITE and \
|
||||
db.has_format(book_id,formmapping[fileform],index_is_id=True):
|
||||
# check make sure incoming is newer.
|
||||
lastupdated=story.getMetadataRaw('dateUpdated')
|
||||
fileupdated=datetime.fromtimestamp(os.stat(db.format_abspath(book_id, formmapping[fileform], index_is_id=True))[8])
|
||||
|
||||
# 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')
|
||||
|
||||
|
||||
book['fileupdated']=fileupdated
|
||||
if not bgmeta:
|
||||
# check make sure incoming is newer.
|
||||
lastupdated=story.getMetadataRaw('dateUpdated')
|
||||
|
||||
# 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')
|
||||
|
||||
# For update, provide a tmp file copy of the existing epub so
|
||||
# it can't change underneath us. Now also overwrite for logpage preserve.
|
||||
@@ -1360,7 +1386,11 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# For HTML format users, make the filename inside the zip something reasonable.
|
||||
# For crazy long titles/authors, limit it to 200chars.
|
||||
# For weird/OS-unsafe characters, use file safe only.
|
||||
tmp = PersistentTemporaryFile(prefix=story.formatFileName("${title}-${author}-",allowunsafefilename=False)[:100],
|
||||
try:
|
||||
prefix = story.formatFileName("${title}-${author}-",allowunsafefilename=False)[:100]
|
||||
except NameError:
|
||||
prefix = "bgmeta-"
|
||||
tmp = PersistentTemporaryFile(prefix=prefix,
|
||||
suffix='.'+options['fileform'],
|
||||
dir=options['tdir'])
|
||||
logger.debug("title:"+book['title'])
|
||||
@@ -1373,6 +1403,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
options={'fileform':'epub',
|
||||
'collision':ADDNEW,
|
||||
'updatemeta':True,
|
||||
'bgmeta':False,
|
||||
'updateepubcover':True},
|
||||
merge=False):
|
||||
'''
|
||||
@@ -1438,7 +1469,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
func = 'arbitrary_n'
|
||||
cpus = self.gui.job_manager.server.pool_size
|
||||
args = ['calibre_plugins.fanficfare_plugin.jobs', 'do_download_worker',
|
||||
(book_list, options, cpus)]
|
||||
(book_list, options, cpus, merge)]
|
||||
desc = _('Download FanFiction Book')
|
||||
job = self.gui.job_manager.run_job(
|
||||
self.Dispatcher(partial(self.download_list_completed,options=options,merge=merge)),
|
||||
@@ -1452,6 +1483,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
options={'fileform':'epub',
|
||||
'collision':ADDNEW,
|
||||
'updatemeta':True,
|
||||
'bgmeta':False,
|
||||
'updateepubcover':True}):
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if book['calibre_id'] and prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
|
||||
@@ -1959,7 +1991,8 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
author_id_to_link_map = dict()
|
||||
for i, author in enumerate(authorlist):
|
||||
author_id_to_link_map[authorids[author]] = authurls[i]
|
||||
if len(authurls) > i:
|
||||
author_id_to_link_map[authorids[author]] = authurls[i]
|
||||
|
||||
# print("author_id_to_link_map:%s\n\n"%author_id_to_link_map)
|
||||
db.new_api.set_link_for_authors(author_id_to_link_map)
|
||||
@@ -2155,6 +2188,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['comment'] = '' # note this is a comment on the d/l or update.
|
||||
book['url'] = ''
|
||||
book['site'] = ''
|
||||
book['series'] = ''
|
||||
book['added'] = False
|
||||
book['pubdate'] = None
|
||||
book['publisher'] = None
|
||||
|
||||
+56
-17
@@ -10,12 +10,15 @@ __docformat__ = 'restructuredtext en'
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import time, traceback
|
||||
import traceback
|
||||
from datetime import time
|
||||
from StringIO import StringIO
|
||||
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
from calibre.constants import numeric_version as calibre_version
|
||||
from calibre.utils.date import local_tz
|
||||
from calibre.library.comments import sanitize_comments_html
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
@@ -23,8 +26,11 @@ from calibre.constants import numeric_version as calibre_version
|
||||
#
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def do_download_worker(book_list, options,
|
||||
cpus, notification=lambda x,y:x):
|
||||
def do_download_worker(book_list,
|
||||
options,
|
||||
cpus,
|
||||
merge=False,
|
||||
notification=lambda x,y:x):
|
||||
'''
|
||||
Master job, to launch child jobs to extract ISBN for a set of books
|
||||
This is run as a worker job in the background to keep the UI more
|
||||
@@ -44,7 +50,7 @@ def do_download_worker(book_list, options,
|
||||
total += 1
|
||||
args = ['calibre_plugins.fanficfare_plugin.jobs',
|
||||
'do_download_for_worker',
|
||||
(book,options)]
|
||||
(book,options,merge)]
|
||||
job = ParallelJob('arbitrary_n',
|
||||
"url:(%s) id:(%s)"%(book['url'],book['calibre_id']),
|
||||
done=None,
|
||||
@@ -90,7 +96,7 @@ def do_download_worker(book_list, options,
|
||||
# return the book list as the job result
|
||||
return book_list
|
||||
|
||||
def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
'''
|
||||
Child job, to download story when run as a worker job
|
||||
'''
|
||||
@@ -147,6 +153,26 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
if 'version' in options:
|
||||
story.setMetadata('version',options['version'])
|
||||
|
||||
book['title'] = story.getMetadata("title", removeallentities=True)
|
||||
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
|
||||
book['publisher'] = story.getMetadata("site")
|
||||
book['url'] = story.getMetadata("storyUrl")
|
||||
book['tags'] = story.getSubjectTags(removeallentities=True)
|
||||
if story.getMetadata("description"):
|
||||
book['comments'] = sanitize_comments_html(story.getMetadata("description"))
|
||||
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'):
|
||||
book['updatedate'] = story.getMetadataRaw('dateUpdated').replace(tzinfo=local_tz)
|
||||
if story.getMetadataRaw('dateCreated'):
|
||||
book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz)
|
||||
else:
|
||||
book['timestamp'] = None # need *something* there for calibre.
|
||||
|
||||
writer = writers.getWriter(options['fileform'],configuration,adapter)
|
||||
|
||||
outfile = book['outfile']
|
||||
@@ -165,12 +191,22 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
|
||||
# preserve logfile even on overwrite.
|
||||
if 'epub_for_update' in book:
|
||||
|
||||
adapter.logfile = get_update_data(book['epub_for_update'])[6]
|
||||
# change the existing entries id to notid so
|
||||
# write_epub writes a whole new set to indicate overwrite.
|
||||
if adapter.logfile:
|
||||
adapter.logfile = adapter.logfile.replace("span id","span notid")
|
||||
|
||||
if options['collision'] == OVERWRITE and 'fileupdated' in book:
|
||||
lastupdated=story.getMetadataRaw('dateUpdated')
|
||||
fileupdated=book['fileupdated']
|
||||
|
||||
# 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')
|
||||
|
||||
|
||||
logger.info("write to %s"%outfile)
|
||||
inject_cal_cols(book,story,configuration)
|
||||
@@ -199,17 +235,20 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
# dup handling from fff_plugin needed for anthology updates.
|
||||
if options['collision'] == UPDATE:
|
||||
if chaptercount == urlchaptercount:
|
||||
book['comment']=_("Already contains %d chapters. Reuse as is.")%chaptercount
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
book['outfile'] = book['epub_for_update'] # for anthology merge ops.
|
||||
return book
|
||||
|
||||
# dup handling from fff_plugin needed for anthology updates.
|
||||
if chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
|
||||
if merge:
|
||||
book['comment']=_("Already contains %d chapters. Reuse as is.")%chaptercount
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
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')
|
||||
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:
|
||||
raise NotGoingToDownload(_("FanFicFare doesn't recognize chapters in existing epub, epub is probably from a different source. Use Overwrite to force update."),'dialog_error.png')
|
||||
|
||||
if not (options['collision'] == UPDATEALWAYS and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
@@ -163,20 +163,29 @@ connect_timeout:60.0
|
||||
## For example, you could change Sci-Fi=>SF, remove *-Centered tags,
|
||||
## etc. See http://docs.python.org/library/re.html (look for re.sub)
|
||||
## for regexp details.
|
||||
##
|
||||
## Make sure to keep at least one space at the start of each line and
|
||||
## to escape % to %%, if used.
|
||||
##
|
||||
## Two, three or five part lines. Two part effect everything.
|
||||
## Three part effect only those key(s) lists.
|
||||
## *Five* part lines. Effect only when trailing conditional key=>regexp matches
|
||||
## metakey[,metakey]=>pattern=>replacement[&&conditionalkey=>regexp]
|
||||
##
|
||||
## Note that if metakey == conditionalkey the conditional is ignored.
|
||||
## You can use \s in the replacement to add explicit spaces. (The config parser
|
||||
## tends to discard trailing spaces.)
|
||||
##
|
||||
## replace_metadata <entry>_LIST options: FanFicFare replace_metadata lines
|
||||
## operate on individual list items for list entries. But if you
|
||||
## want to do a replacement on the joined string for the whole list,
|
||||
## you can by using <entry>_LIST. Example, if you added
|
||||
## calibre_author: calibre_author_LIST=>^(.{,100}).*$=>\1
|
||||
##
|
||||
## You can 'split' one list item into multiple list entries by using
|
||||
## \' in the replacement string.
|
||||
##
|
||||
## Examples:
|
||||
#replace_metadata:
|
||||
# genre,category=>Sci-Fi=>SF
|
||||
# Puella Magi Madoka Magica.* => Madoka
|
||||
@@ -186,6 +195,8 @@ connect_timeout:60.0
|
||||
# .*-Centered=>
|
||||
# characters=>Sam W\.=>Sam Witwicky&&category=>Transformers
|
||||
# characters=>Sam W\.=>Sam Winchester&&category=>Supernatural
|
||||
# category=>Bitextual=>M/M\,F/M
|
||||
|
||||
|
||||
## Include/Exclude metadata
|
||||
##
|
||||
@@ -210,7 +221,7 @@ connect_timeout:60.0
|
||||
##
|
||||
## This is fairly complicated, so it's documented on its own wiki
|
||||
## page:
|
||||
## https://code.google.com/p/fanficdownloader/wiki/InExcludeMetadataFeature
|
||||
## https://github.com/JimmXinu/FanFicFare/wiki/InExcludeMetadataFeature
|
||||
|
||||
## Some readers don't show horizontal rule (<hr />) tags correctly.
|
||||
## This replaces them all with a centered '* * *'. (Note centering
|
||||
@@ -333,12 +344,21 @@ anthology_title_pattern:${title} Anthology
|
||||
## any anthology tags.
|
||||
anthology_tags:Anthology
|
||||
|
||||
## Reorder ships so b/a and c/b/a become a/b and a/b/c. Only separates
|
||||
## on '/', so use replace_metadata to change separator first if
|
||||
## needed. Something like: ships=>[ ]*(/|&|&)[ ]*=>/ You can use
|
||||
## ships_LIST to change the / back to something else if you want.
|
||||
## Reorder ships so b/a and c/b/a become a/b and a/b/c. '/' is no
|
||||
## longer hard coded and can be changed and added to with
|
||||
## sort_ships_splits.
|
||||
sort_ships:false
|
||||
|
||||
## Each line indicates first a regex that should be used to split each
|
||||
## ships entry and then, after => the string to use to merge the parts
|
||||
## back together. \s == blank space.
|
||||
## Each part will have replace_metadata with key ships_CHARS applied.
|
||||
## By default, sort_ships_splits:/=>/
|
||||
## Here's possible an example for AO3:
|
||||
#sort_ships_splits:
|
||||
# [ ]*/[ ]*=>/
|
||||
# [ ]*&[ ]*=>\s&\s
|
||||
|
||||
## join_string_<entry> options -- FanFicFare list entries are comma
|
||||
## separated by default. You can use this to change that. For example,
|
||||
## if you want authors separated with ' & ' instead, use
|
||||
@@ -368,9 +388,8 @@ user_agent:FFF/2.X
|
||||
bulk_load:true
|
||||
|
||||
[base_xenforoforum]
|
||||
## Currently only forums.spacebattles.com and forums.sufficientvelocity.com
|
||||
|
||||
cover_exclusion_regexp:/clear.png
|
||||
cover_exclusion_regexp:/styles/
|
||||
|
||||
## I saw lots of chapters name simply '1.1' etc during testing.
|
||||
strip_chapter_numbers:false
|
||||
@@ -392,28 +411,40 @@ forumtags_label:Tags from Forum
|
||||
add_to_include_metadata_pre:
|
||||
# only keep tagsfromtitle with ( or [ in.
|
||||
tagsfromtitle=~[\[\(]
|
||||
|
||||
|
||||
## disable chapter range in title because of tagsfromtitle processing.
|
||||
title_chapter_range_pattern:
|
||||
|
||||
add_to_replace_metadata:
|
||||
# for QuestionableQuesting NSFW subforum.
|
||||
tagsfromtitle=>^\[NSFW\].*?([\(\[]([^\]\)]+)[\)\]]).*?$=>NSFW,\2
|
||||
# remove anything outside () or []
|
||||
tagsfromtitle=>^.*?([\(\[]([^\]\)]+)[\)\]]).*?$=>\1
|
||||
tagsfromtitle=>^.*?([\(\[]([^\]\)]+)[\)\]]).*?$=>\2
|
||||
# remove () []
|
||||
tagsfromtitle=>[\(\)\[\]]=>
|
||||
# tagsfromtitle=>[\(\)\[\]]=>
|
||||
# change (spaces)slash(or semicolon)(spaces) to comma
|
||||
tagsfromtitle=> *[/;] *=>,
|
||||
tagsfromtitle=> [xX] =>,
|
||||
|
||||
|
||||
# remove [] or () blocks and leading/trailing spaces/dashes/colons
|
||||
title=>[-: ]*[\(\[]([^\]\)]+)[\)\]][-: ]*=>
|
||||
# remove 'Thread' and the next word, usually "Thread 2", "Thread
|
||||
# four", "Thread iv", etc
|
||||
title,tagsfromtitle=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
|
||||
# four", "Thread iv", "Story Thread", etc
|
||||
title,tagsfromtitle=>[-: ]*(Story *)?[Tt]hread [^ ]+[-: ]*=>
|
||||
|
||||
# Normalize 'fanfiction/fanfic/fan-fiction' a little.
|
||||
forumtags=>[Ff]an-?[Ff]ic(tion)?=>FanFiction
|
||||
|
||||
add_to_extra_titlepage_entries:,tagsfromtitle,forumtags
|
||||
|
||||
## XenForo tags are all lowercase everywhere that I've seen. This
|
||||
## makes the first letter of each word uppercase. Applied before
|
||||
## replace_metadata.
|
||||
capitalize_forumtags:true
|
||||
|
||||
## Add both title tags and forumtags to standard (subject) tags.
|
||||
## '.SPLIT' tells the system to split by ','
|
||||
add_to_include_subject_tags:,tagsfromtitle.SPLIT
|
||||
## Or to also include forumtags add this to personal.ini:
|
||||
#add_to_include_subject_tags:,forumtags
|
||||
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
|
||||
@@ -425,6 +456,13 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## the description.
|
||||
description_limit:500
|
||||
|
||||
## Because base_xenforoforum adapters can pull chapter URLs from human
|
||||
## posts, the odds of errors in the chapter URLs are vastly higher.
|
||||
## You can set continue_on_chapter_error:true to continue on after
|
||||
## failing to download a chapter and instead record an error message
|
||||
## in the ebook for that chapter.
|
||||
continue_on_chapter_error:false
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
|
||||
@@ -617,6 +655,7 @@ nook_img_fix:true
|
||||
## URLs like: http://test1.com?sid=12345
|
||||
[test1.com]
|
||||
extratags: FanFiction,Testing
|
||||
|
||||
# extracategories:Fafner
|
||||
# extragenres:Romance,Fluff
|
||||
# extracharacters:Reginald Smythe-Smythe,Mokona,Harry P.
|
||||
@@ -679,7 +718,7 @@ extratags: FanFiction,Testing,HTML
|
||||
|
||||
## AO3 adapter defines a few extra metadata entries.
|
||||
## If there's ever more than 4 series, add series04,series04Url etc.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url,series00HTML,series01HTML,series02HTML,series03HTML
|
||||
fandoms_label:Fandoms
|
||||
freeformtags_label:Freeform Tags
|
||||
freefromtags_label:Freeform Tags
|
||||
@@ -689,6 +728,14 @@ kudos_label:Kudos
|
||||
hits_label:Hits
|
||||
collections_label:Collections
|
||||
bookmarks_label:Bookmarks
|
||||
series00HTML_label:Series
|
||||
series01HTML_label:Additional Series
|
||||
series02HTML_label:Additional Series
|
||||
series03HTML_label:Additional Series
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:series00,series01,series02,series03
|
||||
|
||||
## AO3 doesn't have anything it calls 'genre'. The adapter used to be
|
||||
## hardcoded to include the site specific metadata freeformtags &
|
||||
@@ -707,7 +754,7 @@ include_in_category:fandoms
|
||||
include_in_freefromtags:freeformtags
|
||||
|
||||
## adds to titlepage_entries instead of replacing it.
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series01HTML,series02HTML,series03HTML
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
@@ -857,12 +904,6 @@ extracategories:Buffy: The Vampire Slayer
|
||||
extracharacters:Buffy, Spike
|
||||
extraships:Spike/Buffy
|
||||
|
||||
[devianthearts.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
|
||||
|
||||
[dramione.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1268,7 +1309,7 @@ extracategories:NCIS
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
extracharacters:Willow
|
||||
|
||||
[ninelives.dark-solace.org]
|
||||
[ninelivesarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:The Walking Dead
|
||||
extracharacters:Carol,Daryl
|
||||
@@ -1347,6 +1388,16 @@ extracategories:My Little Pony: Friendship is Magic
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:The Pretender
|
||||
|
||||
[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
|
||||
|
||||
[samandjack.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1516,22 +1567,6 @@ readings_label: Readings
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[thequidditchpitch.org]
|
||||
## 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
|
||||
|
||||
[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,
|
||||
@@ -1901,7 +1936,7 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:NCIS
|
||||
|
||||
[www.nickandgreg.net]
|
||||
[www.nickngreg.nl]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:CSI
|
||||
extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
@@ -59,6 +59,7 @@ Dup from another site'''
|
||||
default_prefs['reject_always'] = False
|
||||
|
||||
default_prefs['updatemeta'] = True
|
||||
default_prefs['bgmeta'] = False
|
||||
default_prefs['updateepubcover'] = False
|
||||
default_prefs['keeptags'] = False
|
||||
default_prefs['suppressauthorsort'] = False
|
||||
|
||||
+469
-425
File diff suppressed because it is too large
Load Diff
+448
-405
File diff suppressed because it is too large
Load Diff
+449
-406
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+452
-408
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+447
-404
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,6 @@ import adapter_ficbooknet
|
||||
import adapter_portkeyorg
|
||||
import adapter_mugglenetcom
|
||||
import adapter_hpfandomnet
|
||||
import adapter_thequidditchpitchorg
|
||||
import adapter_nfacommunitycom
|
||||
import adapter_midnightwhispersca
|
||||
import adapter_ksarchivecom
|
||||
@@ -132,13 +131,12 @@ import adapter_csiforensicscom
|
||||
import adapter_lotrfanfictioncom
|
||||
import adapter_fhsarchivecom
|
||||
import adapter_fanfictionjunkiesde
|
||||
import adapter_devianthearts
|
||||
import adapter_tgstorytimecom
|
||||
import adapter_itcouldhappennet
|
||||
import adapter_forumsspacebattlescom
|
||||
import adapter_forumssufficientvelocitycom
|
||||
import adapter_questionablequestingcom
|
||||
import adapter_ninelivesdarksolaceorg
|
||||
import adapter_forumquestionablequestingcom
|
||||
import adapter_ninelivesarchivecom
|
||||
import adapter_masseffect2in
|
||||
import adapter_quotevcom
|
||||
|
||||
@@ -240,8 +238,11 @@ def getClassFor(url):
|
||||
fixedurl = "http:%s"%url
|
||||
if not fixedurl.startswith("http"):
|
||||
fixedurl = "http://%s"%url
|
||||
## remove any trailing '#' locations.
|
||||
fixedurl = re.sub(r"#.*$","",fixedurl)
|
||||
|
||||
## remove any trailing '#' locations, except for #post-12345 for
|
||||
## XenForo
|
||||
if not "#post-" in fixedurl:
|
||||
fixedurl = re.sub(r"#.*$","",fixedurl)
|
||||
|
||||
parsedUrl = up.urlparse(fixedurl)
|
||||
domain = parsedUrl.netloc.lower()
|
||||
|
||||
@@ -137,7 +137,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ''
|
||||
while value and not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
# sometimes poorly formated desc (<p> w/o </p>) leads
|
||||
# to all labels being included.
|
||||
|
||||
@@ -194,7 +194,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
val = val.nextSibling
|
||||
val = val.nextSibling
|
||||
while not defaultGetattr(val) == 'table':
|
||||
svalue += str(val)
|
||||
svalue += unicode(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ class BdsmGeschichtenAdapter(BaseSiteAdapter):
|
||||
nextLinkMatch = _REGEX_TRAILING_DIGIT.match(nextLink)
|
||||
if nextLinkMatch is not None:
|
||||
curChap = nextLinkMatch.group(1)
|
||||
nextLink = re.sub(_REGEX_TRAILING_DIGIT, str(int(curChap) + 1), nextLink)
|
||||
nextLink = re.sub(_REGEX_TRAILING_DIGIT, unicode(int(curChap) + 1), nextLink)
|
||||
else:
|
||||
break
|
||||
try:
|
||||
@@ -289,7 +289,7 @@ class BdsmGeschichtenAdapter(BaseSiteAdapter):
|
||||
curMax = self.maxChapter + step
|
||||
lastHit = True
|
||||
while True:
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, str(curMax), self.url)
|
||||
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;
|
||||
@@ -316,8 +316,8 @@ class BdsmGeschichtenAdapter(BaseSiteAdapter):
|
||||
logger.debug(curMax)
|
||||
|
||||
for i in xrange(1, self.maxChapter):
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, str(i), self.url)
|
||||
nextChapterTitle = re.sub("1", str(i), title1)
|
||||
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):
|
||||
|
||||
@@ -62,7 +62,7 @@ class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
return cls.READ_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.BASE_URL + 'stories.php?go=') + r'(read|chapters)\&no=\d+$'
|
||||
return re.escape(self.BASE_URL + 'stories.php?go=') + r'(read|chapters)\&(amp;)?no=\d+$'
|
||||
|
||||
# Override stripURLParameters so the "no" parameter won't get stripped
|
||||
@classmethod
|
||||
|
||||
@@ -244,7 +244,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
|
||||
## Everything until the next strong tag.
|
||||
svalue = ""
|
||||
while not isinstance(value,bs.Tag) or value.name != 'strong':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -156,7 +156,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
val = value
|
||||
value = value.previousSibling
|
||||
while not defaultGetattr(val,'class') == 'label':
|
||||
svalue += str(val)
|
||||
svalue += unicode(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 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 re
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class DeviantHeartsAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'devianthearts.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'devhrt'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
def getClass():
|
||||
return DeviantHeartsAdapter
|
||||
|
||||
@@ -217,7 +217,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
## Everything until the next strong tag.
|
||||
svalue = ""
|
||||
while not isinstance(value,bs.Tag) or value.name != 'strong':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -147,8 +147,8 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while 'Rating' not in str(value):
|
||||
svalue += str(value)
|
||||
while 'Rating' not in unicode(value):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -172,7 +172,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
val = value
|
||||
value = value.previousSibling
|
||||
while not defaultGetattr(val,'class') == 'label':
|
||||
svalue += str(val)
|
||||
svalue += unicode(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -165,7 +165,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
i=0
|
||||
for part in pr:
|
||||
i=i+len(stripHTML(part).split(' '))
|
||||
self.story.setMetadata('numWords', str(i))
|
||||
self.story.setMetadata('numWords', unicode(i))
|
||||
|
||||
i=0
|
||||
fandoms = table.findAll('a', href=re.compile(r'/fanfiction/\w+'))
|
||||
|
||||
@@ -159,7 +159,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
## perhaps not the most efficient way to parse this, using
|
||||
## regexps for each rather than something more complex, but
|
||||
## IMO, it's more readable and amenable to change.
|
||||
metastr = stripHTML(str(metap)).replace('\n',' ').replace('\t',' ').replace(u'\u00a0',' ')
|
||||
metastr = stripHTML(unicode(metap)).replace('\n',' ').replace('\t',' ').replace(u'\u00a0',' ')
|
||||
|
||||
m = re.match(r".*?Rating: (.+?) -.*?",metastr)
|
||||
if m:
|
||||
|
||||
@@ -160,7 +160,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
skip=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+str(skip)))
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+unicode(skip)))
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
|
||||
+1
-13
@@ -32,17 +32,5 @@ class QuestionablequestingComAdapter(BaseXenForoForumAdapter):
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'questionablequesting.com'
|
||||
return 'forum.questionablequesting.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ["forum."+cls.getSiteDomain(),cls.getSiteDomain()]
|
||||
|
||||
@classmethod
|
||||
def getConfigSections(cls):
|
||||
"Only needs to be overriden if has additional ini sections."
|
||||
return ['base_xenforoforum','forum.'+cls.getSiteDomain(),cls.getSiteDomain()]
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r'https?://(forum.)?'+re.escape(self.getSiteDomain())+r'/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?'
|
||||
|
||||
@@ -113,7 +113,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
tdstr = tr.findAll('td')[2].string
|
||||
if tdstr and tdstr.isdigit():
|
||||
words+=int(tdstr)
|
||||
self.story.setMetadata('numWords',str(words))
|
||||
self.story.setMetadata('numWords',unicode(words))
|
||||
|
||||
# Find the chapters:
|
||||
tablelist = soup.find('table',{'class':'text'})
|
||||
@@ -135,7 +135,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
#for center in soup.findAll('center'):
|
||||
table = soup.find('table',{'class':'storymaininfo'})
|
||||
if table:
|
||||
metastr = stripHTML(str(table)).replace('\n',' ').replace('\t',' ')
|
||||
metastr = stripHTML(unicode(table)).replace('\n',' ').replace('\t',' ')
|
||||
# Rating: 12+ Story Reviews: 3
|
||||
# Chapters: 3
|
||||
# Characters: Andromeda, Ted, Bellatrix, R. Lestrange, Lucius, Narcissa, OC
|
||||
|
||||
@@ -147,7 +147,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'classification':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -22,7 +22,6 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
@@ -93,14 +92,16 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
soup = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
# set first URL
|
||||
a = soup.find('a', href=re.compile(r"viewstory.php\?sid=\d+"))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/eff/'+a['href'])
|
||||
## href = "javascript:if (confirm('Slash/het fiction which incorporates sexual situations to a somewhat graphic degree as well as graphic violent situations. ')) location = 'viewstory.php?sid=49111&i=1'"
|
||||
m = re.match(r'.*?(viewstory.php\?sid=\d+)&i=\d+.*?',a['href'])
|
||||
self._setURL('http://'+self.host+'/eff/'+m.group(1))
|
||||
if self.parsedUrl.query.split('=',)[1] != self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
url = self.url
|
||||
@@ -114,7 +115,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
soup = self.make_soup(data)
|
||||
|
||||
# self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(a).group('storyId'))
|
||||
|
||||
@@ -129,14 +130,13 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
# fix a typo in the site HTML so I can find the Characters list.
|
||||
authdata = authdata.replace('<td width=10%">','<td width="10%">')
|
||||
|
||||
# hpfandom.net only seems to indicate adult-only by javascript on the story/chapter links.
|
||||
if "javascript:if (confirm('Slash/het fiction which incorporates sexual situations to a somewhat graphic degree and some violence. ')) location = 'viewstory.php?sid=%s'"%self.story.getMetadata('storyId') in authdata \
|
||||
if "javascript:if (confirm('Slash/het fiction which incorporates sexual situations to a somewhat graphic degree as well as graphic violent situations. ')) location = 'viewstory.php?sid=%s&i=1'"%self.story.getMetadata('storyId') in authdata \
|
||||
and not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
authsoup = bs.BeautifulSoup(authdata)
|
||||
authsoup = self.make_soup(authdata)
|
||||
|
||||
reviewsa = authsoup.find('a', href="reviews.php?sid="+self.story.getMetadata('storyId')+"&a=")
|
||||
reviewsa = authsoup.find('a', href=re.compile(r"reviews\.php\?sid="+self.story.getMetadata('storyId')+r".*"))
|
||||
# <table><tr><td><p><b><a ...>
|
||||
metablock = reviewsa.findParent("table")
|
||||
#print("metablock:%s"%metablock)
|
||||
@@ -243,7 +243,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
data = re.sub(r'<table width="100%">.*?</table>','</div>',
|
||||
data,count=1,flags=re.DOTALL)
|
||||
|
||||
soup = bs.BeautifulStoneSoup(data,selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
div = soup.find("div",{'name':'storybody'})
|
||||
#print("\n\ndiv:%s\n\n"%div)
|
||||
|
||||
@@ -222,7 +222,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -207,7 +207,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -195,7 +195,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
# poor HTML(unclosed <p> for one) can cause run on
|
||||
# over the next label.
|
||||
if '<span class="label">' in svalue:
|
||||
|
||||
@@ -237,12 +237,12 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# find num pages
|
||||
pgs = int(soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0])
|
||||
logger.debug("pages: "+str(pgs))
|
||||
logger.debug("pages: "+unicode(pgs))
|
||||
|
||||
# get all the pages
|
||||
for i in xrange(2, pgs+1):
|
||||
try:
|
||||
logger.debug("fetching page "+str(i))
|
||||
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
|
||||
|
||||
@@ -156,7 +156,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
val = value
|
||||
value = value.previousSibling
|
||||
while not defaultGetattr(val,'class') == 'label':
|
||||
svalue += str(val)
|
||||
svalue += unicode(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ class MassEffect2InAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('status', 'In Progress' if storyInProgress else 'Completed')
|
||||
self.story.setMetadata('datePublished', datePublished)
|
||||
self.story.setMetadata('dateUpdated', dateUpdated)
|
||||
self.story.setMetadata('numWords', str(wordCount))
|
||||
self.story.setMetadata('numWords', unicode(wordCount))
|
||||
self.story.setMetadata('numChapters', chapterCount)
|
||||
|
||||
# Site-specific metadata.
|
||||
|
||||
@@ -41,6 +41,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
urltitle='urltitle'
|
||||
cattitle='cattitle'
|
||||
if m:
|
||||
if m.group('id1'):
|
||||
self.story.setMetadata('storyId',m.group('id1'))
|
||||
@@ -50,13 +51,21 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
urltitle=m.group('urltitle2')
|
||||
elif m.group('id3'):
|
||||
self.story.setMetadata('storyId',m.group('id3'))
|
||||
elif m.group('id4'):
|
||||
self.story.setMetadata('storyId',m.group('id4'))
|
||||
cattitle=m.group('cattitle4')
|
||||
urltitle=m.group('urltitle4')
|
||||
elif m.group('id5'):
|
||||
self.story.setMetadata('storyId',m.group('id5'))
|
||||
cattitle=m.group('cattitle5')
|
||||
urltitle=m.group('urltitle5')
|
||||
else:
|
||||
raise InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfic/s/'+urltitle+'/'+self.story.getMetadata('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfic/s/'+cattitle+'/'+urltitle+'/'+self.story.getMetadata('storyId'))
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
@@ -72,7 +81,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fanfic/s/story-title/123456 http://"+cls.getSiteDomain()+"/fanfic/c/story-title/chapter-title/123456/987612"
|
||||
return "http://"+cls.getSiteDomain()+"/fanfic/s/category-name/story-title/123456 http://"+cls.getSiteDomain()+"/fanfic/c/category-name/story-title/123456/987612"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
## old urls
|
||||
@@ -82,8 +91,13 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
## http://www.mediaminer.org/fanfic/c/ghosts-from-the-past/chapter-2/72/174
|
||||
## http://www.mediaminer.org/fanfic/s/robtech-final-missions/61553
|
||||
## http://www.mediaminer.org/fanfic/c/robtech-final-missions/robotech-final-missions-oneshot/61553/189830
|
||||
## even newer urls
|
||||
## http://www.mediaminer.org/fanfic/s/gundam-wing-fan-fiction/the-preventer-operatives/171000
|
||||
## http://www.mediaminer.org/fanfic/c/gundam-wing-fan-fiction/the-preventer-operatives/171000/608822
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/fanfic/"+\
|
||||
r"((s/(?P<urltitle1>[^/]+)/(?P<id1>\d+))|"+\
|
||||
r"((s/(?P<cattitle4>[^/]+)/(?P<urltitle4>[^/]+)/(?P<id4>\d+))|"+\
|
||||
r"((c/(?P<cattitle5>[^/]+)/(?P<urltitle5>[^/]+)/(?P<id5>\d+))/\d+)|"+\
|
||||
r"(s/(?P<urltitle1>[^/]+)/(?P<id1>\d+))|"+\
|
||||
r"((c/(?P<urltitle2>[^/]+)/[^/]+/(?P<id2>\d+))/\d+)|"+\
|
||||
r"(view_st\.php/(?P<id3>\d+)))"
|
||||
|
||||
@@ -109,40 +123,29 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
# [ A - All Readers ], strip '[' ']'
|
||||
## title:
|
||||
## <h1 id="post-title">A, A' Fan Fiction ❯ Mmmmm</h1>
|
||||
titletext = stripHTML(soup.find("h1",{"id":"post-title"}))
|
||||
titletext = titletext[titletext.index(u'❯')+2:]
|
||||
# print("title:(%s)"%titletext)
|
||||
self.story.setMetadata('title',titletext)
|
||||
|
||||
# [ A - All Readers ], strip '[ ' ' ]'
|
||||
## Above title because we remove the smtxt font to get title.
|
||||
smtxt = soup.find("h3",{"id":"post-rating"})
|
||||
smtxt = soup.find("div",{"id":"post-rating"})
|
||||
if not smtxt:
|
||||
logger.error("can't find rating")
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
rating = smtxt.string[1:-1]
|
||||
rating = smtxt.string[2:-2]
|
||||
self.story.setMetadata('rating',rating)
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"/fanfic/src.php/u/\d+"))
|
||||
a = soup.find('a', href=re.compile(r"/user_info.php/\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[-1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
## Title - Good grief. Title varies by chaptered, 1chapter and 'type=one shot'--and even 'one-shot's can have titled chapter.
|
||||
## But, if colspan=2, there's no chapter title.
|
||||
## <td class="ffh">Atmosphere: Chapter 1</b> <font class="smtxt">[ P - Pre-Teen ]</font></td>
|
||||
## <td colspan=2 class="ffh">Hearts of Ice <font class="smtxt">[ P - Pre-Teen ]</font></td>
|
||||
## <td colspan=2 class="ffh">Suzaku no Princess <font class="smtxt">[ P - Pre-Teen ]</font></td>
|
||||
## <td class="ffh">The Kraut, The Bartender, and The Drunkard: Chapter 1</b> <font class="smtxt">[ P - Pre-Teen ]</font></td>
|
||||
## <td class="ffh">Betrayal and Justice: A Cold Heart</b> <font size="-1">( Chapter 1 )</font> <font class="smtxt">[ A - All Readers ]</font></td>
|
||||
## <td class="ffh">Question and Answer: Question and Answer</b> <font size="-1">( One-Shot )</font> <font class="smtxt">[ A - All Readers ]</font></td>
|
||||
# title = soup.find('td',{'class':'ffh'})
|
||||
# for font in title.findAll('font'):
|
||||
# font.extract() # removes 'font' tags from inside the td.
|
||||
# if title.has_attr('colspan'):
|
||||
# titlet = stripHTML(title)
|
||||
# else:
|
||||
# ## No colspan, it's part chapter title--even if it's a one-shot.
|
||||
# titlet = ':'.join(stripHTML(title).split(':')[:-1]) # strip trailing 'Chapter X' or chapter title
|
||||
self.story.setMetadata('title',stripHTML(soup.find('h1',{'id':'post-title'})))
|
||||
|
||||
# save date from first for later.
|
||||
firstdate=None
|
||||
|
||||
@@ -155,7 +158,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# category
|
||||
# <a href="/fanfic/src.php/a/567">Ranma 1/2</a>
|
||||
for a in soup.findAll('a',href=re.compile(r"^/fanfic/src.php/a/")):
|
||||
for a in soup.findAll('a',href=re.compile(r"^/fanfic/a/")):
|
||||
self.story.addToList('category',a.string)
|
||||
|
||||
# genre
|
||||
|
||||
@@ -206,7 +206,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -190,7 +190,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -240,7 +240,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
# ## Everything until the next span class='label'
|
||||
# svalue = ""
|
||||
# while not defaultGetattr(value,'class') == 'label':
|
||||
# svalue += str(value)
|
||||
# svalue += unicode(value)
|
||||
# value = value.nextSibling
|
||||
# self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -194,7 +194,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -65,14 +65,18 @@ class NickAndGregNetAdapter(BaseSiteAdapter):
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.nickandgreg.net'
|
||||
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 re.escape("http://"+self.getSiteDomain()+"/desert_archive/viewstory.php?sid=")+r"\d+$"
|
||||
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.
|
||||
|
||||
+21
-4
@@ -19,12 +19,29 @@
|
||||
import re
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class NineLivesDarkSolaceAdapter(BaseEfictionAdapter):
|
||||
class NineLivesAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'ninelives.dark-solace.org'
|
||||
return 'ninelivesarchive.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['ninelivesarchive.com','ninelives.dark-solace.org']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
@classmethod
|
||||
def getSiteURLPattern(self):
|
||||
return "http://("+self.getSiteDomain()+"|ninelives.dark-solace.org)"+re.escape("/viewstory.php?sid=")+r"(?P<storyId>\d+)$"
|
||||
|
||||
@classmethod
|
||||
def getConfigSections(cls):
|
||||
"Only needs to be overriden if has additional ini sections."
|
||||
return ['base_efiction','ninelives.dark-solace.org',cls.getSiteDomain()]
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return '9lvs'
|
||||
@@ -32,7 +49,7 @@ class NineLivesDarkSolaceAdapter(BaseEfictionAdapter):
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%B %d, %Y"
|
||||
|
||||
|
||||
def getClass():
|
||||
return NineLivesDarkSolaceAdapter
|
||||
return NineLivesAdapter
|
||||
|
||||
@@ -196,7 +196,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
val = val.nextSibling
|
||||
val = val.nextSibling
|
||||
while not defaultGetattr(val) == 'table':
|
||||
svalue += str(val)
|
||||
svalue += unicode(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while value and not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -181,7 +181,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
value = section.nextSibling
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
svalue = ""
|
||||
value = labelspan.nextSibling
|
||||
while not defaultGetattr(value,'class') == 'dark-small-bold':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
|
||||
for genre in svalue.split("/"):
|
||||
|
||||
@@ -123,7 +123,7 @@ class PotionsAndSnitchesOrgSiteAdapter(BaseSiteAdapter):
|
||||
## Everything until the next div class='listbox'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'listbox':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -155,7 +155,10 @@ class PotterFicsComAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
raise e
|
||||
|
||||
#print data
|
||||
if "Esta historia no existe. Probablemente ha sido eliminada." in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
##print data
|
||||
|
||||
#deal with adult content login
|
||||
if self.needToLoginCheck(data):
|
||||
@@ -242,7 +245,7 @@ class PotterFicsComAdapter(BaseSiteAdapter):
|
||||
chnum = 0
|
||||
for li in list:
|
||||
chnum += 1
|
||||
chTitle = str(chnum) + '. ' + li.a.b.string.strip()
|
||||
chTitle = unicode(chnum) + '. ' + li.a.b.string.strip()
|
||||
chURL = makeAbsoluteURL(li.a['href'])
|
||||
chapters.append((chTitle,chURL))
|
||||
#Get reviews, add to total
|
||||
@@ -260,7 +263,7 @@ class PotterFicsComAdapter(BaseSiteAdapter):
|
||||
mb = list.parent
|
||||
#get the div before that, will either be the description, or the google ad bar
|
||||
mb = mb.findPreviousSibling('div')
|
||||
if 'google_ad_client' in str(mb):
|
||||
if 'google_ad_client' in unicode(mb):
|
||||
#couldn't find description, leaving it blank
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -217,7 +217,7 @@ class PotterHeadsAnonymousComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -171,7 +171,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -166,7 +166,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -179,7 +179,7 @@ class QafFicComAdapter(BaseSiteAdapter):
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'classification' and value != None:
|
||||
if "Featured Stories" not in value:
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -59,10 +59,25 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
|
||||
self.story.setMetadata('title', element.find('span', recursive=False).get_text())
|
||||
|
||||
element = soup.find('div', {'class': 'desc_creator'})('a')[1]
|
||||
self.story.setMetadata('author', element.get_text())
|
||||
self.story.setMetadata('authorId', get_url_path_segments(element['href'])[0])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.url, element['href']))
|
||||
element = soup.find('div', {'class': 'desc_creator'})
|
||||
if element:
|
||||
a = element('a')[1]
|
||||
self.story.setMetadata('author', a.get_text())
|
||||
self.story.setMetadata('authorId', get_url_path_segments(a['href'])[0])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.url, a['href']))
|
||||
|
||||
# Multiple authors
|
||||
else:
|
||||
element = soup.find('div', id='qheadx')
|
||||
for a in element('div', recursive=False)[1]('a'):
|
||||
author = a.get_text()
|
||||
if not a.get_text():
|
||||
continue
|
||||
|
||||
self.story.addToList('author', author)
|
||||
self.story.addToList('authorId', get_url_path_segments(a['href'])[0])
|
||||
self.story.addToList('authorUrl', urlparse.urljoin(self.url, a['href']))
|
||||
|
||||
self.setDescription(self.url, soup.find('div', id='qdesct'))
|
||||
self.setCoverImage(self.url, urlparse.urljoin(self.url, soup.find('img', {'class': 'logo'})['src']))
|
||||
|
||||
@@ -74,7 +89,8 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
|
||||
elements = soup('span', {'class': 'q_time'})
|
||||
self.story.setMetadata('datePublished', datetime.datetime.fromtimestamp(float(elements[0]['ts'])))
|
||||
self.story.setMetadata('dateUpdated', datetime.datetime.fromtimestamp(float(elements[1]['ts'])))
|
||||
if len(elements) > 1:
|
||||
self.story.setMetadata('dateUpdated', datetime.datetime.fromtimestamp(float(elements[1]['ts'])))
|
||||
|
||||
for a in soup.find('div', id='rselect')('a'):
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
@@ -82,8 +98,7 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
element = soup.find('div', {'class': 't'})('div', recursive=False)[1].div
|
||||
data = filter(None, (x.strip() for x in element('span')[1].next_sibling.split(u'\xb7')))
|
||||
|
||||
data = filter(None, (x.strip() for x in element.get_text().split(u'\xb7')))
|
||||
if 'completed' in data:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
data.remove('completed')
|
||||
@@ -91,11 +106,17 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
for datum in data:
|
||||
value, key = datum.split()
|
||||
parts = datum.split()
|
||||
# Not a valid metadatum
|
||||
if not len(parts) == 2:
|
||||
continue
|
||||
|
||||
key, value = parts
|
||||
self.story.setMetadata(key, value.replace(',', '').replace('.', ''))
|
||||
|
||||
self.story.setMetadata('favorites', soup.find('div', id='favqn').get_text())
|
||||
self.story.setMetadata('comments', soup.find('a', id='comment_btn').span.get_text())
|
||||
element = soup.find('a', id='comment_btn').span
|
||||
self.story.setMetadata('comments', element.get_text() if element else 0)
|
||||
|
||||
def getChapterText(self, url):
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
@@ -151,7 +151,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'classification':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -251,10 +251,10 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
svalue = ""
|
||||
while i == 0:
|
||||
try:
|
||||
b = str(a)
|
||||
b = unicode(a)
|
||||
svalue += b.split('Summary: ')[1]
|
||||
except:
|
||||
svalue += str(a)
|
||||
svalue += unicode(a)
|
||||
if a.nextSibling != None:
|
||||
a = a.nextSibling
|
||||
else:
|
||||
|
||||
@@ -175,7 +175,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -176,7 +176,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -166,7 +166,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -158,7 +158,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'classification':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -178,7 +178,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
page=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+str(page)))
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+unicode(page)))
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
|
||||
@@ -172,7 +172,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -72,8 +72,13 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
#print("set:%s->%s"%(key,self.story.getMetadata(key)))
|
||||
|
||||
self.chapterUrls = []
|
||||
for (j,chap) in enumerate(self.get_config_list(sections,'chaptertitles'),start=1):
|
||||
self.chapterUrls.append( (chap,self.url+"&chapter=%d"%j) )
|
||||
if self.has_config(sections,'chapter_urls'):
|
||||
for l in self.get_config(sections,'chapter_urls').splitlines() :
|
||||
if l:
|
||||
self.chapterUrls.append( (l[1+l.index(','):],l[:l.index(',')]) )
|
||||
else:
|
||||
for (j,chap) in enumerate(self.get_config_list(sections,'chaptertitles'),start=1):
|
||||
self.chapterUrls.append( (chap,self.url+"&chapter=%d"%j) )
|
||||
# self.chapterUrls = [(u'Prologue '+self.crazystring,self.url+"&chapter=1"),
|
||||
# ('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"),
|
||||
# ]
|
||||
@@ -213,7 +218,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
self.story.addToList('category','SG-1')
|
||||
self.story.addToList('genre','Porn')
|
||||
self.story.addToList('genre','Drama')
|
||||
else:
|
||||
elif idnum < 1000:
|
||||
self.story.setMetadata('authorId','98765')
|
||||
self.story.setMetadata('authorUrl','http://author/url')
|
||||
|
||||
@@ -330,7 +335,8 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
<p>http://test1.com?sid=671 - Succeeds, but sleeps 2sec metadata only</p>
|
||||
<p>http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only</p>
|
||||
<p>http://test1.com?sid=673 - Succeeds, multiple authors, extra categories, genres</p>
|
||||
<p>http://test1.com?sid=673 - Succeeds, no numWords set</p>
|
||||
<p>http://test1.com?sid=674 - Succeeds, no numWords set</p>
|
||||
<p>http://test1.com?sid=700 - 710 - Succeeds, changes sid to 80X</p>
|
||||
<p>http://test1.com?sid=0 - Succeeds, generates some text specifically for testing hyphenation problems with Nook STR/STRwG</p>
|
||||
<p>Odd sid's will be In-Progress, evens complete. sid<10 will be assigned one of four languages and included in a series.</p>
|
||||
</div>
|
||||
@@ -347,13 +353,55 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
<br />
|
||||
</div>
|
||||
'''
|
||||
elif self.story.getMetadata('storyId') == '667':
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
||||
elif 'test1.com' not in url:
|
||||
## for chapter_urls setting.
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
try:
|
||||
origurl = url
|
||||
(data,opened) = self._fetchUrlOpened(url,extrasleep=2.0)
|
||||
url = opened.geturl()
|
||||
if '#' in origurl and '#' not in url:
|
||||
url = url + origurl[origurl.index('#'):]
|
||||
logger.debug("chapter URL redirected to: %s"%url)
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
if '#' in url:
|
||||
anchorid = url.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
|
||||
bq = soup.find('blockquote')
|
||||
|
||||
bq.name='div'
|
||||
|
||||
for iframe in bq.find_all('iframe'):
|
||||
iframe.extract() # calibre book reader & editor don't like iframes to youtube.
|
||||
|
||||
for qdiv in bq.find_all('div',{'class':'quoteExpand'}):
|
||||
qdiv.extract() # Remove <div class="quoteExpand">click to expand</div>
|
||||
|
||||
except Exception as e:
|
||||
if self.getConfig('continue_on_chapter_error'):
|
||||
bq = self.make_soup("""<div>
|
||||
<p><b>Error</b></p>
|
||||
<p>FanFicFare failed to download this chapter. Because you have
|
||||
<b>continue_on_chapter_error</b> set to <b>true</b> in your personal.ini, the download continued.</p>
|
||||
<p>Chapter URL:<br>%s</p>
|
||||
<p>Error:<br><pre>%s</pre></p>
|
||||
</div>"""%(url,traceback.format_exc()))
|
||||
else:
|
||||
raise
|
||||
|
||||
return self.utf8FromSoup(url[:url.index('/',8)+1],bq)
|
||||
|
||||
else:
|
||||
if self.story.getMetadata('storyId') == '667':
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
||||
|
||||
text=u'''
|
||||
<div>
|
||||
<h3 extra="value">Chapter title from site</h3>
|
||||
<p>chapter URL:'''+url+'''</p>
|
||||
<p>Timestamp:'''+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")+'''</p>
|
||||
<p>Lorem '''+self.crazystring+u''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
br breaks<br><br>
|
||||
|
||||
@@ -135,7 +135,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -215,7 +215,7 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -161,7 +161,7 @@ class ThePetulantPoetessComAdapter(BaseSiteAdapter):
|
||||
index = 1
|
||||
found = 0
|
||||
while found == 0:
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&page="+str(index)))
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&page="+unicode(index)))
|
||||
|
||||
for info in asoup.findAll('td', {'class' : 'highlightcolor1'}):
|
||||
a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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 .. import BeautifulSoup as bs
|
||||
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 TheQuidditchPitchOrgAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class TheQuidditchPitchOrgAdapter(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 part. Replace all to remove it usually.
|
||||
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','tqdpch') # 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
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'thequidditchpitch.org' # XXX
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.thequidditchpitch.org','thequidditchpitch.org']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only - Not suitable for readers under the age of legal consent in their country.' 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 = "&ageconsent=ok&warning=4" # XXX
|
||||
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)
|
||||
|
||||
# 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.
|
||||
if ("Not suitable for readers under the age of legal consent in their country." in data \
|
||||
or "Not suitable for readers under 16 yrs. \r\nStories may contain violence, slight nudity, and/or sexual situations." in data ) \
|
||||
and not (self.is_adult or self.getConfig("is_adult")): # XXX
|
||||
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 = bs.BeautifulSoup(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))
|
||||
|
||||
# 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 not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(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'))
|
||||
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)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
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')) # 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 = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
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 = bs.BeautifulStoneSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
# span? Really? span? Yeah... I don't think so.
|
||||
div = soup.find('span', {'style' : 'font-size: 100%;'})
|
||||
div.name='div'
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -193,7 +193,7 @@ class TolkienFanfictionAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('characters', character)
|
||||
logger.debug("Characters: %s" % self.story.getMetadata('characters'))
|
||||
|
||||
logger.debug('Title as `str`: ' + str(title))
|
||||
logger.debug('Title as `str`: ' + unicode(title))
|
||||
# For publication date we need to search
|
||||
try:
|
||||
queryString = urllib.urlencode((
|
||||
|
||||
@@ -217,7 +217,7 @@ class TrekiverseOrgAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ''
|
||||
while value and not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
# sometimes poorly formated desc (<p> w/o </p>) leads
|
||||
# to all labels being included.
|
||||
|
||||
@@ -170,7 +170,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
# poor HTML(unclosed <p> for one) can cause run on
|
||||
# over the next label.
|
||||
if '<span class="label">' in svalue:
|
||||
|
||||
@@ -162,7 +162,7 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -124,7 +124,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
# link instead to find the appropriate metadata.
|
||||
a = soup.find('a', href=re.compile(r'reviews.php\?sid='+self.story.getMetadata('storyId')))
|
||||
metadata = a.findParent('td')
|
||||
metadatachunks = self.utf8FromSoup(None,metadata).split('<br />')
|
||||
metadatachunks = self.utf8FromSoup(None,metadata,allow_replace_br_with_p=False).split('<br />')
|
||||
# process metadata for this story.
|
||||
self.setDescription(url,metadatachunks[1])
|
||||
#self.story.setMetadata('description', metadatachunks[1])
|
||||
|
||||
@@ -131,8 +131,8 @@ class WolverineAndRogueComAdapter(BaseSiteAdapter):
|
||||
while value != None:
|
||||
val = value
|
||||
value = value.previousSibling
|
||||
while "Categories" not in str(val):
|
||||
svalue += str(val)
|
||||
while "Categories" not in unicode(val):
|
||||
svalue += unicode(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class BaseSiteAdapter(Configurable):
|
||||
return (self._decode(data),opened)
|
||||
except u2.HTTPError, he:
|
||||
excpt=he
|
||||
if he.code == 404:
|
||||
if he.code in (403,404):
|
||||
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(he)))
|
||||
break # break out on 404
|
||||
except Exception, e:
|
||||
@@ -557,7 +557,7 @@ class BaseSiteAdapter(Configurable):
|
||||
# This gives us a unicode object, not just a string containing bytes.
|
||||
# (I gave soup a unicode string, you'd think it could give it back...)
|
||||
# Now also does a bunch of other common processing for us.
|
||||
def utf8FromSoup(self,url,soup,fetch=None):
|
||||
def utf8FromSoup(self,url,soup,fetch=None,allow_replace_br_with_p=True):
|
||||
if not fetch:
|
||||
fetch=self._fetchUrlRaw
|
||||
|
||||
@@ -616,7 +616,7 @@ class BaseSiteAdapter(Configurable):
|
||||
# This is primarily for epub updates.
|
||||
retval = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",retval)
|
||||
|
||||
if self.getConfig("replace_br_with_p"):
|
||||
if self.getConfig("replace_br_with_p") and allow_replace_br_with_p:
|
||||
# Apply heuristic processing to replace <br> paragraph
|
||||
# breaks with <p> tags.
|
||||
retval = replace_br_with_p(retval)
|
||||
@@ -624,7 +624,7 @@ class BaseSiteAdapter(Configurable):
|
||||
if self.getConfig('replace_hr'):
|
||||
# replacing a self-closing tag with a container tag in the
|
||||
# soup is more difficult than it first appears. So cheat.
|
||||
retval = retval.replace("<hr />","<div class='center'>* * *</div>")
|
||||
retval = re.sub("<hr[^>]*>","<div class='center'>* * *</div>",retval)
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
errorDiv = soup.find("div", "errortext")
|
||||
self.triedLoggingIn = True
|
||||
else:
|
||||
raise exceptions.FailedToLogin(self.url, str(errorDiv))
|
||||
raise exceptions.FailedToLogin(self.url, unicode(errorDiv))
|
||||
else:
|
||||
warningLink = errorDiv.find("a")
|
||||
if warningLink is not None and ( \
|
||||
@@ -354,9 +354,9 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
errorDiv = soup.find("div", "errortext")
|
||||
self.triedAcceptWarnings = True
|
||||
else:
|
||||
raise exception.FailedToDownload(self.url, str(errorDiv))
|
||||
raise exception.FailedToDownload(self.url, unicode(errorDiv))
|
||||
else:
|
||||
raise exception.FailedToDownload(self.url, str(errorDiv))
|
||||
raise exception.FailedToDownload(self.url, unicode(errorDiv))
|
||||
|
||||
# title and author
|
||||
pagetitleDiv = soup.find("div", {"id": "pagetitle"})
|
||||
@@ -382,7 +382,7 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
if (type(nextEl) is bs.Tag):
|
||||
valueStr += nextEl.prettify()
|
||||
else:
|
||||
valueStr += str(nextEl)
|
||||
valueStr += unicode(nextEl)
|
||||
nextEl = nextEl.nextSibling
|
||||
key = labelSpan.text.strip()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
@@ -31,6 +32,7 @@ logger = logging.getLogger(__name__)
|
||||
class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
#logger.info("init url: "+url)
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8",
|
||||
@@ -38,47 +40,51 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
|
||||
|
||||
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL(self.getURLPrefix() + '/'+m.group('tp')+'/'+self.story.getMetadata('storyId')+'/')
|
||||
#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')+'/')
|
||||
else:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
# normalized story URL.
|
||||
self._setURL(self.getURLPrefix() + '/'+m.group('tp')+'/'+self.story.getMetadata('storyId')+'/')
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','fsb')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b %d, %Y at %I:%M %p"
|
||||
|
||||
|
||||
@classmethod
|
||||
def getConfigSections(cls):
|
||||
"Only needs to be overriden if has additional ini sections."
|
||||
return ['base_xenforoforum',cls.getConfigSection()]
|
||||
|
||||
|
||||
@classmethod
|
||||
def getURLPrefix(cls):
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'https://' + cls.getSiteDomain()
|
||||
return 'https://' + cls.getSiteDomain()
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.getURLPrefix()+"/threads/some-story-name.123456/"
|
||||
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+)/?"
|
||||
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?[^#]*?(#post-(?P<post>\d+))?$"
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
adapters that will work with the page cache need to implement
|
||||
@@ -86,6 +92,42 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
'''
|
||||
return True
|
||||
|
||||
def performLogin(self):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['login'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['login'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['register'] = '0'
|
||||
params['cookie_check'] = '1'
|
||||
params['_xfToken'] = ''
|
||||
params['redirect'] = 'https://' + self.getSiteDomain() + '/'
|
||||
|
||||
if not params['password']:
|
||||
return
|
||||
|
||||
## https://forum.questionablequesting.com/login/login
|
||||
loginUrl = 'https://' + self.getSiteDomain() + '/login/login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['login']))
|
||||
|
||||
# soup = self.make_soup(self._fetchUrl(loginUrl))
|
||||
# params['ctkn']=soup.find('input', {'name':'ctkn'})['value']
|
||||
# params[soup.find('input', {'id':'password'})['name']] = params['password']
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Log Out" not in d :
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['login']))
|
||||
raise exceptions.FailedToLogin(self.url,params['login'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
@@ -99,8 +141,13 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
elif e.code == 403:
|
||||
self.performLogin()
|
||||
(data,opened) = self._fetchUrlOpened(useurl)
|
||||
useurl = opened.geturl()
|
||||
logger.info("use useurl: "+useurl)
|
||||
else:
|
||||
raise e
|
||||
raise
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
@@ -112,7 +159,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
|
||||
h1 = soup.find('div',{'class':'titleBar'}).h1
|
||||
self.story.setMetadata('title',stripHTML(h1))
|
||||
|
||||
|
||||
if '#' in useurl:
|
||||
anchorid = useurl.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
@@ -129,19 +176,22 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('datePublished', date)
|
||||
if not self.story.getMetadataRaw('dateUpdated') or date > self.story.getMetadataRaw('dateUpdated'):
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
|
||||
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'}):
|
||||
self.story.addToList('forumtags',stripHTML(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)
|
||||
|
||||
|
||||
# Now go hunting for the 'chapter list'.
|
||||
bq = soup.find('blockquote') # assume first posting contains TOC urls.
|
||||
|
||||
|
||||
bq.name='div'
|
||||
|
||||
for iframe in bq.find_all('iframe'):
|
||||
@@ -149,7 +199,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
|
||||
for qdiv in bq.find_all('div',{'class':'quoteExpand'}):
|
||||
qdiv.extract() # Remove <div class="quoteExpand">click to expand</div>
|
||||
|
||||
|
||||
self.setDescription(useurl,bq)
|
||||
|
||||
# otherwise, use first post links--include first post since
|
||||
@@ -157,33 +207,37 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
if not self.chapterUrls:
|
||||
self.chapterUrls.append(("First Post",useurl))
|
||||
for (url,name) in [ (x['href'],stripHTML(x)) for x in bq.find_all('a') ]:
|
||||
logger.debug("found chapurl:%s"%url)
|
||||
#logger.debug("found chapurl:%s"%url)
|
||||
if not url.startswith('http'):
|
||||
url = self.getURLPrefix()+'/'+url
|
||||
|
||||
|
||||
if ( url.startswith(self.getURLPrefix()) or
|
||||
url.startswith('http://'+self.getSiteDomain()) or
|
||||
url.startswith('https://'+self.getSiteDomain()) ) and ('/posts/' in url or '/threads/' in url):
|
||||
|
||||
# brute force way to deal with SB's http->https change when hardcoded http urls.
|
||||
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
|
||||
logger.debug("used chapurl:%s"%(url))
|
||||
|
||||
url = re.sub(r'(^[\'"]+|[\'"]+$)','',url) # strip leading or trailing '" from incorrect quoting.
|
||||
|
||||
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]:
|
||||
# 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
|
||||
# from the 'first' posting created and updated.
|
||||
date = self.make_date(soup.find('a',{'class':'datePermalink'}))
|
||||
if date:
|
||||
self.story.setMetadata('datePublished', date)
|
||||
self.story.setMetadata('dateUpdated', date) # updated overwritten below if found.
|
||||
|
||||
|
||||
date = self.make_date(soup.find('div',{'class':'editDate'}))
|
||||
if date:
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
def make_date(self,parenttag): # forums use a BS thing where dates
|
||||
@@ -204,7 +258,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
except:
|
||||
logger.debug('No date found in %s'%parenttag)
|
||||
return None
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
@@ -217,28 +271,49 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
# https://forums.sufficientvelocity.com/posts/39915/
|
||||
if '#post-' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('#post-')[1]+'/'
|
||||
|
||||
origurl = url
|
||||
(data,opened) = self._fetchUrlOpened(url)
|
||||
url = opened.geturl()
|
||||
if '#' in origurl and '#' not in url:
|
||||
url = url + origurl[origurl.index('#'):]
|
||||
logger.debug("chapter URL redirected to: %s"%url)
|
||||
|
||||
soup = self.make_soup(data)
|
||||
## Same as above except for for case where author mistakenly
|
||||
## used the reply link instead of normal link to post.
|
||||
# "http://forums.spacebattles.com/threads/manager-worm-story-thread-iv.301602/reply?quote=15962513"
|
||||
# https://forums.spacebattles.com/posts/
|
||||
if 'reply?quote=' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('reply?quote=')[1]+'/'
|
||||
|
||||
if '#' in url:
|
||||
anchorid = url.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
|
||||
bq = soup.find('blockquote')
|
||||
try:
|
||||
origurl = url
|
||||
(data,opened) = self._fetchUrlOpened(url)
|
||||
url = opened.geturl()
|
||||
if '#' in origurl and '#' not in url:
|
||||
url = url + origurl[origurl.index('#'):]
|
||||
logger.debug("chapter URL redirected to: %s"%url)
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
if '#' in url:
|
||||
anchorid = url.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
|
||||
bq = soup.find('blockquote')
|
||||
|
||||
bq.name='div'
|
||||
|
||||
for iframe in bq.find_all('iframe'):
|
||||
iframe.extract() # calibre book reader & editor don't like iframes to youtube.
|
||||
|
||||
for qdiv in bq.find_all('div',{'class':'quoteExpand'}):
|
||||
qdiv.extract() # Remove <div class="quoteExpand">click to expand</div>
|
||||
|
||||
except Exception as e:
|
||||
if self.getConfig('continue_on_chapter_error'):
|
||||
bq = self.make_soup("""<div>
|
||||
<p><b>Error</b></p>
|
||||
<p>FanFicFare failed to download this chapter. Because you have
|
||||
<b>continue_on_chapter_error</b> set to <b>true</b> in your personal.ini, the download continued.</p>
|
||||
<p>Chapter URL:<br>%s</p>
|
||||
<p>Error:<br><pre>%s</pre></p>
|
||||
</div>"""%(url,traceback.format_exc()))
|
||||
else:
|
||||
raise
|
||||
|
||||
bq.name='div'
|
||||
|
||||
for iframe in bq.find_all('iframe'):
|
||||
iframe.extract() # calibre book reader & editor don't like iframes to youtube.
|
||||
|
||||
for qdiv in bq.find_all('div',{'class':'quoteExpand'}):
|
||||
qdiv.extract() # Remove <div class="quoteExpand">click to expand</div>
|
||||
|
||||
return self.utf8FromSoup(url,bq)
|
||||
# XenForo uses <base href="https://forums.spacebattles.com/" />
|
||||
return self.utf8FromSoup(self.getURLPrefix()+'/',bq)
|
||||
|
||||
@@ -159,6 +159,7 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
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:
|
||||
@@ -264,6 +265,14 @@ def do_download(arg,
|
||||
|
||||
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)
|
||||
|
||||
@@ -82,7 +82,7 @@ def get_valid_sections():
|
||||
sitesections = list(othersections)
|
||||
for section in sites:
|
||||
sitesections.append(section)
|
||||
# also allows [www.base_efiction] and [www.base_forum]. Not
|
||||
# also allows [www.base_efiction] and [www.base_xenforoforum]. Not
|
||||
# likely to matter.
|
||||
if section.startswith('www.'):
|
||||
# add w/o www if has www
|
||||
@@ -166,6 +166,12 @@ def get_valid_set_options():
|
||||
'include_images':(None,['epub','html'],boollist),
|
||||
'grayscale_images':(None,['epub','html'],boollist),
|
||||
'no_image_processing':(None,['epub','html'],boollist),
|
||||
|
||||
'continue_on_chapter_error':(['base_xenforoforum',
|
||||
'forums.spacebattles.com',
|
||||
'forums.sufficientvelocity.com',
|
||||
'questionablequesting.com',
|
||||
],None,boollist),
|
||||
}
|
||||
|
||||
return dict(valdict)
|
||||
@@ -296,6 +302,7 @@ def get_valid_keywords():
|
||||
'replace_metadata',
|
||||
'slow_down_sleep_time',
|
||||
'sort_ships',
|
||||
'sort_ships_splits',
|
||||
'strip_chapter_numbers',
|
||||
'strip_chapter_numeral',
|
||||
'strip_text_links',
|
||||
@@ -320,6 +327,7 @@ def get_valid_keywords():
|
||||
'wrap_width',
|
||||
'zip_filename',
|
||||
'zip_output',
|
||||
'continue_on_chapter_error',
|
||||
])
|
||||
|
||||
# *known* entry keywords -- or rather regexps for them.
|
||||
|
||||
+77
-42
@@ -198,20 +198,29 @@ connect_timeout:60.0
|
||||
## For example, you could change Sci-Fi=>SF, remove *-Centered tags,
|
||||
## etc. See http://docs.python.org/library/re.html (look for re.sub)
|
||||
## for regexp details.
|
||||
##
|
||||
## Make sure to keep at least one space at the start of each line and
|
||||
## to escape % to %%, if used.
|
||||
##
|
||||
## Two, three or five part lines. Two part effect everything.
|
||||
## Three part effect only those key(s) lists.
|
||||
## *Five* part lines. Effect only when trailing conditional key=>regexp matches
|
||||
## metakey[,metakey]=>pattern=>replacement[&&conditionalkey=>regexp]
|
||||
##
|
||||
## Note that if metakey == conditionalkey the conditional is ignored.
|
||||
## You can use \s in the replacement to add explicit spaces. (The config parser
|
||||
## tends to discard trailing spaces.)
|
||||
##
|
||||
## replace_metadata <entry>_LIST options: FanFicFare replace_metadata lines
|
||||
## operate on individual list items for list entries. But if you
|
||||
## want to do a replacement on the joined string for the whole list,
|
||||
## you can by using <entry>_LIST. Example, if you added
|
||||
## calibre_author: calibre_author_LIST=>^(.{,100}).*$=>\1
|
||||
##
|
||||
## You can 'split' one list item into multiple list entries by using
|
||||
## \' in the replacement string.
|
||||
##
|
||||
## Examples:
|
||||
#replace_metadata:
|
||||
# genre,category=>Sci-Fi=>SF
|
||||
# Puella Magi Madoka Magica.* => Madoka
|
||||
@@ -221,6 +230,8 @@ connect_timeout:60.0
|
||||
# .*-Centered=>
|
||||
# characters=>Sam W\.=>Sam Witwicky&&category=>Transformers
|
||||
# characters=>Sam W\.=>Sam Winchester&&category=>Supernatural
|
||||
# category=>Bitextual=>M/M\,F/M
|
||||
|
||||
|
||||
## Include/Exclude metadata
|
||||
##
|
||||
@@ -245,7 +256,7 @@ connect_timeout:60.0
|
||||
##
|
||||
## This is fairly complicated, so it's documented on its own wiki
|
||||
## page:
|
||||
## https://code.google.com/p/fanficdownloader/wiki/InExcludeMetadataFeature
|
||||
## https://github.com/JimmXinu/FanFicFare/wiki/InExcludeMetadataFeature
|
||||
|
||||
## Some readers don't show horizontal rule (<hr />) tags correctly.
|
||||
## This replaces them all with a centered '* * *'. (Note centering
|
||||
@@ -332,12 +343,21 @@ chapter_title_new_pattern:(new) ${title}
|
||||
## (new) is just text and can be changed.
|
||||
chapter_title_addnew_pattern:${index}. (new) ${title}
|
||||
|
||||
## Reorder ships so b/a and c/b/a become a/b and a/b/c. Only separates
|
||||
## on '/', so use replace_metadata to change separator first if
|
||||
## needed. Something like: ships=>[ ]*(/|&|&)[ ]*=>/ You can use
|
||||
## ships_LIST to change the / back to something else if you want.
|
||||
## Reorder ships so b/a and c/b/a become a/b and a/b/c. '/' is no
|
||||
## longer hard coded and can be changed and added to with
|
||||
## sort_ships_splits.
|
||||
sort_ships:false
|
||||
|
||||
## Each line indicates first a regex that should be used to split each
|
||||
## ships entry and then, after => the string to use to merge the parts
|
||||
## back together. \s == blank space.
|
||||
## Each part will have replace_metadata with key ships_CHARS applied.
|
||||
## By default, sort_ships_splits:/=>/
|
||||
## Here's possible an example for AO3:
|
||||
#sort_ships_splits:
|
||||
# [ ]*/[ ]*=>/
|
||||
# [ ]*&[ ]*=>\s&\s
|
||||
|
||||
## join_string_<entry> options -- FanFicFare list entries are comma
|
||||
## separated by default. You can use this to change that. For example,
|
||||
## if you want authors separated with ' & ' instead, use
|
||||
@@ -367,9 +387,8 @@ user_agent:FFF/2.X
|
||||
bulk_load:true
|
||||
|
||||
[base_xenforoforum]
|
||||
## Currently only forums.spacebattles.com and forums.sufficientvelocity.com
|
||||
|
||||
cover_exclusion_regexp:/clear.png
|
||||
cover_exclusion_regexp:/styles/
|
||||
|
||||
## I saw lots of chapters name simply '1.1' etc during testing.
|
||||
strip_chapter_numbers:false
|
||||
@@ -391,28 +410,40 @@ forumtags_label:Tags from Forum
|
||||
add_to_include_metadata_pre:
|
||||
# only keep tagsfromtitle with ( or [ in.
|
||||
tagsfromtitle=~[\[\(]
|
||||
|
||||
|
||||
## disable chapter range in title because of tagsfromtitle processing.
|
||||
title_chapter_range_pattern:
|
||||
|
||||
add_to_replace_metadata:
|
||||
# for QuestionableQuesting NSFW subforum.
|
||||
tagsfromtitle=>^\[NSFW\].*?([\(\[]([^\]\)]+)[\)\]]).*?$=>NSFW,\2
|
||||
# remove anything outside () or []
|
||||
tagsfromtitle=>^.*?([\(\[]([^\]\)]+)[\)\]]).*?$=>\1
|
||||
tagsfromtitle=>^.*?([\(\[]([^\]\)]+)[\)\]]).*?$=>\2
|
||||
# remove () []
|
||||
tagsfromtitle=>[\(\)\[\]]=>
|
||||
# tagsfromtitle=>[\(\)\[\]]=>
|
||||
# change (spaces)slash(or semicolon)(spaces) to comma
|
||||
tagsfromtitle=> *[/;] *=>,
|
||||
tagsfromtitle=> [xX] =>,
|
||||
|
||||
|
||||
# remove [] or () blocks and leading/trailing spaces/dashes/colons
|
||||
title=>[-: ]*[\(\[]([^\]\)]+)[\)\]][-: ]*=>
|
||||
# remove 'Thread' and the next word, usually "Thread 2", "Thread
|
||||
# four", "Thread iv", etc
|
||||
title,tagsfromtitle=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
|
||||
# four", "Thread iv", "Story Thread", etc
|
||||
title,tagsfromtitle=>[-: ]*(Story *)?[Tt]hread [^ ]+[-: ]*=>
|
||||
|
||||
# Normalize 'fanfiction/fanfic/fan-fiction' a little.
|
||||
forumtags=>[Ff]an-?[Ff]ic(tion)?=>FanFiction
|
||||
|
||||
add_to_extra_titlepage_entries:,tagsfromtitle,forumtags
|
||||
|
||||
## XenForo tags are all lowercase everywhere that I've seen. This
|
||||
## makes the first letter of each word uppercase. Applied before
|
||||
## replace_metadata.
|
||||
capitalize_forumtags:true
|
||||
|
||||
## Add both title tags and forumtags to standard (subject) tags.
|
||||
## '.SPLIT' tells the system to split by ','
|
||||
add_to_include_subject_tags:,tagsfromtitle.SPLIT
|
||||
## Or to also include forumtags add this to personal.ini:
|
||||
#add_to_include_subject_tags:,forumtags
|
||||
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
|
||||
@@ -424,6 +455,13 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## the description.
|
||||
description_limit:500
|
||||
|
||||
## Because base_xenforoforum adapters can pull chapter URLs from human
|
||||
## posts, the odds of errors in the chapter URLs are vastly higher.
|
||||
## You can set continue_on_chapter_error:true to continue on after
|
||||
## failing to download a chapter and instead record an error message
|
||||
## in the ebook for that chapter.
|
||||
continue_on_chapter_error:false
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
|
||||
@@ -623,6 +661,7 @@ nook_img_fix:true
|
||||
## URLs like: http://test1.com?sid=12345
|
||||
[test1.com]
|
||||
extratags: FanFiction,Testing
|
||||
|
||||
# extracategories:Fafner
|
||||
# extragenres:Romance,Fluff
|
||||
# extracharacters:Reginald Smythe-Smythe,Mokona,Harry P.
|
||||
@@ -685,7 +724,7 @@ extratags: FanFiction,Testing,HTML
|
||||
|
||||
## AO3 adapter defines a few extra metadata entries.
|
||||
## If there's ever more than 4 series, add series04,series04Url etc.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url,series00HTML,series01HTML,series02HTML,series03HTML
|
||||
fandoms_label:Fandoms
|
||||
freeformtags_label:Freeform Tags
|
||||
freefromtags_label:Freeform Tags
|
||||
@@ -695,6 +734,14 @@ kudos_label:Kudos
|
||||
hits_label:Hits
|
||||
collections_label:Collections
|
||||
bookmarks_label:Bookmarks
|
||||
series00HTML_label:Series
|
||||
series01HTML_label:Additional Series
|
||||
series02HTML_label:Additional Series
|
||||
series03HTML_label:Additional Series
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:series00,series01,series02,series03
|
||||
|
||||
## AO3 doesn't have anything it calls 'genre'. The adapter used to be
|
||||
## hardcoded to include the site specific metadata freeformtags &
|
||||
@@ -713,7 +760,7 @@ include_in_category:fandoms
|
||||
include_in_freefromtags:freeformtags
|
||||
|
||||
## adds to titlepage_entries instead of replacing it.
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series01HTML,series02HTML,series03HTML
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
@@ -863,12 +910,6 @@ extracategories:Buffy: The Vampire Slayer
|
||||
extracharacters:Buffy, Spike
|
||||
extraships:Spike/Buffy
|
||||
|
||||
[devianthearts.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
|
||||
|
||||
[dramione.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1256,7 +1297,7 @@ extracategories:NCIS
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
extracharacters:Willow
|
||||
|
||||
[ninelives.dark-solace.org]
|
||||
[ninelivesarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:The Walking Dead
|
||||
extracharacters:Carol,Daryl
|
||||
@@ -1335,6 +1376,16 @@ extracategories:My Little Pony: Friendship is Magic
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:The Pretender
|
||||
|
||||
[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
|
||||
|
||||
[samandjack.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1504,22 +1555,6 @@ readings_label: Readings
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[thequidditchpitch.org]
|
||||
## 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
|
||||
|
||||
[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,
|
||||
@@ -1883,7 +1918,7 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:NCIS
|
||||
|
||||
[www.nickandgreg.net]
|
||||
[www.nickngreg.nl]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:CSI
|
||||
extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
+19
-9
@@ -57,7 +57,7 @@ def get_update_data(inputio,
|
||||
if( relpath+item.getAttribute("href") == oldcoverhtmlhref ):
|
||||
oldcoverhtmltype = item.getAttribute("media-type")
|
||||
break
|
||||
soup = bs.BeautifulSoup(oldcoverhtmldata.decode("utf-8"))
|
||||
soup = bs.BeautifulSoup(oldcoverhtmldata.decode("utf-8"),"html5lib")
|
||||
src = None
|
||||
# first img or image tag.
|
||||
imgs = soup.findAll('img')
|
||||
@@ -208,7 +208,7 @@ def get_story_url_from_html(inputio,_is_good_url=None):
|
||||
#print("---- item:%s"%item)
|
||||
if( item.getAttribute("media-type") == "application/xhtml+xml" ):
|
||||
filehref=relpath+item.getAttribute("href")
|
||||
soup = bs.BeautifulSoup(epub.read(filehref).decode("utf-8"))
|
||||
soup = bs.BeautifulSoup(epub.read(filehref).decode("utf-8"),"html5lib")
|
||||
for link in soup.findAll('a',href=re.compile(r'^http.*')):
|
||||
ahref=link['href']
|
||||
#print("href:(%s)"%ahref)
|
||||
@@ -272,26 +272,37 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
tag = soup.find('meta',{'name':'chaptertoctitle'})
|
||||
if tag:
|
||||
chaptertoctitle = tag['content']
|
||||
elif chapterorigtitle:
|
||||
chaptertoctitle = chapterorigtitle
|
||||
|
||||
chaptertitle = None
|
||||
tag = soup.find('meta',{'name':'chaptertitle'})
|
||||
if tag:
|
||||
chaptertitle = tag['content']
|
||||
chaptertitle_tag = tag
|
||||
|
||||
#logger.debug("chaptertitle:(%s) chapterorigtitle:(%s)"%(chaptertitle, chapterorigtitle))
|
||||
if chaptertitle and chapterorigtitle and chapterorigtitle != chaptertitle:
|
||||
origdata = data
|
||||
# print("\n%s\n%s\n"%(chapterorigtitle,chaptertitle))
|
||||
data = data.replace(u'<meta name="chaptertitle" content="'+chaptertitle+u'"></meta>',
|
||||
u'<meta name="chaptertitle" content="'+chapterorigtitle+u'"></meta>')
|
||||
data = data.replace(u'<title>'+chaptertitle+u'</title>',u'<title>'+chapterorigtitle+u'</title>')
|
||||
data = data.replace(u'<h3>'+chaptertitle+u'</h3>',u'<h3>'+chapterorigtitle+u'</h3>')
|
||||
# data = data.replace(u'<meta name="chaptertitle" content="'+chaptertitle+u'"></meta>',
|
||||
# u'<meta name="chaptertitle" content="'+chapterorigtitle+u'"></meta>')
|
||||
# data = data.replace(u'<title>'+chaptertitle+u'</title>',u'<title>'+chapterorigtitle+u'</title>')
|
||||
# data = data.replace(u'<h3>'+chaptertitle+u'</h3>',u'<h3>'+chapterorigtitle+u'</h3>')
|
||||
chaptertitle_tag['content'] = chapterorigtitle
|
||||
title_tag = soup.find('title')
|
||||
if title_tag and title_tag.string == chaptertitle:
|
||||
title_tag.string.replace_with(chapterorigtitle)
|
||||
|
||||
h3_tag = soup.find('h3')
|
||||
if h3_tag and h3_tag.string == chaptertitle:
|
||||
h3_tag.string.replace_with(chapterorigtitle)
|
||||
|
||||
data = unicode(soup)
|
||||
|
||||
entrychanged = ( origdata != data )
|
||||
changed = changed or entrychanged
|
||||
|
||||
if entrychanged:
|
||||
logger.debug("\nentrychanged:%s\n"%zf)
|
||||
_replace_tocncx(tocncxdom,zf,chaptertoctitle)
|
||||
## Also look for and update individual
|
||||
## book toc.ncx files for anthology in case
|
||||
@@ -330,7 +341,6 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def _replace_tocncx(tocncxdom,zf,chaptertoctitle):
|
||||
## go after the TOC entry, too.
|
||||
# <navPoint id="file0005" playOrder="6">
|
||||
|
||||
@@ -25,7 +25,7 @@ import urlparse
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
from bs4 import BeautifulSoup
|
||||
from gziphttp import GZipProcessor
|
||||
|
||||
import adapters
|
||||
@@ -82,7 +82,7 @@ def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrict
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
|
||||
soup = BeautifulSoup(data)
|
||||
soup = BeautifulSoup(data,"html5lib")
|
||||
if restrictsearch:
|
||||
soup = soup.find(*restrictsearch)
|
||||
#logger.debug("restrict search:%s"%soup)
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ class HtmlProcessor:
|
||||
'''Replace <pre> tags with HTML-ified text.'''
|
||||
pres = self._soup.findAll('pre')
|
||||
for pre in pres:
|
||||
pre.replaceWith(self._FixPreContents(str(pre.contents[0])))
|
||||
pre.replaceWith(self._FixPreContents(unicode(pre.contents[0])))
|
||||
|
||||
def _FixPreContents(self, text):
|
||||
if self.unfill:
|
||||
|
||||
+61
-32
@@ -77,7 +77,7 @@ try:
|
||||
|
||||
if removetrans and img.has_transparent_pixels():
|
||||
canvas = Image()
|
||||
canvas.create_canvas(int(img.size[0]), int(img.size[1]), str(background))
|
||||
canvas.create_canvas(int(img.size[0]), int(img.size[1]), unicode(background))
|
||||
canvas.compose(img)
|
||||
img = canvas
|
||||
export = True
|
||||
@@ -380,29 +380,34 @@ def set_in_ex_clude(setting):
|
||||
## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp]
|
||||
def make_replacements(replace):
|
||||
retval=[]
|
||||
for line in replace.splitlines():
|
||||
# print("replacement line:%s"%line)
|
||||
(metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None)
|
||||
if "&&" in line:
|
||||
(line,conditional) = line.split("&&")
|
||||
(condkey,condregexp) = conditional.split("=>")
|
||||
if "=>" in line:
|
||||
parts = line.split("=>")
|
||||
if len(parts) > 2:
|
||||
metakeys = map( lambda x: x.strip(), parts[0].split(",") )
|
||||
(regexp,replacement)=parts[1:]
|
||||
else:
|
||||
(regexp,replacement)=parts
|
||||
|
||||
if regexp:
|
||||
regexp = re_compile(regexp,line)
|
||||
if condregexp:
|
||||
condregexp = re_compile(condregexp,line)
|
||||
# A way to explicitly include spaces in the
|
||||
# replacement string. The .ini parser eats any
|
||||
# trailing spaces.
|
||||
replacement=replacement.replace(SPACE_REPLACE,' ')
|
||||
retval.append([metakeys,regexp,replacement,condkey,condregexp])
|
||||
for fullline in replace.splitlines():
|
||||
line=fullline
|
||||
try:
|
||||
(metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None)
|
||||
if "&&" in line:
|
||||
(line,conditional) = line.split("&&")
|
||||
(condkey,condregexp) = conditional.split("=>")
|
||||
if "=>" in line:
|
||||
parts = line.split("=>")
|
||||
if len(parts) > 2:
|
||||
metakeys = map( lambda x: x.strip(), parts[0].split(",") )
|
||||
(regexp,replacement)=parts[1:]
|
||||
else:
|
||||
(regexp,replacement)=parts
|
||||
|
||||
if regexp:
|
||||
regexp = re_compile(regexp,line)
|
||||
if condregexp:
|
||||
condregexp = re_compile(condregexp,line)
|
||||
# A way to explicitly include spaces in the
|
||||
# replacement string. The .ini parser eats any
|
||||
# trailing spaces.
|
||||
replacement=replacement.replace(SPACE_REPLACE,' ')
|
||||
retval.append([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)
|
||||
# raise
|
||||
return retval
|
||||
|
||||
class Story(Configurable):
|
||||
@@ -682,7 +687,10 @@ class Story(Configurable):
|
||||
if self.isList('author'): # more than one author, assume multiple authorUrl too.
|
||||
htmllist=[]
|
||||
for i, v in enumerate(self.getList('author')):
|
||||
aurl = self.getList('authorUrl')[i]
|
||||
if len(self.getList('authorUrl')) <= i:
|
||||
aurl = None
|
||||
else:
|
||||
aurl = self.getList('authorUrl')[i]
|
||||
auth = v
|
||||
# make sure doreplacements & removeallentities are honored.
|
||||
if doreplacements:
|
||||
@@ -790,6 +798,33 @@ class Story(Configurable):
|
||||
if retlist is None:
|
||||
retlist = []
|
||||
|
||||
# reorder ships so b/a and c/b/a become a/b and a/b/c. Only on '/',
|
||||
# use replace_metadata to change separator first if needed.
|
||||
# ships=>[ ]*(/|&|&)[ ]*=>/
|
||||
if listname == 'ships' and self.getConfig('sort_ships') and doreplacements and retlist:
|
||||
# retlist = [ '/'.join(sorted(x.split('/'))) for x in retlist ]
|
||||
## empty default of /=>/
|
||||
sort_ships_splits = self.getConfig('sort_ships_splits',"/=>/")
|
||||
|
||||
for line in sort_ships_splits.splitlines():
|
||||
if line:
|
||||
## logger.debug("sort_ships_splits:%s"%line)
|
||||
## logger.debug(retlist)
|
||||
(splitre,splitmerge) = line.split("=>")
|
||||
splitmerge = splitmerge.replace(SPACE_REPLACE,' ')
|
||||
newretlist = []
|
||||
for x in retlist:
|
||||
curlist = []
|
||||
for y in re.split(splitre,x):
|
||||
## for SPLIT_META(\,)
|
||||
if doreplacements:
|
||||
y = self.doReplacements(y,'ships_CHARS',return_list=True)
|
||||
curlist.extend(y)
|
||||
newretlist.append( splitmerge.join(sorted(curlist)) )
|
||||
|
||||
retlist = newretlist
|
||||
## logger.debug(retlist)
|
||||
|
||||
if retlist:
|
||||
if doreplacements:
|
||||
newretlist = []
|
||||
@@ -809,12 +844,6 @@ class Story(Configurable):
|
||||
)) > 1:
|
||||
retlist.append(self.getConfig('add_genre_when_multi_category'))
|
||||
|
||||
# reorder ships so b/a and c/b/a become a/b and a/b/c. Only on '/',
|
||||
# use replace_metadata to change separator first if needed.
|
||||
# ships=>[ ]*(/|&|&)[ ]*=>/
|
||||
if listname == 'ships' and self.getConfig('sort_ships') and retlist:
|
||||
retlist = [ '/'.join(sorted(x.split('/'))) for x in retlist ]
|
||||
|
||||
if retlist:
|
||||
if listname in ('author','authorUrl','authorId') or self.getConfig('keep_in_order_'+listname):
|
||||
# need to retain order for author & authorUrl so the
|
||||
@@ -1067,7 +1096,7 @@ class Story(Configurable):
|
||||
return retlist
|
||||
|
||||
def __str__(self):
|
||||
return "Metadata: " +str(self.metadata)
|
||||
return "Metadata: " +unicode(self.metadata)
|
||||
|
||||
def commaGroups(s):
|
||||
groups = []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#-*-coding:utf-8-*-
|
||||
#-*-coding:utf-8-*-
|
||||
# Code taken from http://python.su/forum/viewtopic.php?pid=66946
|
||||
import unicodedata
|
||||
def is_syllable(letter):
|
||||
@@ -10,11 +10,11 @@ def is_consonant(letter):
|
||||
return not is_syllable(letter)
|
||||
def romanize(letter):
|
||||
try:
|
||||
str(letter)
|
||||
unicode(letter)
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
else:
|
||||
return str(letter)
|
||||
return unicode(letter)
|
||||
unid = unicodedata.name(letter)
|
||||
exceptions = {"NUMERO SIGN": "No", "LEFT-POINTING DOUBLE ANGLE QUOTATION MARK": "\"", "RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK": "\"", "DASH": "-"}
|
||||
for name_contains in exceptions:
|
||||
@@ -54,4 +54,4 @@ def translit(text):
|
||||
#bolgh.: Ah, chudna b'lgharska ziem'o, polyushkvay ts'ftyashchi zhita.
|
||||
#sierb.: Nieuriednie notshnie damie dosadjivalie su Dzhieku K.
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -48,7 +48,6 @@ class EpubWriter(BaseStoryWriter):
|
||||
self.EPUB_CSS = string.Template('''${output_css}''')
|
||||
|
||||
self.EPUB_TITLE_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>${title} by ${author}</title>
|
||||
@@ -75,7 +74,6 @@ ${value}<br />
|
||||
''')
|
||||
|
||||
self.EPUB_TABLE_TITLE_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>${title} by ${author}</title>
|
||||
@@ -106,7 +104,6 @@ ${value}<br />
|
||||
''')
|
||||
|
||||
self.EPUB_TOC_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>${title} by ${author}</title>
|
||||
@@ -128,7 +125,6 @@ ${value}<br />
|
||||
''')
|
||||
|
||||
self.EPUB_CHAPTER_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>${chapter}</title>
|
||||
@@ -148,7 +144,6 @@ ${value}<br />
|
||||
''')
|
||||
|
||||
self.EPUB_LOG_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>Update Log</title>
|
||||
@@ -233,7 +228,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
# <span id="dateUpdated">1975-04-15</span>
|
||||
span = '<span id="%s">'%entry
|
||||
idx = logfile.rindex(span)+len(span)
|
||||
values[entry] = logfile[idx:logfile.index('</span>',idx)]
|
||||
values[entry] = logfile[idx:logfile.index('</span>\n',idx)]
|
||||
except Exception, e:
|
||||
#print("e:%s"%e)
|
||||
pass
|
||||
@@ -284,7 +279,9 @@ div { margin: 0pt; padding: 0pt; }
|
||||
retval = retval + END.substitute(self.story.getAllMetadata())
|
||||
|
||||
if self.getConfig('replace_hr'):
|
||||
retval = retval.replace("<hr />","<div class='center'>* * *</div>")
|
||||
# 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
|
||||
|
||||
@@ -581,7 +578,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
if title :
|
||||
navPoint = newTag(tocncxdom,"navPoint",
|
||||
attrs={'id':id,
|
||||
'playOrder':str(index)})
|
||||
'playOrder':unicode(index)})
|
||||
tocnavMap.appendChild(navPoint)
|
||||
navLabel = newTag(tocncxdom,"navLabel")
|
||||
navPoint.appendChild(navLabel)
|
||||
@@ -656,13 +653,16 @@ div { margin: 0pt; padding: 0pt; }
|
||||
|
||||
for index, chap in enumerate(self.story.getChapters()): # (url,title,html)
|
||||
if chap.html:
|
||||
logger.debug('Writing chapter text for: %s' % chap.title)
|
||||
#logger.debug('Writing chapter text for: %s' % chap.title)
|
||||
vals={'url':removeEntities(chap.url),
|
||||
'chapter':chap.title,
|
||||
'origchapter':chap.origtitle,
|
||||
'tocchapter':chap.toctitle,
|
||||
'chapter':removeEntities(chap.title),
|
||||
'origchapter':removeEntities(chap.origtitle),
|
||||
'tocchapter':removeEntities(chap.toctitle),
|
||||
'index':"%04d"%(index+1),
|
||||
'number':index+1}
|
||||
# escape double quotes in all vals.
|
||||
for k,v in vals.items():
|
||||
if isinstance(v,basestring): vals[k]=v.replace('"','"')
|
||||
fullhtml = CHAPTER_START.substitute(vals) + \
|
||||
chap.html + CHAPTER_END.substitute(vals)
|
||||
# ffnet(& maybe others) gives the whole chapter text
|
||||
|
||||
@@ -33,9 +33,8 @@ class HTMLWriter(BaseStoryWriter):
|
||||
def __init__(self, config, story):
|
||||
BaseStoryWriter.__init__(self, config, story)
|
||||
|
||||
self.HTML_FILE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
self.HTML_FILE_START = string.Template('''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${title} by ${author}</title>
|
||||
<style type="text/css">
|
||||
@@ -87,7 +86,6 @@ ${output_css}
|
||||
|
||||
|
||||
def writeStoryImpl(self, out):
|
||||
|
||||
if self.hasConfig("cover_content"):
|
||||
COVER = string.Template(self.getConfig("cover_content"))
|
||||
else:
|
||||
|
||||
@@ -23,7 +23,7 @@ setup(
|
||||
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||
# the version across setup.py and the project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version="2.2.13",
|
||||
version="2.2.15",
|
||||
|
||||
description='A tool for downloading fanfiction to eBook formats',
|
||||
long_description=long_description,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanficfare
|
||||
application: fanficfare
|
||||
version: 2-2-13
|
||||
version: 2-2-15
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
+1
-13
@@ -27,18 +27,6 @@
|
||||
much easier. </p>
|
||||
</div>
|
||||
|
||||
<h3>Changes:</h3>
|
||||
<ul>
|
||||
<li>New site: <a href="http://www.quotev.com/">quotev.com</a>, thanks cryzed!</li>
|
||||
<li>Convert dark-solace.org(Elysium fields) to eFiction Base adapter to correct for site changes.</li>
|
||||
<li>Fix eFiction Base Author URLs.</li>
|
||||
<li>Switch chapter #post-12345 chapter URLs to permalinks for XenForoForum Base adapters.</li>
|
||||
<li>Record forum thread tags in XenForoForum Base adapters. Appears on title page by default, but not calibre tags.</li>
|
||||
<li>Change output_filename_safepattern to replace leading '.' for benefit of *nix OSes.</li>
|
||||
<li>Fix mediaminer.org for site changes.</li>
|
||||
<li>Fix to hpfandom.net change story URL when given chapter URL instead of story start URL.</li>
|
||||
<li>Allow forum.questionablequesting.com for questionablequesting.com.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="https://github.com/JimmXinu/FanFicFare/wiki/FAQs">FAQs</a>.
|
||||
@@ -47,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-12.fanficfare.appspot.com">previous version
|
||||
<a href="http://2-2-14.fanficfare.appspot.com">previous version
|
||||
</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
|
||||
+6
-6
@@ -153,7 +153,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(str(e)))
|
||||
self.redirect("/?error=custom&errtext=%s"%urlEscape(unicode(e)))
|
||||
else: # not update, assume display for edit
|
||||
if uconfig is not None and uconfig.config:
|
||||
config = uconfig.config
|
||||
@@ -309,7 +309,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="+str(fic.key())+"&fake=file."+fic.format)
|
||||
fic.escaped_url = urlEscape(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')
|
||||
@@ -391,7 +391,7 @@ class FanfictionDownloader(UserConfigServer):
|
||||
try:
|
||||
configuration = self.getUserConfig(user,url,format)
|
||||
except Exception, e:
|
||||
self.redirect("/?error=custom&errtext=%s"%urlEscape("There's an error in your User Configuration: "+str(e)))
|
||||
self.redirect("/?error=custom&errtext=%s"%urlEscape("There's an error in your User Configuration: "+unicode(e)))
|
||||
return
|
||||
|
||||
adapter = adapters.getAdapter(configuration,url)
|
||||
@@ -425,7 +425,7 @@ class FanfictionDownloader(UserConfigServer):
|
||||
|
||||
taskqueue.add(url='/fdowntask',
|
||||
queue_name="download",
|
||||
params={'id':str(download.key()),
|
||||
params={'id':unicode(download.key()),
|
||||
'format':format,
|
||||
'url':download.url,
|
||||
'login':login,
|
||||
@@ -433,7 +433,7 @@ class FanfictionDownloader(UserConfigServer):
|
||||
'user':user.email(),
|
||||
'is_adult':is_adult})
|
||||
|
||||
logging.info("enqueued download key: " + str(download.key()))
|
||||
logging.info("enqueued download key: " + unicode(download.key()))
|
||||
|
||||
except (exceptions.FailedToLogin,exceptions.AdultCheckRequired), e:
|
||||
download.failure = unicode(e)
|
||||
@@ -467,7 +467,7 @@ class FanfictionDownloader(UserConfigServer):
|
||||
download.failure = unicode(e)
|
||||
download.put()
|
||||
|
||||
self.redirect('/status?id='+str(download.key()))
|
||||
self.redirect('/status?id='+unicode(download.key()))
|
||||
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user