Compare commits

...
63 Commits
Author SHA1 Message Date
Jim Miller 6b28482514 Bump versions 2015-11-23 15:30:33 -06:00
Jim Miller b210007f59 Update translations 2015-11-23 15:30:18 -06:00
Jim Miller 08bc200c5c Updates for mediaminer.org. 2015-11-19 13:46:13 -06:00
Jim Miller 43ec9c0232 Remove some debug output. 2015-11-19 13:45:44 -06:00
Jim Miller a0c752c61f Make sort_ships honor doreplacements (and .NOREPL) flag. 2015-11-18 12:51:53 -06:00
Jim Miller ed84f10bfb Remove debug some output 2015-11-14 09:25:00 -06:00
Jim Miller 1d1e43f16f Change str() to unicode() many places. 2015-11-13 14:45:26 -06:00
Jim Miller ba4a29d646 Update translations. 2015-11-13 14:44:51 -06:00
Jim Miller 7ba523f20d Reduce over long error msgs when editing personal.ini(plugin). 2015-11-06 15:48:12 -06:00
Jim Miller 0e42f758ab Add sort_ships_splits and ships_CHARS. 2015-11-05 20:26:08 -06:00
Jim Miller e2995a7614 Add sort_ships_splits and ships_CHARS. 2015-11-05 13:56:07 -06:00
Jim Miller 3abb78a15e Fix to allow 'Update Calibre Metadata Only(Saved Column)' as default. 2015-11-04 10:58:47 -06:00
Jim Miller 3b9b9c1eb7 Fix for UnNew epubs with & or double quote in chap title. 2015-10-31 14:27:25 -05:00
Jim Miller 235c5ae62f Fix for whofic.com with replace_br_with_p 2015-10-31 14:26:51 -05:00
Jim Miller b2238e10e7 Add html5lib to more BS4 calls, convert another place to BS4. 2015-10-22 18:16:30 -05:00
Jim Miller a5bc7bd757 Comment out debug that can cause additional errors depending on content. 2015-10-22 18:15:42 -05:00
Jim Miller 36d848fde9 Add series00-04HTml to defaults.ini for AO3. 2015-10-21 18:43:16 -05:00
Jim Miller 793ec37782 Bump versions. 2015-10-18 15:48:32 -05:00
Jim Miller 49b9994a3b Fix for issue with Email fetch using last used Add options instead of default Add options. 2015-10-15 13:18:43 -05:00
Jim Miller 6d1798c29e Fix replace_hr feature so it works with BS4 html. 2015-10-14 23:53:13 -05:00
Jim Miller 897976b4bd Accept only forum.questionablequesting.com, not questionablequesting.com. 2015-10-14 16:28:02 -05:00
Jim Miller ed4f7a4c1d Update html tag header at beginning of each epub file. Prompted by failure of old header on latest tolino ereader. 2015-10-14 14:03:48 -05:00
Jim Miller ab5c930248 Change default HTML output to HTML5 header. 2015-10-14 12:39:42 -05:00
Jim Miller 6dd1cc7daa Improve replace_metadata comments in defaults.ini 2015-10-13 21:37:38 -05:00
Jim Miller 44325ef0ce Tweak to epub logpage that helps with finding desc. 2015-10-13 16:19:47 -05:00
Jim Miller c55553bbbf Update wiki link in defaults.ini. 2015-10-13 10:48:42 -05:00
Jim Miller 7956a1a7e1 Add chapter_urls feature to [teststory] feature. Currently only orks with base_xenforo chapter URLs. 2015-10-11 10:46:08 -05:00
Jim Miller afa14c568e Fix for unNew with & in chapter title. 2015-10-10 17:14:32 -05:00
Jim Miller df9065b1ba Improve handling for authors without authorUrl (mostly for teststory). 2015-10-09 11:25:39 -05:00
Jim Miller a4cc07c8ff Correct cover_exclusion_regexp for base_xenforoforum. 2015-10-09 11:24:38 -05:00
Jim Miller 5261decb02 Don't errorif authorUrl list shorter than author list, use no_author_link. For test1.com. 2015-10-08 18:18:14 -05:00
Jim Miller 9993836eca Account for base href in XenForo so emoticon images work. 2015-10-08 18:16:41 -05:00
Jim Miller 33916543e3 Accept individual post URLs (#post-) for base_xenforoforum adapters. 2015-10-08 14:35:50 -05:00
Jim Miller ede81a458e Accept individual post URLs (#post-) for base_xenforoforum adapters. 2015-10-08 14:06:48 -05:00
Jim Miller f3af754c71 Add capitalize_forumtags feature to base_xenforoforum and include forumtags by default. 2015-10-07 11:17:43 -05:00
Jim Miller 801e14e028 Update Translations. 2015-10-07 10:20:38 -05:00
Jim Miller 2440f002fd Fix for Overwrite mode. 2015-10-07 10:16:38 -05:00
Jim Miller a992829c04 Update Translations. 2015-10-06 11:11:04 -05:00
Jim Miller 55e434e9fb Fix bloodshedverse.com URL & vs &. 2015-10-02 14:54:25 -05:00
Jim Miller 0c0e439cc6 Remove devianthearts.com--Server not found for a week. 2015-10-01 09:45:20 -05:00
Jim Miller 3e431c843a Update strings for translation. 2015-09-30 10:41:31 -05:00
Jim Miller be04eed180 Add Background Metadata feature for updates. 2015-09-30 10:28:11 -05:00
Jim Miller 37a084a699 Fix QQ [NSFW] tagsfromtitle. 2015-09-29 13:15:32 -05:00
Jim Miller 5bc5c85248 Add XenForo(QuestionableQuesting) user/pass. Add 'Story' to XenForo 'Thread' title removal. 2015-09-29 12:45:11 -05:00
Jim Miller 63eed56f23 Fix for UnNew not working correctly with double quotes in chapter titles. May need to update-always existing epubs. 2015-09-28 23:36:27 -05:00
Jim Miller a2491be351 Get continue_on_chapter_error to not show red in PI personal.ini edit. 2015-09-28 14:06:45 -05:00
Jim Miller 289398dc66 Change nickandgrep section names. 2015-09-28 14:06:19 -05:00
Jim Miller 5040c44572 Add to base_xenforoforum: fix for author using reply URL, continue_on_chapter_error feature, remove 'Story' in front of 'Thread' in title. 2015-09-28 13:32:24 -05:00
Jim Miller e2d1a693dd Merge pull request #108 from cryzed/master
Fixed issues
2015-09-27 16:31:15 -05:00
cryzed 1f52833732 Fixed issues described here: http://www.mobileread.com/forums/showpost.php?p=3174596&postcount=570 2015-09-27 23:15:06 +02:00
Jim Miller b708a715a8 Add internal doc for another test1.com mode. 2015-09-25 17:07:25 -05:00
Jim Miller 35d7db0319 Move site ninelives.dark-solace.org to new domain ninelivesarchive.com. 2015-09-25 17:06:51 -05:00
Jim Miller c3b3e94bfc Use pagecache and cookiejar between downloads in CLI. 2015-09-24 18:32:07 -05:00
Jim Miller b41fc45c1c Remove thequidditchpitch.org, domain is parked. 2015-09-24 16:13:22 -05:00
Jim Miller 07d3e4a603 Remove thequidditchpitch.org, domain is parked. 2015-09-24 16:03:08 -05:00
Jim Miller 340fe65bf6 Remove thequidditchpitch.org, domain is parked. 2015-09-24 16:02:20 -05:00
Jim Miller 5d481b1d2a Change site www.nickandgreg.net to www.nickngreg.nl while still accepting old URLs. 2015-09-24 15:54:43 -05:00
Jim Miller 95b799663e Add check for non-existance/removed story to potterfics.com. 2015-09-24 15:45:40 -05:00
Jim Miller 51ea8f18a7 Add check for non-existance/removed story to potterfics.com. 2015-09-24 15:45:25 -05:00
Jim Miller 2943de51e0 Fixes for hpfandom.net changes. 2015-09-24 14:10:33 -05:00
Jim Miller 7f00c56ecf Strip leading/trailing single or double quotes from chapter URLs. Author typo fix. 2015-09-23 22:25:59 -05:00
Jim Miller ba1c439d28 Merge pull request #107 from cryzed/master
Fix failing comment count parsing for quotev.com for 0 comments
2015-09-20 09:43:25 -05:00
cryzed a182d16a6c Fix failing comment count parsing for quotev.com for 0 comments 2015-09-20 14:38:20 +02:00
97 changed files with 4020 additions and 3676 deletions
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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)
+35 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+77 -42
View File
@@ -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=>[ ]*(/|&amp;|&)[ ]*=>/ 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:
# [ ]*/[ ]*=>/
# [ ]*&amp;[ ]*=>\s&amp;\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
+1
View File
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -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)
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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+'))
+1 -1
View File
@@ -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:
@@ -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
+1 -1
View File
@@ -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))
+9 -9
View File
@@ -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)
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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.
+29 -26
View File
@@ -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 &#10095; 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))
+1 -1
View File
@@ -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.
@@ -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)
+1 -1
View File
@@ -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))
+6 -3
View File
@@ -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))
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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))
+30 -9
View File
@@ -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))
+2 -2
View File
@@ -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))
+55 -7
View File
@@ -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&lt;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))
+1 -1
View File
@@ -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((
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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
+4 -4
View File
@@ -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()
+127 -52
View File
@@ -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)
+9
View File
@@ -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)
+9 -1
View 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
View File
@@ -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=>[ ]*(/|&amp;|&)[ ]*=>/ 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:
# [ ]*/[ ]*=>/
# [ ]*&amp;[ ]*=>\s&amp;\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
View File
@@ -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">
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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=>[ ]*(/|&amp;|&)[ ]*=>/
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=>[ ]*(/|&amp;|&)[ ]*=>/
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 = []
+4 -4
View File
@@ -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()
+12 -12
View File
@@ -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('"','&quot;')
fullhtml = CHAPTER_START.substitute(vals) + \
chap.html + CHAPTER_END.substitute(vals)
# ffnet(& maybe others) gives the whole chapter text
+2 -4
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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