Compare commits

...
Author SHA1 Message Date
Jim Miller c6875d472d Bump cli/web version too. 2013-03-28 20:41:43 -05:00
Jim Miller 5758e364e4 Bump version. 2013-03-28 20:39:18 -05:00
Jim Miller d9a99fb7e9 Another fix for fanfiction.net changes. 2013-03-28 16:40:59 -05:00
Jim Miller 0a32cdb277 Added tag FanFictionDownLoader-4.4.51 for changeset aaf38d4e578b 2013-03-28 12:31:23 -05:00
Jim Miller 00c1fe8704 Added tag calibre-plugin-1.7.18 for changeset aaf38d4e578b 2013-03-28 12:31:10 -05:00
Jim Miller bb96ecd5fc Bump versions. 2013-03-28 12:30:57 -05:00
Jim Miller 64866f7da6 Add author to chapter TOC for multi-author stories on TtH and WraithBait.
AO3 doesn't reliably report chapter author.
2013-03-28 12:19:32 -05:00
Jim Miller 73815bda43 CLI: Add normalized option when getting story URL list from a URL. 2013-03-28 11:01:52 -05:00
Jim Miller d54acd936e PI: Allow partial download with <url>[1-5] in addition to <url>[1,5]. 2013-03-28 11:01:14 -05:00
Jim Miller e88244bebd Fix for failure to correctly load old JSON settings. 2013-03-28 11:00:21 -05:00
Jim Miller 02bdaf1086 Added tag FanFictionDownLoader-4.4.50 for changeset 3452b4bc5b3e 2013-03-26 13:46:20 -05:00
Jim Miller 21d5a39958 Added tag calibre-plugin-1.7.17 for changeset 3452b4bc5b3e 2013-03-26 13:46:08 -05:00
Jim Miller db661b1f9d YA fix for fanfiction.net changes, fix for collision setting, shorten PI name. 2013-03-26 13:45:56 -05:00
Jim Miller 0f654e86d6 Added tag calibre-plugin-1.7.16 for changeset e58cac739a8a 2013-03-25 16:41:10 -05:00
Jim Miller 063ee09e36 Added tag FanFictionDownLoader-4.4.49 for changeset e58cac739a8a 2013-03-25 16:40:59 -05:00
Jim Miller 4bb26278ad Fix fanfiction.net, bump versions. 2013-03-25 16:40:35 -05:00
Jim Miller d1f11f8ac4 Added tag FanFictionDownLoader-4.4.48 for changeset 1adcdcfb03ce 2013-03-25 12:57:58 -05:00
Jim Miller ec352e728e Added tag calibre-plugin-1.7.15 for changeset 1adcdcfb03ce 2013-03-25 12:57:44 -05:00
Jim Miller e4e5e4a47a Bump versions, update index.html. 2013-03-25 12:57:24 -05:00
Jim Miller 8632cfdbd6 Improvements to Reject URL list feature, fix PrefsViewer, cleanup dialogs.py. 2013-03-21 12:24:59 -05:00
Jim Miller c943ce7fce Set seriesHTML to series w/o link when no seriesUrl. 2013-03-21 10:56:03 -05:00
Jim Miller 7ccddd5a07 Check for existing Series Anthology books (by seriesUrl) on story add/update. PI 2013-03-20 10:49:43 -05:00
Jim Miller 3678bf6bf1 Add user/pass for dokuga.com. 2013-03-19 22:03:35 -05:00
Jim Miller 33bb1b2d29 Add user/pass for dokuga.com. 2013-03-19 21:58:11 -05:00
Jim Miller 3141191e43 Add user/pass for dokuga.com. 2013-03-19 21:57:52 -05:00
Jim Miller 536ea0b027 Only populate seriesHTML when series is set. 2013-03-19 19:56:25 -05:00
Jim Miller 34e03bf4eb Add seriesUrl (and generated seriesHTML) as valid metadata entries. 2013-03-19 14:22:07 -05:00
Jim Miller 48c81b1d1e Add seriesUrl (and generated seriesHTML) as valid metadata entries. 2013-03-19 14:21:14 -05:00
Jim Miller 381d3031e6 Fix default focus after hide/show. 2013-03-18 23:03:47 -05:00
Jim Miller a193e80f88 Fix drag and drop after hide/show. 2013-03-18 17:55:48 -05:00
Jim Miller 3410e20412 Restructure to make Add from URLs and Create Anthology for URLs modeless. 2013-03-18 17:31:37 -05:00
Jim Miller 7f568d54bf Added tag FanFictionDownLoader-4.4.47 for changeset ed6d76edf815 2013-03-18 11:36:28 -05:00
Jim Miller 619141ef94 Added tag calibre-plugin-1.7.14 for changeset ed6d76edf815 2013-03-18 11:36:00 -05:00
Jim Miller e6128b3174 Bump versions, add more test cases to test1.com adapter. 2013-03-18 11:31:31 -05:00
Jim Miller 875e139bcd Fixes for fimfiction.net changes. 2013-03-16 19:41:15 -05:00
Jim Miller db97d126bf Don't strip HTML entities during recursion--confuses replace_metadata. 2013-03-15 23:37:21 -05:00
Jim Miller 92597efce6 PI: Strip ',' from metadata set to int/float col by custom_columns_settings.
For favs, reviews, etc from site specific metadata.
2013-03-15 23:36:33 -05:00
Jim Miller 6b9cb35761 Added tag calibre-plugin-1.7.13 for changeset 4607b1addf58 2013-03-15 16:20:04 -05:00
76 changed files with 669 additions and 522 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-46
version: 4-4-52
runtime: python27
api_version: 1
threadsafe: true
+1 -1
View File
@@ -27,7 +27,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 7, 13)
version = (1, 7, 19)
minimum_calibre_version = (0, 8, 57)
#: This field defines the GUI plugin class that contains all the code
+46 -46
View File
@@ -18,10 +18,10 @@ from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
from calibre.utils.config import JSONConfig
from calibre.gui2.ui import get_gui
from calibre_plugins.fanfictiondownloader_plugin.prefs import prefs
from calibre_plugins.fanfictiondownloader_plugin.prefs import prefs, PREFS_NAMESPACE
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order, RejectListDialog,
EditTextDialog)
EditTextDialog, RejectUrlEntry)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters \
import (getConfigSections, getNormalStoryURL)
@@ -37,22 +37,16 @@ class RejectURLList:
self.sync_lock = threading.RLock()
self.listcache = None
def _read_list_from_text(self,text,addreasontext=None):
cache = {}
def _read_list_from_text(self,text,addreasontext=''):
cache = OrderedDict()
#print("_read_list_from_text")
for line in text.splitlines():
if ',' in line:
(rejurl,note) = line.split(',',1)
else:
(rejurl,note) = (line,'')
rejurl = getNormalStoryURL(rejurl)
if rejurl:
if addreasontext and note:
note = note +" - "+addreasontext
elif addreasontext:
note = addreasontext
cache[rejurl] = note
return cache
rue = RejectUrlEntry(line,addreasontext=addreasontext,fromline=True)
#print("rue.url:%s"%rue.url)
if rue.valid:
cache[rue.url] = rue
return cache
def _get_listcache(self):
if self.listcache == None:
@@ -60,26 +54,35 @@ class RejectURLList:
return self.listcache
def _save_list(self,listcache):
rejectlist = []
for url in listcache:
rejectlist.append("%s,%s"%(url,listcache[url]))
self.prefs['rejecturls'] = '\n'.join(rejectlist)
#print("_save_list")
self.prefs['rejecturls'] = '\n'.join([x.to_line() for x in listcache.values()])
self.prefs.save_to_db()
self.listcache = None
def clear_cache(self):
self.listcache = None
# true if url is in list.
def check(self,url):
with self.sync_lock:
listcache = self._get_listcache()
return url in listcache
def get_note(self,url):
with self.sync_lock:
listcache = self._get_listcache()
if url in listcache:
note = listcache[url]
return note
return listcache[url].note
# not found
return None
return ''
def get_full_note(self,url):
with self.sync_lock:
listcache = self._get_listcache()
if url in listcache:
return listcache[url].fullnote()
# not found
return ''
def remove(self,url):
with self.sync_lock:
@@ -89,27 +92,26 @@ class RejectURLList:
self._save_list(listcache)
def add_text(self,rejecttext,addreasontext):
self.add(self._read_list_from_text(rejecttext,addreasontext).items())
self.add(self._read_list_from_text(rejecttext,addreasontext).values())
def add(self,rejectlist,clear=False):
# rejectlist=list of (url,note) tuples.
with self.sync_lock:
if clear:
listcache={}
listcache=OrderedDict()
else:
listcache = self._get_listcache()
for (url,note) in rejectlist:
listcache[url]=note
for l in rejectlist:
listcache[l.url]=l
self._save_list(listcache)
def get_list(self):
return copy.deepcopy(self._get_listcache())
return self._get_listcache().values()
def get_reject_reasons(self):
return self.prefs['rejectreasons'].splitlines()
rejecturllist = RejectURLList(prefs)
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
@@ -173,6 +175,7 @@ class ConfigWidget(QWidget):
prefs['adddialogstaysontop'] = self.basic_tab.adddialogstaysontop.isChecked()
prefs['includeimages'] = self.basic_tab.includeimages.isChecked()
prefs['lookforurlinhtml'] = self.basic_tab.lookforurlinhtml.isChecked()
prefs['checkforseriesurlid'] = self.basic_tab.checkforseriesurlid.isChecked()
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
if self.readinglist_tab:
@@ -369,6 +372,11 @@ class BasicTab(QWidget):
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
self.l.addWidget(self.lookforurlinhtml)
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.setChecked(prefs['checkforseriesurlid'])
self.l.addWidget(self.checkforseriesurlid)
self.injectseries = QCheckBox("Inject calibre Series when none found?",self)
self.injectseries.setToolTip("If no series is found, inject the calibre series (if there is one) so it appears on the FFDL title page(not cover).")
self.injectseries.setChecked(prefs['injectseries'])
@@ -412,12 +420,8 @@ class BasicTab(QWidget):
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
def show_rejectlist(self):
rejectlist = []
for (url,note) in rejecturllist.get_list().items():
rejectlist.append((None,url,note,note))
d = RejectListDialog(self,
rejectlist,
rejecturllist.get_list(),
rejectreasons=rejecturllist.get_reject_reasons(),
header="Edit Reject URLs List",
show_delete=False,
@@ -427,11 +431,7 @@ class BasicTab(QWidget):
if d.result() != d.Accepted:
return
rejectlist=[]
for (bookid,url,note) in d.get_reject_list():
rejectlist.append((url,note))
rejecturllist.add(rejectlist,clear=True)
rejecturllist.add(d.get_reject_list(),clear=True)
def show_reject_reasons(self):
d = EditTextDialog(self,
@@ -446,11 +446,11 @@ class BasicTab(QWidget):
def add_reject_urls(self):
d = EditTextDialog(self,
"http://example.com?story.php?sid=5,Reason why I rejected it",
"http://example.com/story.php?sid=5,Reason why I rejected it\nhttp://example.com/story.php?sid=6,Title by Author - Reason why I rejected it",
icon=self.windowIcon(),
title="Add Reject URLs",
label="Add Reject URLs. Use: <b>http://...,note</b><br>Invalid story URLs will be ignored.",
tooltip="One URL per line, everything after <b>,</b> will be put in the note.",
label="Add Reject URLs. Use: <b>http://...,note</b> or <b>http://...,title by author - note</b><br>Invalid story URLs will be ignored.",
tooltip="One URL per line:\n<b>http://...,note</b>\n<b>http://...,title by author - note</b>",
rejectreasons=rejecturllist.get_reject_reasons(),
reasonslabel='Add this reason to all URLs added:')
d.exec_()
+254 -294
View File
@@ -7,33 +7,27 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__docformat__ = 'restructuredtext en'
import traceback
import traceback, re
from functools import partial
import urllib
import email
from PyQt4 import QtGui
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout,
QGridLayout, QPushButton, QProgressDialog, QString, QLabel,
QCheckBox, QIcon, QTextCursor, QTextEdit, QLineEdit, QInputDialog,
QComboBox, QClipboard, QVariant, QProgressDialog, QTimer,
QDialogButtonBox, QPixmap, Qt, QAbstractItemView, SIGNAL,
QTableWidgetItem )
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QString, QLabel, QCheckBox, QIcon, QLineEdit,
QComboBox, QVariant, QProgressDialog, QTimer, QDialogButtonBox,
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal)
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.complete2 import EditWithComplete
from calibre import confirm_config_name
from calibre.gui2 import dynamic
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import (ReadOnlyTableWidgetItem, ReadOnlyTextIconWidgetItem, SizePersistedDialog,
ImageTitleLayout, get_icon)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_html, get_urls_from_text
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getNormalStoryURL
SKIP=u'Skip'
ADDNEW=u'Add New Book'
@@ -53,7 +47,62 @@ collision_order=[SKIP,
anthology_collision_order=[UPDATE,
UPDATEALWAYS,
OVERWRITEALWAYS]
class RejectUrlEntry:
matchpat=re.compile(r"^(?P<url>[^,]+)(,(?P<fullnote>(((?P<title>.+) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
def __init__(self,url_or_line,note=None,title=None,auth=None,
addreasontext=None,fromline=False):
self.url=url_or_line
self.note=note
self.title=title
self.auth=auth
self.valid=False
if fromline:
mc = re.match(self.matchpat,url_or_line)
if mc:
#print("mc:%s"%mc.groupdict())
(url,title,auth,note) = mc.group('url','title','auth','note')
if not mc.group('title'):
title=''
auth=''
note=mc.group('fullnote')
self.url=url
self.note=note
self.title=title
self.auth=auth
if not self.note:
if addreasontext:
self.note = addreasontext
else:
self.note = ''
else:
if addreasontext:
self.note = self.note + ' - ' + addreasontext
self.url = getNormalStoryURL(self.url)
self.valid = self.url != None
def to_line(self):
# always 'url,'
return self.url+","+self.fullnote()
def fullnote(self):
retval = ""
if self.title and self.auth:
retval = retval + "%s by %s"%(self.title,self.auth)
if self.note:
retval = retval + " - "
if self.note:
retval = retval + self.note
return retval
# This is a more than slightly kludgey way to get
# EditWithComplete to *not* alpha-order the reasons, but leave
# them in the order entered. If
@@ -81,18 +130,8 @@ class DroppableQTextEdit(QTextEdit):
def dropEvent(self,event):
# print("event:%s"%event)
# print("event.mimeData():%s"%event.mimeData())
# print("event.mimeData().text():%s"%str(event.mimeData().text()))
# print("event.mimeData().data():%s"%str(event.mimeData().data()))
# print("event.mimeData().formats():%s"%[str(f) for f in event.mimeData().formats()])
# for f in event.mimeData().formats():
# try:
# print("event.mimeData().data('%s'):%s"%(f,event.mimeData().data(f)))
# except:
# print("failed %s"%f)
mimetype='text/uri-list'
# print("event.mimeData().data('%s'):%s"%(mimetype,event.mimeData().data(mimetype)))
urllist=[]
filelist="%s"%event.mimeData().data(mimetype)
@@ -133,25 +172,11 @@ class DroppableQTextEdit(QTextEdit):
class AddNewDialog(SizePersistedDialog):
def __init__(self, gui, prefs, icon, url_list_text, merge=False, newmerge=False):
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog')
self.gui = gui
self.merge = merge
self.newmerge = newmerge
go_signal = pyqtSignal(object, object, object, object)
if merge:
labeltext = 'Story URL(s) for anthology, one per line:'
tooltiptext = 'URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.'
collisiontext = 'If Story Already Exists in Anthology?'
collisiontooltip = "What to do if there's already an existing story with the same URL in the anthology."
else:
labeltext = 'Story URL(s), one per line:'
tooltiptext = 'URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.'
collisiontext = 'If Story Already Exists?'
collisiontooltip = "What to do if there's already an existing story with the same URL or title and author."
if prefs['adddialogstaysontop']:
QDialog.setWindowFlags ( self, Qt.Dialog|Qt.WindowStaysOnTopHint )
def __init__(self, gui, prefs, icon):
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog')
self.prefs = prefs
self.setMinimumWidth(300)
self.l = QVBoxLayout()
@@ -160,69 +185,158 @@ class AddNewDialog(SizePersistedDialog):
self.setWindowTitle('FanFictionDownLoader')
self.setWindowIcon(icon)
self.l.addWidget(QLabel(labeltext))
self.toplabel=QLabel("Toplabel")
self.l.addWidget(self.toplabel)
self.url = DroppableQTextEdit(self)
self.url.setToolTip(tooltiptext)
self.url.setToolTip("UrlTooltip")
self.url.setLineWrapMode(QTextEdit.NoWrap)
self.url.setText(url_list_text)
self.l.addWidget(self.url)
if not merge:
horz = QHBoxLayout()
label = QLabel('Output &Format:')
horz.addWidget(label)
self.fileform = QComboBox(self)
self.fileform.addItem('epub')
self.fileform.addItem('mobi')
self.fileform.addItem('html')
self.fileform.addItem('txt')
self.fileform.setCurrentIndex(self.fileform.findText(prefs['fileform']))
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)
horz.addWidget(self.fileform)
self.l.addLayout(horz)
if not newmerge:
horz = QHBoxLayout()
label = QLabel(collisiontext)
horz.addWidget(label)
self.collision = QComboBox(self)
self.collision.setToolTip(collisiontooltip)
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
label.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
horz = QHBoxLayout()
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(prefs['updatemeta'])
horz.addWidget(self.updatemeta)
if not merge: # hide if anthology merge.
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(prefs['updateepubcover'])
horz.addWidget(self.updateepubcover)
self.l.addLayout(horz)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
self.l.addWidget(button_box)
self.merge = self.newmerge = False
if url_list_text:
button_box.button(QDialogButtonBox.Ok).setFocus()
# elements to hide when doing merge.
self.mergehide = []
# elements to show again when doing *update* merge
self.mergeupdateshow = []
horz = QHBoxLayout()
label = QLabel('Output &Format:')
self.mergehide.append(label)
self.fileform = QComboBox(self)
self.fileform.addItem('epub')
self.fileform.addItem('mobi')
self.fileform.addItem('html')
self.fileform.addItem('txt')
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
self.fileform.activated.connect(self.set_collisions)
horz.addWidget(label)
label.setBuddy(self.fileform)
horz.addWidget(self.fileform)
self.l.addLayout(horz)
self.mergehide.append(self.fileform)
horz = QHBoxLayout()
self.collisionlabel = QLabel("CollisionLabel")
horz.addWidget(self.collisionlabel)
self.collision = QComboBox(self)
self.collision.setToolTip("CollisionToolTip")
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
self.collisionlabel.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
self.mergehide.append(self.collisionlabel)
self.mergehide.append(self.collision)
self.mergeupdateshow.append(self.collisionlabel)
self.mergeupdateshow.append(self.collision)
horz = QHBoxLayout()
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(prefs['updatemeta'])
horz.addWidget(self.updatemeta)
self.mergehide.append(self.updatemeta)
self.mergeupdateshow.append(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(prefs['updateepubcover'])
horz.addWidget(self.updateepubcover)
self.mergehide.append(self.updateepubcover)
self.l.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)
self.l.addWidget(self.button_box)
# invoke the
def ok_clicked(self):
self.hide()
print("ok_clicked called")
self.go_signal.emit( self.get_ffdl_options(),
self.get_urlstext(),
self.merge,
self.extrapayload )
def show_dialog(self,
url_list_text,
callback,
show=True,
merge=False,
newmerge=True,
extraoptions={},
extrapayload=None):
# rather than mutex in ffdl_plugin, just bail here if it's
# already in use.
if self.isVisible(): return
try:
self.go_signal.disconnect()
except:
pass # if not already connected.
self.go_signal.connect(callback)
self.merge = merge
self.newmerge = newmerge
self.extraoptions = extraoptions
self.extrapayload = extrapayload
if self.merge:
self.toplabel.setText('Story URL(s) for anthology, one per line:')
self.url.setToolTip('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.')
self.collisionlabel.setText('If Story Already Exists in Anthology?')
self.collision.setToolTip("What to do if there's already an existing story with the same URL in the anthology.")
for widget in self.mergehide:
widget.setVisible(False)
if not self.newmerge:
for widget in self.mergeupdateshow:
widget.setVisible(True)
else:
for widget in self.mergehide:
widget.setVisible(True)
self.toplabel.setText('Story URL(s), one per line:')
self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.')
self.collisionlabel.setText('If Story Already Exists?')
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
# Need to re-able after hiding/showing
self.setAcceptDrops(True)
self.url.setFocus()
if self.prefs['adddialogstaysontop']:
QDialog.setWindowFlags ( self, Qt.Dialog | Qt.WindowStaysOnTopHint )
else:
QDialog.setWindowFlags ( self, Qt.Dialog )
if not self.merge:
self.fileform.setCurrentIndex(self.fileform.findText(self.prefs['fileform']))
# add collision options
self.set_collisions()
i = self.collision.findText(self.prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
self.updatemeta.setChecked(self.prefs['updatemeta'])
if not self.merge:
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
self.url.setText(url_list_text)
if url_list_text:
self.button_box.button(QDialogButtonBox.Ok).setFocus()
# restore saved size.
self.resize_dialog()
if show: # so anthology update can be modal still.
self.show()
#self.resize(self.sizeHint())
def set_collisions(self):
@@ -240,27 +354,21 @@ class AddNewDialog(SizePersistedDialog):
self.collision.setCurrentIndex(i)
def get_ffdl_options(self):
retval = {
'fileform': unicode(self.fileform.currentText()),
'collision': unicode(self.collision.currentText()),
'updatemeta': self.updatemeta.isChecked(),
'updateepubcover': self.updateepubcover.isChecked(),
}
if self.merge:
retval['fileform']=='epub'
retval['updateepubcover']=True
if self.newmerge:
updatemeta=True
collision=ADDNEW
else:
updatemeta=self.updatemeta.isChecked()
collision=unicode(self.collision.currentText())
return {
'fileform': 'epub',
'collision': collision,
'updatemeta': updatemeta,
'updateepubcover': True,
}
else:
return {
'fileform': unicode(self.fileform.currentText()),
'collision': unicode(self.collision.currentText()),
'updatemeta': self.updatemeta.isChecked(),
'updateepubcover': self.updateepubcover.isChecked(),
}
retval['updatemeta']=True
retval['collision']=ADDNEW
return dict(retval.items() + self.extraoptions.items() )
def get_urlstext(self):
return unicode(self.url.toPlainText())
@@ -279,7 +387,6 @@ class CollectURLDialog(SizePersistedDialog):
'''
def __init__(self, gui, title, url_text, epubmerge_plugin=None):
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:get story urls')
self.gui = gui
self.status=False
self.anthology=False
@@ -333,7 +440,6 @@ class UserPassDialog(QDialog):
'''
def __init__(self, gui, site, exception=None):
QDialog.__init__(self, gui)
self.gui = gui
self.status=False
self.l = QGridLayout()
@@ -392,7 +498,6 @@ class LoopProgressDialog(QProgressDialog):
QString(), 0, len(book_list), gui)
self.setWindowTitle(win_title)
self.setMinimumWidth(500)
self.gui = gui
self.book_list = book_list
self.foreach_function = foreach_function
self.finish_function = finish_function
@@ -441,7 +546,6 @@ class LoopProgressDialog(QProgressDialog):
def do_when_finished(self):
self.hide()
self.gui = None
# Queues a job to process these books in the background.
self.finish_function(self.book_list)
@@ -490,7 +594,6 @@ class UpdateExistingDialog(SizePersistedDialog):
def __init__(self, gui, header, prefs, icon, books,
save_size_name='fanfictiondownloader_plugin:update list dialog'):
SizePersistedDialog.__init__(self, gui, save_size_name)
self.gui = gui
self.setWindowTitle(header)
self.setWindowIcon(icon)
@@ -508,11 +611,7 @@ class UpdateExistingDialog(SizePersistedDialog):
button_layout = QVBoxLayout()
books_layout.addLayout(button_layout)
# self.move_up_button = QtGui.QToolButton(self)
# self.move_up_button.setToolTip('Move selected books up the list')
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
# button_layout.addWidget(self.move_up_button)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
self.remove_button = QtGui.QToolButton(self)
@@ -522,11 +621,6 @@ class UpdateExistingDialog(SizePersistedDialog):
button_layout.addWidget(self.remove_button)
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem1)
# self.move_down_button = QtGui.QToolButton(self)
# self.move_down_button.setToolTip('Move selected books down the list')
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
# button_layout.addWidget(self.move_down_button)
options_layout = QHBoxLayout()
@@ -552,7 +646,6 @@ class UpdateExistingDialog(SizePersistedDialog):
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
# self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.')
label.setBuddy(self.collision)
options_layout.addWidget(self.collision)
@@ -661,11 +754,9 @@ class StoryListTableWidget(QTableWidget):
self.setItem(row, 2, AuthorTableWidgetItem(", ".join(book['author']), ", ".join(book['author_sort'])))
url_cell = ReadOnlyTableWidgetItem(book['url'])
#url_cell.setData(Qt.UserRole, QVariant(book['url']))
self.setItem(row, 3, url_cell)
comment_cell = ReadOnlyTableWidgetItem(book['comment'])
#comment_cell.setData(Qt.UserRole, QVariant(book))
self.setItem(row, 4, comment_cell)
def get_books(self):
@@ -699,56 +790,6 @@ class StoryListTableWidget(QTableWidget):
self.selectRow(row)
self.scrollToItem(self.currentItem())
def move_rows_up(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
first_sel_row = rows[0].row()
if first_sel_row <= 0:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in selrows:
self.swap_row_widgets(selrow - 1, selrow + 1)
scroll_to_row = first_sel_row - 1
if scroll_to_row > 0:
scroll_to_row = scroll_to_row - 1
self.scrollToItem(self.item(scroll_to_row, 0))
def move_rows_down(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
last_sel_row = rows[-1].row()
if last_sel_row == self.rowCount() - 1:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in reversed(selrows):
self.swap_row_widgets(selrow + 2, selrow)
scroll_to_row = last_sel_row + 1
if scroll_to_row < self.rowCount() - 1:
scroll_to_row = scroll_to_row + 1
self.scrollToItem(self.item(scroll_to_row, 0))
def swap_row_widgets(self, src_row, dest_row):
self.blockSignals(True)
self.insertRow(dest_row)
for col in range(0, self.columnCount()):
self.setItem(dest_row, col, self.takeItem(src_row, col))
self.removeRow(src_row)
self.blockSignals(False)
class RejectListTableWidget(QTableWidget):
def __init__(self, parent,rejectreasons=[]):
@@ -756,85 +797,53 @@ class RejectListTableWidget(QTableWidget):
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.rejectreasons = rejectreasons
def on_headersection_clicked(self):
self.setSortingEnabled(True)
def populate_table(self, reject_list):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(reject_list))
header_labels = ['URL', 'Note']
header_labels = ['URL', 'Title', 'Author', 'Note']
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.horizontalHeader().setStretchLastSection(True)
#self.verticalHeader().setDefaultSectionSize(24)
self.verticalHeader().hide()
# need sortingEnbled to sort, but off to up & down.
self.connect(self.horizontalHeader(),
SIGNAL('sectionClicked(int)'),
self.on_headersection_clicked)
# it's generally recommended to enable sort after pop, not
# before. But then it needs to be sorted on a column and I'd
# rather keep the order given.
self.setSortingEnabled(True)
# row is just row number.
for row, rejectrow in enumerate(reject_list):
#print("populating table:%s"%rejectrow.to_line())
self.populate_table_row(row,rejectrow)
self.resizeColumnsToContents()
self.setMinimumColumnWidth(1, 100)
self.setMinimumColumnWidth(2, 100)
self.setMinimumColumnWidth(0, 100)
self.setMinimumColumnWidth(3, 100)
self.setMinimumSize(300, 0)
def setMinimumColumnWidth(self, col, minimum):
if self.columnWidth(col) < minimum:
self.setColumnWidth(col, minimum)
def populate_table_row(self, row, rejectrow):
(bookid,url,titleauth,oldrejnote) = rejectrow
if oldrejnote:
noteprefix = note = oldrejnote
# incase the existing note ends with one of the known reasons.
for reason in self.rejectreasons:
if noteprefix.endswith(' - '+reason):
noteprefix = noteprefix[:-len(' - '+reason)]
break
else:
noteprefix = note = titleauth
if len(noteprefix) > 0:
noteprefix = noteprefix+' - '
url_cell = ReadOnlyTableWidgetItem(url)
url_cell.setData(Qt.UserRole, QVariant(bookid))
url_cell.setToolTip('URL to add to the Reject List.')
self.setItem(row, 0, url_cell)
def populate_table_row(self, row, rej):
self.setItem(row, 0, ReadOnlyTableWidgetItem(rej.url))
self.setItem(row, 1, ReadOnlyTableWidgetItem(rej.title))
self.setItem(row, 2, ReadOnlyTableWidgetItem(rej.auth))
note_cell = EditWithComplete(self)
note_cell.lineEdit().mcompleter.model().set_items = \
partial(complete_model_set_items_kludge,
note_cell.lineEdit().mcompleter.model())
items = [note]+[ noteprefix+x for x in self.rejectreasons ]
items = [rej.note]+self.rejectreasons
note_cell.update_items_cache(items)
note_cell.show_initial_value(note)
note_cell.show_initial_value(rej.note)
note_cell.set_separator(None)
note_cell.setToolTip('Select or Edit Reject Note.')
self.setCellWidget(row, 1, note_cell)
self.setCellWidget(row, 3, note_cell)
# note_cell = QTableWidgetItem(note)
# note_cell.setToolTip('Double-click to edit note.')
# self.setItem(row, 1, note_cell)
def get_reject_list(self):
rejectrows = []
for row in range(self.rowCount()):
bookid = self.item(row, 0).data(Qt.UserRole).toPyObject()
url = unicode(self.item(row, 0).text())
note = unicode(self.cellWidget(row, 1).currentText()).strip()
rejectrows.append((bookid,url,note))
return rejectrows
def remove_selected_rows(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
@@ -857,57 +866,6 @@ class RejectListTableWidget(QTableWidget):
self.selectRow(row)
self.scrollToItem(self.currentItem())
def move_rows_up(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
first_sel_row = rows[0].row()
if first_sel_row <= 0:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in selrows:
self.swap_row_widgets(selrow - 1, selrow + 1)
scroll_to_row = first_sel_row - 1
if scroll_to_row > 0:
scroll_to_row = scroll_to_row - 1
self.scrollToItem(self.item(scroll_to_row, 0))
def move_rows_down(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
last_sel_row = rows[-1].row()
if last_sel_row == self.rowCount() - 1:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in reversed(selrows):
self.swap_row_widgets(selrow + 2, selrow)
scroll_to_row = last_sel_row + 1
if scroll_to_row < self.rowCount() - 1:
scroll_to_row = scroll_to_row + 1
self.scrollToItem(self.item(scroll_to_row, 0))
def swap_row_widgets(self, src_row, dest_row):
self.blockSignals(True)
self.setSortingEnabled(False)
self.insertRow(dest_row)
for col in range(0, self.columnCount()):
self.setItem(dest_row, col, self.takeItem(src_row, col))
self.removeRow(src_row)
self.blockSignals(False)
class RejectListDialog(SizePersistedDialog):
def __init__(self, gui, reject_list,
rejectreasons=[],
@@ -917,7 +875,6 @@ class RejectListDialog(SizePersistedDialog):
show_all_reasons=True,
save_size_name='ffdl:reject list dialog'):
SizePersistedDialog.__init__(self, gui, save_size_name)
self.gui = gui
self.setWindowTitle(header)
self.setWindowIcon(get_icon(icon))
@@ -937,21 +894,13 @@ class RejectListDialog(SizePersistedDialog):
rejects_layout.addLayout(button_layout)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
# self.move_up_button = QtGui.QToolButton(self)
# self.move_up_button.setToolTip('Move selected books up the list')
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
# button_layout.addWidget(self.move_up_button)
self.remove_button = QtGui.QToolButton(self)
self.remove_button.setToolTip('Remove selected URL(s) from the list')
self.remove_button.setIcon(get_icon('list_remove.png'))
self.remove_button.clicked.connect(self.remove_from_list)
button_layout.addWidget(self.remove_button)
# self.move_down_button = QtGui.QToolButton(self)
# self.move_down_button.setToolTip('Move selected books down the list')
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
# button_layout.addWidget(self.move_down_button)
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem1)
@@ -998,10 +947,21 @@ class RejectListDialog(SizePersistedDialog):
self.rejects_table.remove_selected_rows()
def get_reject_list(self):
return self.rejects_table.get_reject_list()
rejectrows = []
for row in range(self.rejects_table.rowCount()):
url = unicode(self.rejects_table.item(row, 0).text()).strip()
title = unicode(self.rejects_table.item(row, 1).text()).strip()
auth = unicode(self.rejects_table.item(row, 2).text()).strip()
note = unicode(self.rejects_table.cellWidget(row, 3).currentText()).strip()
rejectrows.append(RejectUrlEntry(url,note,title,auth,self.get_reason_text()))
return rejectrows
def get_reason_text(self):
return unicode(self.reason_edit.currentText()).strip()
try:
return unicode(self.reason_edit.currentText()).strip()
except:
# doesn't have self.reason_edit when editing existing list.
return None
def get_deletebooks(self):
return self.deletebooks.isChecked()
+97 -90
View File
@@ -47,7 +47,7 @@ from calibre_plugins.fanfictiondownloader_plugin.dialogs import (
AddNewDialog, UpdateExistingDialog,
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog, RejectListDialog,
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY,
NotGoingToDownload )
NotGoingToDownload, RejectUrlEntry )
# because calibre immediately transforms html into zip and don't want
# to have an 'if html'. db.has_format is cool with the case mismatch,
@@ -102,7 +102,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# are not found in the zip file will result in null QIcons.
icon = get_icon('images/icon.png')
#self.qaction.setText('FFDL')
self.qaction.setText('FanFictionDL')
# The qaction is automatically created from the action_spec defined
# above
@@ -126,6 +126,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# been displayed once.
self.rebuild_menus()
self.add_new_dialog = AddNewDialog(self.gui,
prefs,
self.qaction.icon())
## Kludgey, yes, but with the real configuration inside the
## library now, how else would a user be able to change this
## setting if it's crashing calibre?
@@ -136,7 +140,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
file_path = os.path.join(calibre_config_dir,
*("plugins/fanfictiondownloader_macmenuhack.txt".split('/')))
file_path = os.path.abspath(file_path)
print("macmenuhack file_path:%s"%file_path)
print("Plugin %s macmenuhack file_path:%s"%(self.name,file_path))
self.macmenuhack = os.access(file_path, os.F_OK)
return self.macmenuhack
@@ -399,17 +403,17 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# want title/author, too, for rejects.
self.populate_book_from_calibre_id(book,db)
if book['url']:
# get existing note, if there is one.
book['oldrejnote']=rejecturllist.check(book['url'])
# get existing note, if on rejected list.
book['oldrejnote']=rejecturllist.get_note(book['url'])
def reject_list_urls_finish(self, book_list):
# construct reject list of tuples:
# (calibre_id, url, "title, authors", old reject note).
reject_list = [ ( x['calibre_id'],x['url'],
"%s by %s"%(x['title'],
', '.join(x['author'])),
x['oldrejnote'])
reject_list = [ RejectUrlEntry(x['url'],
x['oldrejnote'],
x['title'],
', '.join(x['author']))
for x in book_list if x['good'] ]
if reject_list:
d = RejectListDialog(self.gui,reject_list,
@@ -419,19 +423,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
if d.result() != d.Accepted:
return
bookids=[]
rejectlist=[]
addreasontext=d.get_reason_text()
for (bookid,url,note) in d.get_reject_list():
bookids.append(bookid)
if addreasontext and note:
note = note +" - "+addreasontext
elif addreasontext:
note = addreasontext
rejectlist.append((url,note))
print("Adding (%s) to Reject List: %s"%(url,note))
rejecturllist.add(rejectlist)
rejecturllist.add(d.get_reject_list())
if d.get_deletebooks():
self.gui.iactions['Remove Books'].delete_books()
@@ -440,41 +432,22 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
message="<p>Rejecting FFDL URLs: None of the books selected have FanFiction URLs.</p><p>Proceed to Remove?</p>"
if confirm(message,'fanfictiondownloader_reject_non_fanfiction', self.gui):
self.gui.iactions['Remove Books'].delete_books()
def add_dialog(self,url_list_text=None,merge=False,anthology_url=None):
#print("add_dialog()")
def add_dialog(self,url_list_text=None,merge=False,anthology_url=None):
'Both new individual stories and new anthologies are created here.'
if not url_list_text:
url_list = self.get_urls_clip()
url_list_text = "\n".join(url_list)
# self.gui is the main calibre GUI. It acts as the gateway to access
# all the elements of the calibre user interface, it should also be the
# parent of the dialog
# AddNewDialog just collects URLs, format and presents buttons.
d = AddNewDialog(self.gui,
prefs,
self.qaction.icon(),
url_list_text,
merge=merge,
newmerge=merge # if here, it's a new anthology.
)
d.exec_()
if d.result() != d.Accepted:
return
url_list = split_text_to_urls(d.get_urlstext())
add_books = self.convert_urls_to_books(url_list)
#print("add_books:%s"%add_books)
#print("options:%s"%d.get_ffdl_options())
options = d.get_ffdl_options()
options['version'] = self.version
options['anthology_url']=anthology_url
print(self.version)
self.prep_downloads( options, add_books, merge=merge )
# AddNewDialog collects URLs, format and presents buttons.
# add_new_dialog is modeless and reused, both for new stories
# and anthologies, and for updating existing anthologies.
self.add_new_dialog.show_dialog(url_list_text,
self.prep_downloads,
merge=merge,
newmerge=True,
extraoptions={'anthology_url':anthology_url})
def update_anthology(self):
if not self.get_epubmerge_plugin():
@@ -488,7 +461,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
if len(self.gui.library_view.get_selected_ids()) != 1:
self.gui.status_bar.show_message(_('Can only update 1 anthology at a time'), 3000)
return
#print("update_existing()")
db = self.gui.current_db
book_id = self.gui.library_view.get_selected_ids()[0]
@@ -530,25 +502,30 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
url_list_text = "\n".join(url_list)
#print("urlmapfile:%s"%urlmapfile)
# AddNewDialog collects URLs, format and presents buttons.
# add_new_dialog is modeless and reused, both for new stories
# and anthologies, and for updating existing anthologies.
self.add_new_dialog.show_dialog(url_list_text,
self.prep_anthology_downloads,
show=False,
merge=True,
newmerge=False,
extrapayload=urlmapfile,
extraoptions={'tdir':tdir,
'mergebook':mergebook})
# Need to use AddNewDialog modal here because it's an update
# of an existing book. Don't want the user deleting it or
# switching libraries on us.
self.add_new_dialog.exec_()
# self.gui is the main calibre GUI. It acts as the gateway to access
# all the elements of the calibre user interface, it should also be the
# parent of the dialog
# AddNewDialog just collects URLs, format and presents buttons.
d = AddNewDialog(self.gui,
prefs,
self.qaction.icon(),
url_list_text,
merge=True,
newmerge=False
)
d.exec_()
if d.result() != d.Accepted:
return
url_list = split_text_to_urls(d.get_urlstext())
update_books = self.convert_urls_to_books(url_list)
def prep_anthology_downloads(self, options, update_books,
merge=False, urlmapfile=None):
if isinstance(update_books,basestring):
url_list = split_text_to_urls(update_books)
update_books = self.convert_urls_to_books(url_list)
for j, book in enumerate(update_books):
url = book['url']
@@ -574,13 +551,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
print("Canceling anthology update due to removed stories.")
return
options = d.get_ffdl_options()
options['version'] = self.version
options['tdir'] = tdir
#options['collision'] = UPDATEALWAYS
print(self.version)
options['mergebook'] = mergebook
# Now that we've
self.prep_downloads( options, update_books, merge=True )
def update_dialog(self):
@@ -631,8 +602,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# only if there's some good ones.
if 0 < len(filter(lambda x : x['good'], update_books)):
options = d.get_ffdl_options()
options['version'] = self.version
print(self.version)
self.prep_downloads( options, update_books )
def get_urls_clip(self,storyurls=True):
@@ -648,9 +617,16 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# No need to do anything with perfs here, but we could.
prefs
def prep_downloads(self, options, books, merge=False):
def prep_downloads(self, options, books, merge=False, extrapayload=None):
'''Fetch metadata for stories from servers, launch BG job when done.'''
if isinstance(books,basestring):
url_list = split_text_to_urls(books)
books = self.convert_urls_to_books(url_list)
options['version'] = self.version
print(self.version)
#print("prep_downloads:%s"%books)
if 'tdir' not in options: # if merging an anthology, there's alread a tdir.
@@ -687,12 +663,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
print("url:%s"%url)
if not merge: # skip reject list when merging.
rejnote = rejecturllist.check(url)
if rejnote:
if rejecturllist.check(url):
rejnote = rejecturllist.get_full_note(url)
if question_dialog(self.gui, 'Reject URL?',
'<p>Reject URL?</p>'+
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
"<p>Click 'No' to download anyway.</p>",
'<h3>Reject URL?</h3>'+
'<p><b>%s</b> is on your Reject URL list:</p><p>"<b>%s</b>"</p>'%(url,rejnote)+
"<p>Click '<b>Yes</b>' to Reject.</p>"+
"<p>Click '<b>No</b>' to download anyway.</p>",
show_copy_button=False):
book['comment'] = "Story on Reject URLs list (%s)."%rejnote
book['good']=False
@@ -701,9 +678,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
return
else:
if question_dialog(self.gui, 'Remove Reject URL?',
"<p>Remove URL from Reject List?</p>"+
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
"<p>Click 'Yes' to remove it from the list and download,<br /> 'No' to download, but leave it on the Reject list.</p>",
"<h3>Remove URL from Reject List?</h3>"+
'<p><b>%s</b> is on your Reject URL list:</p><p>"<b>%s</b>"</p>'%(url,rejnote)+
"<p>Click '<b>Yes</b>' to remove it from the list,</p>"+
"<p>Click '<b>No</b>' to leave it on the list.</p>",
show_copy_button=False):
rejecturllist.remove(url)
@@ -753,6 +731,31 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# let other exceptions percolate up.
story = adapter.getStoryMetadataOnly()
series = story.getMetadata('series')
if not merge and series and prefs['checkforseriesurlid']:
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
searchstr = 'identifiers:"~ur(i|l):=%s"'%story.getMetadata('seriesUrl').replace(":","|")
identicalbooks = db.search_getting_ids(searchstr, None)
# print("searchstr:%s"%searchstr)
# print("identicalbooks:%s"%identicalbooks)
if len(identicalbooks) > 0 and question_dialog(self.gui, 'Skip Story?',
'<h3>Skip Anthology Story?</h3>'+
'<p>"<b>%s</b>" is in series "<b><a href="%s">%s</a></b>" that you have an anthology book for.</p>'%
(story.getMetadata('title'),story.getMetadata('seriesUrl'),series[:series.index(' [')])+
"<p>Click '<b>Yes</b>' to Skip.</p>"+
"<p>Click '<b>No</b>' to download anyway.</p>",
show_copy_button=False):
book['comment'] = "Story in Series Anthology(%s)."%series
book['title'] = story.getMetadata('title')
book['author'] = [story.getMetadata('author')]
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'])
@@ -1363,9 +1366,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
else:
coldef = custom_columns[custcol]
label = coldef['label']
if flag == 'r' or book['added']: # flag 'n' isn't actually needed--*always* set if configured and new book.
db.set_custom(book_id, book['all_metadata'][meta], label=label, commit=False)
if coldef['datatype'] in ('int','float'): # for favs, etc--site specific metadata.
val = unicode(book['all_metadata'][meta]).replace(",","")
else:
val = book['all_metadata'][meta]
db.set_custom(book_id, val, label=label, commit=False)
if flag == 'a':
vallist = []
@@ -1526,7 +1533,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
def convert_url_to_book(self, url):
book = self.make_book()
# look here for [\d,\d] at end of url, and remove?
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>,)?(?P<end>\d+)?\])?$",url)
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>[,-])?(?P<end>\d+)?\])?$",url)
#print("url:(%s) begin:(%s) end:(%s)"%(mc.group('url'),mc.group('begin'),mc.group('end')))
url = mc.group('url')
book['begin'] = mc.group('begin')
+9 -8
View File
@@ -39,6 +39,7 @@ default_prefs['deleteotherforms'] = False
default_prefs['adddialogstaysontop'] = False
default_prefs['includeimages'] = False
default_prefs['lookforurlinhtml'] = False
default_prefs['checkforseriesurlid'] = True
default_prefs['injectseries'] = False
default_prefs['send_lists'] = ''
@@ -60,6 +61,13 @@ default_prefs['allow_custcol_from_ini'] = True
default_prefs['std_cols_newonly'] = {}
# This is where all preferences for this plugin *were* stored
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
def set_library_config(library_config,db):
db.prefs.set_namespaced(PREFS_NAMESPACE,
PREFS_KEY_SETTINGS,
@@ -73,7 +81,7 @@ def get_library_config(db):
if library_id in old_prefs:
#print("get prefs from old_prefs")
library_config = old_prefs[library_id]
set_library_config(library_config)
set_library_config(library_config,db)
old_prefs["migrated to library db %s"%library_id] = old_prefs[library_id]
del old_prefs[library_id]
@@ -83,13 +91,6 @@ def get_library_config(db):
copy.deepcopy(default_prefs))
return library_config
# This is where all preferences for this plugin *were* stored
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
# fake out so I don't have to change the prefs calls anywhere. The
# Java programmer in me is offended by op-overloading, but it's very
# tidy.
+16 -6
View File
@@ -44,6 +44,9 @@ language_label:Language
characters_label:Characters
ships_label:Relationships
series_label:Series
seriesUrl_label:Series URL
## seriesHTML is series as a link to seriesUrl.
seriesHTML_label:Series
## Completed/In-Progress
status_label:Status
## Dates story first published, last updated, and downloaded(last with time).
@@ -81,7 +84,7 @@ dateUpdated_format:%%Y-%%m-%%d
## You can include extra text or HTML that will be included as-is in
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
## All current formats already include title and author.
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
## Try to collect series name and number of this story in series.
## Some sites (ab)use 'series' for reading lists and personal
@@ -270,7 +273,7 @@ output_css:
[txt]
## Add URLs since there aren't links.
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
titlepage_entries: series,seriesUrl,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
## Width to word wrap text output. 0 indicates no wrapping.
wrap_width: 78
@@ -304,13 +307,13 @@ include_logpage: false
## if in the list. You can include extra text or HTML that will be
## included as-is in each log entry. Eg: logpage_entries: ...,<br />,
## summary,<br />,...
logpage_entries: dateCreated,datePublished,dateUpdated,numChapters,numWords,status,title,author,description,category,genre,rating,warnings
logpage_entries: dateCreated,datePublished,dateUpdated,numChapters,numWords,status,series,title,author,description,category,genre,rating,warnings
## epub->mobi conversions typically don't like tables.
titlepage_use_table: false
## When using tables, make these span both columns.
wide_titlepage_entries: description, storyUrl, author URL
wide_titlepage_entries: description, storyUrl, authorUrl, seriesUrl
## output background color--only used by html and epub (and ignored in
## epub by many readers). Included below in output_css--will be
@@ -969,6 +972,13 @@ extracategories:InuYasha
extracharacters:Sesshoumaru,Kagome
extraships:Sesshoumaru/Kagome
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
[www.dotmoon.net]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
@@ -1219,12 +1229,12 @@ extraships:Harry Potter/Ginny Weasley
# www.squidge.org/peja calls it Fandom <shrug>
category_label:Fandom
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
[www.squidge.org/peja:txt]
## Add URLs since there aren't links.
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
titlepage_entries: series,seriesUrl,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
[www.storiesofarda.com]
## Site dedicated to these categories/characters/ships
+5 -2
View File
@@ -94,6 +94,9 @@ def main(argv,
parser.add_option("-l", "--list",
action="store_true", dest="list",
help="Get list of valid story URLs from page given.",)
parser.add_option("-n", "--normalize-list",
action="store_true", dest="normalize",default=False,
help="Get list of valid story URLs from page given, but normalized to standard forms.",)
parser.add_option("-d", "--debug",
action="store_true", dest="debug",
help="Show debug output while downloading.",)
@@ -169,8 +172,8 @@ def main(argv,
(var,val) = opt.split('=')
configuration.set("overrides",var,val)
if options.list:
retlist = get_urls_from_page(args[0], configuration)
if options.list or options.normalize:
retlist = get_urls_from_page(args[0], configuration, normalize=options.normalize)
print "\n".join(retlist)
return
+1 -1
View File
@@ -153,7 +153,7 @@ def getAdapter(config,url):
#logger.debug("trying url:"+url)
(cls,fixedurl) = getClassFor(url)
logger.debug("fixedurl:"+fixedurl)
#logger.debug("fixedurl:"+fixedurl)
if cls:
adapter = cls(config,fixedurl) # raises InvalidStoryURL
return adapter
@@ -195,6 +195,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
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
@@ -295,6 +295,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
series_url = 'http://'+self.host+'/fanfic/'+b['href']
series_index = int(a.text.split(' ')[1])
self.setSeries(series_name, series_index)
self.story.setMetadata('seriesUrl',series_url)
except:
# I find it hard to care if the series parsing fails
@@ -311,6 +311,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
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
@@ -284,6 +284,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
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
@@ -212,6 +212,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
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
@@ -252,6 +252,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -218,6 +218,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
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
+47 -7
View File
@@ -81,6 +81,44 @@ class DokugaComAdapter(BaseSiteAdapter):
def getSiteURLPattern(self):
return r"http://"+self.getSiteDomain()+"/(fanfiction|spark)?/story/\d+/?\d+?$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if 'The author has disabled anonymous viewing for this story.' in data:
return True
else:
return False
def performLogin(self, url,soup):
params = {}
if self.password:
params['username'] = self.username
params['passwd'] = self.password
else:
params['username'] = self.getConfig("username")
params['passwd'] = self.getConfig("password")
params['Submit'] = 'Submit'
# copy all hidden input tags to pick up appropriate tokens.
for tag in soup.findAll('input',{'type':'hidden'}):
params[tag['name']] = tag['value']
loginUrl = 'http://' + self.getSiteDomain() + '/fanfiction'
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
params['username']))
d = self._postUrl(loginUrl, params)
if "Your session has expired. Please log in again." in d:
d = self._postUrl(loginUrl, params)
if "Logout" not in d : #Member Account
logger.info("Failed to login to URL %s as %s" % (loginUrl,
params['username']))
raise exceptions.FailedToLogin(url,params['username'])
return False
else:
return True
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
@@ -97,12 +135,18 @@ class DokugaComAdapter(BaseSiteAdapter):
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
if self.needToLoginCheck(data):
# need to log in for this one.
self.performLogin(url,soup)
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data)
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.
@@ -218,10 +262,6 @@ class DokugaComAdapter(BaseSiteAdapter):
a=div.text.split('Words ')
if len(a)==2: self.story.setMetadata('numWords', a[1])
# grab the text for an individual chapter.
def getChapterText(self, url):
@@ -273,6 +273,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -274,6 +274,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
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
@@ -195,6 +195,7 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
name=seriessoup.find('div', {'id' : 'pagetitle'})
name.find('a').extract()
self.setSeries(name.text.split(' by[')[0], i)
self.story.setMetadata('seriesUrl',series_url)
i=0
break
i+=1
@@ -280,6 +280,7 @@ class EFPFanFicNet(BaseSiteAdapter):
for a in storyas:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId'))+'&i=1':
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -230,6 +230,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -132,9 +132,10 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
## For 1, use the second link.
## For 2, fetch the crossover page and pull the two categories from there.
categories = soup.findAll('a',{'class':'xcontrast_txt'})
categories = soup.find('div',{'id':'pre_story_links'}).findAll('a',{'class':'xcontrast_txt'})
#print("xcontrast_txt a:%s"%categories)
if len(categories) > 1:
self.story.addToList('category',stripHTML(categories[-1]))
self.story.addToList('category',stripHTML(categories[1]))
elif 'Crossover' in categories[0]['href']:
caturl = "http://%s%s"%(self.getSiteDomain(),categories[0]['href'])
catsoup = bs.BeautifulSoup(self._fetchUrl(caturl))
@@ -158,12 +159,17 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
if summarydiv:
self.setDescription(url,stripHTML(summarydiv))
metatext = stripHTML(gui_table1i.find('div', {'style':'color:gray;'})).replace('Hurt/Comfort','Hurt-Comfort')
grayspan = gui_table1i.find('span', {'class':'xgray xcontrast_txt'})
# for b in grayspan.findAll('button'):
# b.extract()
metatext = stripHTML(grayspan).replace('Hurt/Comfort','Hurt-Comfort')
#logger.debug("metatext:(%s)"%metatext)
metalist = metatext.split(" - ")
#logger.debug("metatext:(%s)"%metalist)
#logger.debug("metalist:(%s)"%metalist)
# Rated: Fiction K - English - Words: 158,078 - Published: 02-04-11
# Rated: Fiction T - English - Adventure/Sci-Fi - Naruto U. - Chapters: 22 - Words: 114,414 - Reviews: 395 - Favs: 779 - Follows: 835 - Updated: 03-21-13 - Published: 04-28-12 - id: 8067258
# rating is obtained above more robustly.
if metalist[0].startswith('Rated:'):
@@ -179,7 +179,11 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
self.setCoverImage(self.url,coverurl)
self.setDescription(self.url,soup.find("div", {"class":"description"}))
# fimf has started including extra stuff inside the description div.
descdivstr = "%s"%soup.find("div", {"class":"description"})
hrstr="<hr />"
descdivstr = '<div class="description">'+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
self.setDescription(self.url,descdivstr)
# Dates are in Unix time
# Take the publish date from the first chapter posted
@@ -188,7 +192,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
rawDateUpdated = storyMetadata["date_modified"]
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
chars = soup.find("div", {"class":"story"})
chars = soup.find("div", {"class":"inner_data"})
# fimfic stopped putting the char name on or around the char
# icon now for some reason. Pull it from the image name with
# some heuristics.
@@ -187,6 +187,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
i = a.parent.text.split('(')[1].split(')')[0]
self.setSeries(a.text, i)
self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
except:
pass
try:
@@ -275,6 +275,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -206,6 +206,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -193,6 +193,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -264,6 +264,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -234,6 +234,7 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
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
@@ -285,6 +285,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -225,6 +225,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -212,6 +212,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
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
@@ -268,6 +268,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -260,6 +260,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -306,6 +306,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -163,6 +163,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
if 'Series' in label:
self.setSeries(stripHTML(value.nextSibling), value.nextSibling.nextSibling.string[2:])
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+value.nextSibling['href'])
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
story=asoup.find('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId')))
@@ -164,6 +164,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
if 'Series' in label:
if "No Series" not in value.nextSibling.string:
self.setSeries(stripHTML(value.nextSibling), value.nextSibling.nextSibling.string[2:])
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+value.nextSibling['href'])
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
story=asoup.find('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId')))
@@ -193,7 +193,7 @@ class NCISFictionNetAdapter(BaseSiteAdapter):
series_name = a.find('a').string
i = a.text.split('#')[1]
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+a.find('a')['href'])
except:
# I find it hard to care if the series parsing fails
pass
@@ -264,6 +264,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -244,6 +244,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
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
@@ -275,6 +275,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -225,6 +225,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -182,6 +182,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
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
@@ -228,6 +228,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -193,6 +193,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
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
@@ -223,6 +223,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -236,6 +236,7 @@ class QafFicComAdapter(BaseSiteAdapter):
name=seriessoup.find('div', {'id' : 'pagetitle'})
name.find('a').extract()
self.setSeries(name.text.split(' by[')[0], i)
self.story.setMetadata('seriesUrl',series_url)
i=0
break
i+=1
@@ -207,6 +207,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
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
@@ -222,6 +222,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -233,6 +233,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -226,6 +226,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -215,6 +215,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
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
@@ -212,6 +212,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
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
@@ -204,6 +204,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
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
@@ -249,6 +249,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -220,6 +220,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
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
+28 -11
View File
@@ -101,8 +101,10 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
}
self.story.setMetadata('language',langs[idnum%len(langs)])
self.setSeries('The Great Test',idnum)
self.story.setMetadata('seriesUrl','http://test1.com?seriesid=1')
if idnum == 0:
self.setSeries("A Nook Hyphen Test "+self.story.getMetadata('dateCreated'),idnum)
self.story.setMetadata('seriesUrl','http://test1.com?seriesid=0')
self.story.setMetadata('rating','Tweenie')
@@ -126,21 +128,36 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
self.story.addToList('warnings','Swearing')
self.story.addToList('warnings','Violence')
self.story.addToList('category','Harry Potter')
self.story.addToList('category','Furbie')
self.story.addToList('category','Crossover')
self.story.addToList('category',u'Puella Magi Madoka Magica/魔法少女まどか★マギカ')
self.story.addToList('category',u'Magical Girl Lyrical Nanoha')
if self.story.getMetadata('storyId') == '80':
self.story.addToList('category',u'Rizzoli & Isles')
self.story.addToList('characters','J. Rizzoli')
elif self.story.getMetadata('storyId') == '81':
self.story.addToList('category',u'Pitch Perfect')
self.story.addToList('characters','Chloe B.')
elif self.story.getMetadata('storyId') == '83':
self.story.addToList('category',u'Rizzoli & Isles')
self.story.addToList('characters','J. Rizzoli')
self.story.addToList('category',u'Pitch Perfect')
self.story.addToList('characters','Chloe B.')
elif self.story.getMetadata('storyId') == '82':
self.story.addToList('characters','Henry (Once Upon a Time)')
self.story.addToList('category',u'Once Upon a Time (TV)')
else:
self.story.addToList('category','Harry Potter')
self.story.addToList('category','Furbie')
self.story.addToList('category','Crossover')
self.story.addToList('category',u'Puella Magi Madoka Magica/魔法少女まどか★マギカ')
self.story.addToList('category',u'Magical Girl Lyrical Nanoha')
self.story.addToList('category',u'Once Upon a Time (TV)')
self.story.addToList('characters','Bob Smith')
self.story.addToList('characters','George Johnson')
self.story.addToList('characters','Fred Smythe')
self.story.addToList('genre','Fantasy')
self.story.addToList('genre','Comedy')
self.story.addToList('genre','Sci-Fi')
self.story.addToList('genre','Noir')
self.story.addToList('characters','Bob Smith')
self.story.addToList('characters','George Johnson')
self.story.addToList('characters','Fred Smythe')
self.story.addToList('listX','xVal1')
self.story.addToList('listX','xVal2')
self.story.addToList('listX','xVal3')
@@ -189,6 +189,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
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
@@ -283,6 +283,7 @@ class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
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
@@ -263,6 +263,7 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
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
@@ -227,6 +227,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
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
@@ -163,17 +163,44 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
infodata = self._fetchUrl(infourl)
infosoup = bs.BeautifulSoup(infodata)
for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")):
self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1])
self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-"))
self.story.addToList('author',stripHTML(a))
# for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")):
# self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1])
# self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-"))
# self.story.addToList('author',stripHTML(a))
# second verticaltable is the chapter list.
table = infosoup.findAll('table',{'class':'verticaltable'})[1]
for a in table.findAll('a',href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))):
autha = a.findNext('a',href=re.compile(r"^/Author-\d+"))
self.story.addToList('authorId',autha['href'].split('/')[1].split('-')[1])
self.story.addToList('authorUrl','http://'+self.host+autha['href'].replace("/Author-","/AuthorStories-"))
self.story.addToList('author',stripHTML(autha))
# include leading number to match 1. ... 2. ...
self.chapterUrls.append(("%d. %s by %s"%(len(self.chapterUrls)+1,
stripHTML(a),
stripHTML(autha)),'http://'+self.host+a['href']))
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(url)
else:
raise e
else: # single author:
# Find the chapter selector
select = soup.find('select', { 'name' : 'chapnav' } )
if select is None:
# no selector found, so it's a one-chapter story.
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
allOptions = select.findAll('option')
for o in allOptions:
url = "http://"+self.host+o['value']
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(o),url))
self.story.setMetadata('numChapters',len(self.chapterUrls))
try:
# going to pull part of the meta data from *primary* author list page.
logger.debug("**AUTHOR** URL: "+authorurl)
@@ -235,29 +262,12 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
if BtVS:
self.story.addToList('category','Buffy: The Vampire Slayer')
# Find the chapter selector
select = soup.find('select', { 'name' : 'chapnav' } )
if select is None:
# no selector found, so it's a one-chapter story.
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
allOptions = select.findAll('option')
for o in allOptions:
url = "http://"+self.host+o['value']
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(o),url))
self.story.setMetadata('numChapters',len(self.chapterUrls))
pseries = soup.find('p', {'style':'margin-top:0px'})
m = re.match('This story is No\. (?P<num>\d+) in the series &quot;(?P<series>.+)&quot;\.',
pseries.text)
if m:
self.setSeries(m.group('series'),m.group('num'))
return
self.story.setMetadata('seriesUrl',"http://"+self.host+pseries.find('a')['href'])
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
@@ -163,6 +163,7 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
for a in storyas:
if a['href'] == ('/read/'+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -217,6 +217,7 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
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
@@ -246,6 +246,7 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
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
@@ -206,6 +206,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
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
@@ -204,6 +204,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
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
@@ -277,6 +277,7 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -194,6 +194,7 @@ class WolverineAndRogueComAdapter(BaseSiteAdapter):
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
@@ -126,8 +126,13 @@ class WraithBaitComAdapter(BaseSiteAdapter):
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# include author on chapters if multiple authors.
if len(alist) > 1:
add = " by %s"%stripHTML(chapter.findNext('a', href=re.compile(r"viewuser.php\?uid=\d+")))
else:
add = ""
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
self.chapterUrls.append((stripHTML(chapter)+add,'http://'+self.host+'/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
@@ -196,6 +201,7 @@ class WraithBaitComAdapter(BaseSiteAdapter):
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
+2
View File
@@ -50,6 +50,7 @@ class Configuration(ConfigParser.SafeConfigParser):
'characters',
'ships',
'series',
'seriesUrl',
'status',
'datePublished',
'dateUpdated',
@@ -75,6 +76,7 @@ class Configuration(ConfigParser.SafeConfigParser):
'langcode',
'output_css',
'authorHTML',
'seriesHTML',
'lastupdate'
]
+16 -10
View File
@@ -25,7 +25,7 @@ from gziphttp import GZipProcessor
import adapters
from configurable import Configuration
def get_urls_from_page(url,configuration=None):
def get_urls_from_page(url,configuration=None,normalize=False):
if not configuration:
configuration = Configuration("test1.com","EPUB")
@@ -54,11 +54,11 @@ def get_urls_from_page(url,configuration=None):
opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
data = opener.open(url).read()
return get_urls_from_html(data,url)
return get_urls_from_html(data,url,configuration,normalize)
def get_urls_from_html(data,url=None,configuration=None):
def get_urls_from_html(data,url=None,configuration=None,normalize=False):
normalized = set() # normalized url
normalized = [] # normalized url
retlist = [] # orig urls.
if not configuration:
@@ -82,16 +82,19 @@ def get_urls_from_html(data,url=None,configuration=None):
href = href.replace('&index=1','')
adapter = adapters.getAdapter(configuration,href)
if adapter.story.getMetadata('storyUrl') not in normalized:
normalized.add(adapter.story.getMetadata('storyUrl'))
normalized.append(adapter.story.getMetadata('storyUrl'))
retlist.append(href)
except:
pass
return retlist
if normalize:
return normalized
else:
return retlist
def get_urls_from_text(data,configuration=None):
def get_urls_from_text(data,configuration=None,normalize=False):
normalized = set() # normalized url
normalized = [] # normalized url
retlist = [] # orig urls.
if not configuration:
@@ -109,12 +112,15 @@ def get_urls_from_text(data,configuration=None):
href = href.replace('&index=1','')
adapter = adapters.getAdapter(configuration,href)
if adapter.story.getMetadata('storyUrl') not in normalized:
normalized.add(adapter.story.getMetadata('storyUrl'))
normalized.append(adapter.story.getMetadata('storyUrl'))
retlist.append(href)
except:
pass
return retlist
if normalize:
return normalized
else:
return retlist
def form_url(parenturl,url):
url = url.strip() # ran across an image with a space in the
+14 -6
View File
@@ -352,7 +352,7 @@ class Story(Configurable):
allmetadata = {}
# special handling for authors/authorUrls
authlinkhtml="<a class='authorlink' href='%s'>%s</a>"
linkhtml="<a class='%slink' href='%s'>%s</a>"
if self.isList('author'): # more than one author, assume multiple authorUrl too.
htmllist=[]
for i, v in enumerate(self.getList('author')):
@@ -366,12 +366,18 @@ class Story(Configurable):
aurl=removeAllEntities(aurl)
auth=removeAllEntities(auth)
htmllist.append(authlinkhtml%(aurl,auth))
htmllist.append(linkhtml%('author',aurl,auth))
self.setMetadata('authorHTML',', '.join(htmllist))
else:
self.setMetadata('authorHTML',authlinkhtml%(self.getMetadata('authorUrl', removeallentities, doreplacements),
self.getMetadata('author', removeallentities, doreplacements)))
self.setMetadata('authorHTML',linkhtml%('author',self.getMetadata('authorUrl', removeallentities, doreplacements),
self.getMetadata('author', removeallentities, doreplacements)))
if self.getMetadataRaw('seriesUrl') != None:
self.setMetadata('seriesHTML',linkhtml%('series',self.getMetadata('seriesUrl', removeallentities, doreplacements),
self.getMetadata('series', removeallentities, doreplacements)))
elif self.getMetadataRaw('series') != None:
self.setMetadata('seriesHTML',self.getMetadataRaw('series'))
for k in self.getValidMetaList():
if self.isList(k) and keeplists:
allmetadata[k] = self.getList(k, removeallentities, doreplacements)
@@ -415,11 +421,13 @@ class Story(Configurable):
# includelist prevents infinite recursion of include_in_'s
if self.hasConfig("include_in_"+listname) and listname not in includelist:
for k in self.getConfigList("include_in_"+listname):
retlist.extend(self.getList(k,removeallentities,doreplacements,includelist=includelist+[listname]))
retlist.extend(self.getList(k,removeallentities=False,
doreplacements=doreplacements,includelist=includelist+[listname]))
else:
if not self.isList(listname):
retlist = [self.getMetadata(listname,removeallentities, doreplacements)]
retlist = [self.getMetadata(listname,removeallentities=False,
doreplacements=doreplacements)]
else:
retlist = self.getMetadataRaw(listname)
+4 -2
View File
@@ -57,7 +57,9 @@
<h3>Changes:</h3>
<p>
<ul>
<li>Change how fimfiction.net story descriptions are collected.</li>
<li>Another fix for fanfiction.net changes.</li>
<li>Add author to chapter TOC for multi-author stories on TtH and WraithBait.<br />
(AO3 doesn't reliably report chapter author.)</li>
</ul>
</p>
<p>
@@ -68,7 +70,7 @@
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
<a href="http://4-4-45.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-4-51.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
+16 -6
View File
@@ -44,6 +44,9 @@ language_label:Language
characters_label:Characters
ships_label:Relationships
series_label:Series
seriesUrl_label:Series URL
## seriesHTML is series as a link to seriesUrl.
seriesHTML_label:Series
## Completed/In-Progress
status_label:Status
## Dates story first published, last updated, and downloaded(last with time).
@@ -81,7 +84,7 @@ dateUpdated_format:%%Y-%%m-%%d
## You can include extra text or HTML that will be included as-is in
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
## All current formats already include title and author.
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
## Try to collect series name and number of this story in series.
## Some sites (ab)use 'series' for reading lists and personal
@@ -249,7 +252,7 @@ output_css:
[txt]
## Add URLs since there aren't links.
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
titlepage_entries: series,seriesUrl,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
## Width to word wrap text output. 0 indicates no wrapping.
wrap_width: 78
@@ -282,13 +285,13 @@ include_logpage: false
## if in the list. You can include extra text or HTML that will be
## included as-is in each log entry. Eg: logpage_entries: ...,<br />,
## summary,<br />,...
logpage_entries: dateCreated,datePublished,dateUpdated,numChapters,numWords,status,title,author,description,category,genre,rating,warnings
logpage_entries: dateCreated,datePublished,dateUpdated,numChapters,numWords,status,series,title,author,description,category,genre,rating,warnings
## epub->mobi conversions typically don't like tables.
titlepage_use_table: false
## When using tables, make these span both columns.
wide_titlepage_entries: description, storyUrl, author URL
wide_titlepage_entries: description, storyUrl, authorUrl, seriesUrl
## output background color--only used by html and epub (and ignored in
## epub by many readers). Included below in output_css--will be
@@ -951,6 +954,13 @@ extracategories:InuYasha
extracharacters:Sesshoumaru,Kagome
extraships:Sesshoumaru/Kagome
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
[www.dotmoon.net]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
@@ -1198,12 +1208,12 @@ extraships:Harry Potter/Ginny Weasley
# www.squidge.org/peja calls it Fandom <shrug>
category_label:Fandom
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
[www.squidge.org/peja:txt]
## Add URLs since there aren't links.
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
titlepage_entries: series,seriesUrl,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
[www.storiesofarda.com]
## Site dedicated to these categories/characters/ships